提交 0cc18764 作者: 洪东保

定时任务修改

父级 cdaad871
......@@ -170,7 +170,7 @@ public class RecordTemplateController {
* 模板测试效果
* @param file 用户自主上传的转录文件
* @param meetingInstId 历史会议主键id
* @param id 模板id
* @param content 模板提示词
* @return
*/
@PostMapping("/testGenerate")
......
......@@ -127,7 +127,6 @@ public class UnifiedController {
//取消预约会议
}
// //todo 待测试
// @GetMapping("/sameNameInsertTid")
// public void sameNameInsertTid(String corpid, String corpsecret) throws IOException {
// String weComToken = getWeComToken(corpid,corpsecret);
......
......@@ -43,17 +43,18 @@ public class CmeetingJob {
@Value("${isDev}")
private Boolean isDev;
// @PostConstruct
public void weComUserInit(){
// @PostConstruct
public void weComUserInit() {
weComUserSync();
}
// @PostConstruct
public void tencentUserInit(){
// @PostConstruct
public void tencentUserInit() {
TencentUserSync();
}
// @PostConstruct
public void userBindInit(){
// @PostConstruct
public void userBindInit() {
userBind();
}
......@@ -109,28 +110,28 @@ public class CmeetingJob {
log.info("-------关联企微腾会人员定时任务结束--------");
}
@Scheduled(fixedRate = 20 * 60 * 1000,initialDelay = 2 * 60 * 1000)
@Scheduled(fixedRate = 20 * 60 * 1000, initialDelay = 2 * 60 * 1000)
public void execute() {
if (isDev) {
return;
}
//查出企微id和腾会id的关联关系
List<UserId> userIdRelations = userIdMapper.selectList(null);
Map<String,String> widTidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getWid,UserId::getTid));
Map<String,String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid,UserId::getWid));
Map<String, String> widTidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getWid, UserId::getTid));
Map<String, String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid, UserId::getWid));
//查出企微的人员信息
List<WeComUser> weComUserList = weComService.list();
Map<String,WeComUser> weComUserMap = weComUserList.stream().collect(Collectors.toMap(WeComUser::getUserId, Function.identity()));
Map<String, WeComUser> weComUserMap = weComUserList.stream().collect(Collectors.toMap(WeComUser::getUserId, Function.identity()));
//智能体授权人员
List<UserDTO> accessUserIds = tencentMeetingService.getAccessUserIds(widTidRelations);
if (CollectionUtils.isEmpty(accessUserIds)) {
log.info("无生成纪要权限的人员");
return;
}else{
} else {
log.info("生成纪要权限人员:->{}", accessUserIds.stream().map(UserDTO::getWid).collect(Collectors.joining(",")));
}
List<TencentMeetingVO.RecordFile> meetingFiles = tencentMeetingService.getMeetingFiles(accessUserIds,weComUserMap);
List<TencentMeetingVO.RecordFile> meetingFiles = tencentMeetingService.getMeetingFiles(accessUserIds, weComUserMap);
if (meetingFiles == null || meetingFiles.isEmpty()) {
log.info("没有录制文件需要处理");
......@@ -141,14 +142,14 @@ public class CmeetingJob {
List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers = meetingRecordTemplateService.selectAuthorizedUsers();
// 提交处理任务
producer.submitBatchTasks(meetingFiles,authorizedUsers,tidWidRelations,Boolean.FALSE);
producer.submitBatchTasks(meetingFiles, authorizedUsers, tidWidRelations, Boolean.FALSE);
}
/**
* 定时扫描早于一小时之前的,所有未重试过的会议,重新生成纪要
*/
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 10 * 60 * 1000)
@Scheduled(fixedRate = 30 * 60 * 1000, initialDelay = 10 * 60 * 1000)
public void meetingMinutesRetry() {
if (isDev) {
return;
......@@ -160,9 +161,9 @@ public class CmeetingJob {
//查出所有早于一小时前的,生成失败且未重试过的会议
List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated,Boolean.FALSE)
.eq(MeetingInfo::getGenerateRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
.eq(MeetingInfo::getIsGenerated, Boolean.FALSE)
.eq(MeetingInfo::getGenerateRetry, Boolean.FALSE)
.le(MeetingInfo::getSyncTime, LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
......@@ -180,13 +181,13 @@ public class CmeetingJob {
//查出企微id和腾会id的关联关系
List<UserId> userIdRelations = userIdMapper.selectList(null);
Map<String,String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid,UserId::getWid));
Map<String, String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid, UserId::getWid));
//获取模板授权的人员
List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers = meetingRecordTemplateService.selectAuthorizedUsers();
// 提交处理任务
producer.submitBatchTasks(meetingFiles,authorizedUsers,tidWidRelations, Boolean.TRUE);
producer.submitBatchTasks(meetingFiles, authorizedUsers, tidWidRelations, Boolean.TRUE);
log.info("-------生成纪要重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
......@@ -196,7 +197,7 @@ public class CmeetingJob {
/**
* 定时扫描早于一小时之前的,所有邮件推送未重试过的会议,重新推送邮件
*/
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 15 * 60 * 1000)
@Scheduled(fixedRate = 30 * 60 * 1000, initialDelay = 15 * 60 * 1000)
public void emailPushRetry() {
if (isDev) {
return;
......@@ -208,11 +209,11 @@ public class CmeetingJob {
//查出所有早于一小时前的,邮件推送失败且未重试过的会议
List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated,Boolean.TRUE)
.eq(MeetingInfo::getEmailPushAccess,Boolean.TRUE)
.eq(MeetingInfo::getIsPushed,Boolean.FALSE)
.eq(MeetingInfo::getPushRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
.eq(MeetingInfo::getIsGenerated, Boolean.TRUE)
.eq(MeetingInfo::getEmailPushAccess, Boolean.TRUE)
.eq(MeetingInfo::getIsPushed, Boolean.FALSE)
.eq(MeetingInfo::getPushRetry, Boolean.FALSE)
.le(MeetingInfo::getSyncTime, LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
......@@ -229,9 +230,9 @@ public class CmeetingJob {
//查出企微id和腾会id的关联关系
List<UserId> userIdRelations = userIdMapper.selectList(null);
Map<String,String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid,UserId::getWid));
Map<String, String> tidWidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getTid, UserId::getWid));
// 提交处理任务
producer.submitEmailPushTasks(meetingFiles,tidWidRelations);
producer.submitEmailPushTasks(meetingFiles, tidWidRelations);
log.info("-------邮件推送重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
......
......@@ -12,6 +12,7 @@ import cn.chatbot.openai.service.LLMService;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
......@@ -24,6 +25,7 @@ import com.cmeeting.constant.UserAdminRouteConstant;
import com.cmeeting.dto.DocResultDto;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.email.EmailSender;
import com.cmeeting.exception.RobotBaseException;
import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
......@@ -86,23 +88,19 @@ public class FileProcessTask {
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;
private String llmApiAddr;
private Boolean finalRetry; //表示是兜底重试机制
private MeetingInfoMapper meetingInfoMapper;
private MinioUtils minioUtils;
private RedisUtils redisUtils;
private EmailSender emailSender;
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
private ProcessLogService processLogService;
//获取模板授权的人员
private List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers;
//腾会id企微id对应关系
private Map<String,String> tidWidRelations;
private Map<String, String> tidWidRelations;
private UserAdminConfig userAdminConfig;
//获取到的token
private String adminToken;
......@@ -112,16 +110,17 @@ public class FileProcessTask {
// 实际处理逻辑
public void process() {
// TODO 根据meeting和subMeetingId上锁
// boolean getKey = redisUtils.setnx("meeting_" + meetingId + subMeetingId, meetingId, 200000);
// while (getKey) {
// Thread.sleep(1000);
// }
boolean isSuccess = false;
while (retryCount <= MAX_RETRY && !isSuccess) {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
try {
//已保存的会议信息
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId));
String meetingDate = meetingInfo.getStartTime().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
//下面再查一遍会议信息的意义是,为了获取会议中的子会议Id,如果是周期会议,需要保存下来(重要)
......@@ -191,136 +190,63 @@ public class FileProcessTask {
// }
// 获取参会成员明细
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));
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse =
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
V1MeetingsMeetingIdParticipantsGet200Response participantsData = participantsResponse.getData();
V1MeetingsMeetingIdParticipantsGet200Response participantsData = TencentMeetingApiUtil.ApiV1MeetingsMeetingIdParticipantsGetRequest(meetingId, subMeetingId);
if (participantsData == null) {
throw new RobotBaseException("获取参会成员明细失败, meetingId: " + meetingId);
}
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = participantsData.getParticipants();
String participantNames = participants.stream().map(item -> new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、"));
meetingInfo.setParticipantUsers(participantNames);
//每场会议可能会分段录制,查出每个文件的转录记录后拼接
StringBuffer recordTextBuffer = new StringBuffer();
StringBuilder recordTextBuffer = new StringBuilder();
for (String recordFileId : recordFileIdList) {
//查询录制转写详情
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);
if(StringUtils.isNotEmpty(recordTextContent.replaceAll("\\n","").trim())){
recordTextBuffer.append("\n\n");
recordTextBuffer.append(recordTextContent);
}
}
}
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
recordTextBuffer.append(TencentMeetingApiUtil.ApiV1AddressesRecordFileIdGetRequest(recordFileId));
}
if(StringUtils.isEmpty(recordTextBuffer.toString().replaceAll("\\n","").trim())){
log.info("获取的转录文本为空,跳过纪要生成,meetingId:{},fileRecordId:{}",meetingId,recordFileIdList.toString());
processLogService.log(meetingId,subMeetingId,"获取的转录文本为空,跳过纪要生成");
if (StringUtils.isEmpty(recordTextBuffer.toString().replaceAll("\\n", "").trim())) {
log.info("获取的转录文本为空,跳过纪要生成,meetingId:{},fileRecordId:{}", meetingId, recordFileIdList.toString());
processLogService.log(meetingId, subMeetingId, "获取的转录文本为空,跳过纪要生成");
throw new RuntimeException("获取的转录文本为空,跳过纪要生成");
}
// 3. 处理文件 (调用Claude API等)
String choiceTemplateType = choiceTemplateType(meetingInfo.getSubject(),recordTextBuffer.toString());
log.info("choiceTemplateType->{}",choiceTemplateType);
//获取系统模板
List<MeetingRecordTemplate> recordTemplateList = meetingRecordTemplateMapper.selectList(
new LambdaQueryWrapper<MeetingRecordTemplate>().eq(MeetingRecordTemplate::getType, RecordTemplateConstant.TEMPLATE_TYPE_SYSTEM)
.eq(MeetingRecordTemplate::getIsDel,Boolean.FALSE).isNotNull(MeetingRecordTemplate::getTemplate));
Map<Integer, List<String>> authorizedUserMap = authorizedUsers.stream().collect(Collectors.toMap(item -> item.getRecordTemplateId(), item -> item.getUserIdList()));
// TODO 1. 根据转录文件内容recordTextBuffer判断使用模板类型
String choiceTemplateType = choiceTemplateType(meetingInfo.getSubject(), recordTextBuffer.toString());
log.info("choiceTemplateType->{}", choiceTemplateType);
// TODO 2. 获取这个会议需要使用的一个模板
// 2.1 封装一个方法:入参(a.工号/腾讯会议uid,b.choiceTemplateType);结果(一个模板MeetingRecordTemplate)
MeetingRecordTemplate template = new MeetingRecordTemplate();
List<EmailPush.Attachment> attachments = new ArrayList<>();
String hostUid = meetingInfo.getHostUid();
String toUserCode = tidWidRelations.get(meetingInfo.getHostUid());
if(!tidWidRelations.containsKey(hostUid)){
log.info("用户{}暂未关联企微信息,无法生成纪要文件",hostUid);
processLogService.log(meetingId,subMeetingId,"用户"+hostUid+"暂未关联企微信息,无法生成纪要文件");
if (!tidWidRelations.containsKey(hostUid)) {
log.info("用户{}暂未关联企微信息,无法生成纪要文件", hostUid);
processLogService.log(meetingId, subMeetingId, "用户" + hostUid + "暂未关联企微信息,无法生成纪要文件");
continue;
}
String processedResult = null;
for (MeetingRecordTemplate template : recordTemplateList) {
//判断本次纪要有模板生成权限
if(!authorizedUserMap.containsKey(template.getId())){
log.info("模板{}暂未授权给任意对象",template.getName());
processLogService.log(meetingId,subMeetingId,"模板"+template.getName()+"暂未授权给任意对象");
continue;
}
List<String> authorizedUserIds = authorizedUserMap.get(template.getId());
if(!authorizedUserIds.contains(tidWidRelations.get(hostUid))){
log.info("用户{}暂无模板{}权限",hostUid,template.getName());
processLogService.log(meetingId,subMeetingId,"用户"+hostUid+"暂无模板"+template.getName()+"权限");
continue;
}else{
log.info("用户{}允许应用模板{}",hostUid,template.getName());
processLogService.log(meetingId,subMeetingId,"用户"+hostUid+"允许应用模板"+template.getName());
}
//暂时让所有模板共用一个提示词,两个模板输出同样的结果
if(StringUtils.isEmpty(processedResult)){
processedResult = processWithClaude(recordTextBuffer.toString(),meetingDate,participantNames,template.getPrompt());
}
String minutesPath = saveResult(processedResult, recordTextBuffer.toString().getBytes(StandardCharsets.UTF_8), meetingInfo,toUserCode, template);
try(InputStream is = new FileInputStream(minutesPath)){
byte[] meetingMinutesBytes = IOUtils.toByteArray(is);
EmailPush.Attachment attachment = EmailPush.Attachment.builder().name(meetingInfo.getSubject()+"会议纪要_"+template.getName()).bytes(meetingMinutesBytes).build();
attachments.add(attachment);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
FileUtil.del(minutesPath);
}
String processedResult = processWithClaude(recordTextBuffer.toString(), meetingDate, participantNames, template.getPrompt());
String minutesPath = saveResult(processedResult, recordTextBuffer.toString().getBytes(StandardCharsets.UTF_8), meetingInfo, toUserCode, template);
try (InputStream is = new FileInputStream(minutesPath)) {
byte[] meetingMinutesBytes = IOUtils.toByteArray(is);
EmailPush.Attachment attachment = EmailPush.Attachment.builder().name(meetingInfo.getSubject() + "会议纪要_" + template.getName()).bytes(meetingMinutesBytes).build();
attachments.add(attachment);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
FileUtil.del(minutesPath);
}
if(CollectionUtils.isEmpty(attachments)){
log.info("用户{}暂无任何模板权限,纪要生成失败",hostUid);
if (CollectionUtils.isEmpty(attachments)) {
log.info("用户{}暂无任何模板权限,纪要生成失败", hostUid);
isSuccess = false;
continue;
}
if(!tidWidRelations.containsKey(meetingInfo.getHostUid())){
if (!tidWidRelations.containsKey(meetingInfo.getHostUid())) {
log.error("邮件推送重试失败: 主持人对应关系未配置。meetingId {}", meetingId);
processLogService.log(meetingId,subMeetingId,"邮件推送重试失败: 主持人对应关系未配置。meetingId "+meetingId);
processLogService.log(meetingId, subMeetingId, "邮件推送重试失败: 主持人对应关系未配置。meetingId " + meetingId);
continue;
}
EmailPush emailPushBuilder = EmailPush.builder()
.toEmail(meetingInfo.getEmail())
.meetingId(meetingId)
......@@ -338,20 +264,20 @@ public class FileProcessTask {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(meetingId,subMeetingId,sw.toString());
processLogService.log(meetingId, subMeetingId, sw.toString());
// 异常处理
retryCount++;
if (retryCount > MAX_RETRY) {
log.error("达到最大重试次数:meetingId {}", meetingId);
//如果是兜底重试,最终还是失败了,设置会议的重试状态为已重试
if(finalRetry){
if (finalRetry) {
meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getGenerateRetry,Boolean.TRUE));
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId)
.set(MeetingInfo::getGenerateRetry, Boolean.TRUE));
}
}else{
} else {
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
......@@ -364,7 +290,7 @@ public class FileProcessTask {
}
}
private byte[] downloadFile(String url) {
// 实现文件下载逻辑
OkHttpClient client = new OkHttpClient();
......@@ -373,14 +299,14 @@ public class FileProcessTask {
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().bytes();
}catch (Exception e){
} catch (Exception e) {
throw new RuntimeException("下载文件失败", e);
}
}
private String getRecordTextContent(byte[] fileData) {
// 定义文件路径和文件名
String fullPath = savePath + (System.currentTimeMillis() / 1000L) + ".docx";
String fullPath = savePath + (System.currentTimeMillis() / 1000L) + ".docx";
// 将字节内容写入本地文件
try (FileOutputStream fos = new FileOutputStream(fullPath)) {
fos.write(fileData);
......@@ -403,25 +329,62 @@ public class FileProcessTask {
/**
* 提供会议转录文件和会议主题,判断会议类型
* @param subject
* @param textContent
*
* @param subject 会议主题
* @param transcript 转录文件内容
* @return
*/
private String choiceTemplateType(String subject,String textContent) {
private String choiceTemplateType(String subject, String transcript) {
String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO";
String apiAddr = llmApiAddr + "/llm/sse-invoke";
String model = "arn:aws:bedrock:us-east-1:491822380689:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0";
int maxTokens = 5000;
String prompt = "请先对以下会议转写记录进行简要总结(不超过200字),然后根据会议主题以及总结内容判断该会议最可能属于哪种类型:\\n\\n会议主题: {0}\\n会议转写记录: {1}\\n\\n第一步:请简要总结会议的主要内容和目的(不超过200字)。\\n\\n第二步:根据上述总结和会议主题,将会议分类为以下类型之一:\\n1. 项目沟通会 - 与具体项目进展、问题讨论相关的会议\\n2. 重要会议 - 高层决策、战略规划等重要会议\\n3. 启动会 - 项目启动、活动筹备等初始会议\\n4. 其他 - 不符合以上任何分类\\n\\n分类权重判断规则:\\n- 基础权重分配:会议内容总结(70%)、会议主题(30%)\\n- 会议主题权重动态调整:\\n * 如果会议主题包含明确分类关键词(如\\\"启动会\\\"、\\\"项目沟通\\\"、\\\"战略决策\\\"等),则会议主题权重提升至60%,总结内容权重调整为40%\\n * 如果会议主题过于简单或模糊(如仅包含\\\"沟通\\\"、\\\"讨论\\\"、\\\"会议\\\"等通用词),则会议主题权重降低至15%,总结内容权重提升至85%\\n- 一致性判断:如果会议主题和总结内容指向不同分类,优先采用内容总结的分类,除非会议主题非常明确且规范\\n\\n输出格式:\\n请只返回分类名称(如\\\"项目沟通会\\\"),不要包含其他内容(如生成的会议总结等)。";
// String prompt = "请先对以下会议转写记录进行简要总结(不超过200字),然后根据会议主题以及总结内容判断该会议最可能属于哪种类型:\\n\\n会议主题: {0}\\n会议转写记录: {1}\\n\\n第一步:请简要总结会议的主要内容和目的(不超过200字)。\\n\\n第二步:根据上述总结和会议主题,将会议分类为以下类型之一:\\n1. 项目沟通会 - 与具体项目进展、问题讨论相关的会议\\n2. 重要会议 - 高层决策、战略规划等重要会议\\n3. 启动会 - 项目启动、活动筹备等初始会议\\n4. 其他 - 不符合以上任何分类\\n\\n分类权重判断规则:\\n- 基础权重分配:会议内容总结(70%)、会议主题(30%)\\n- 会议主题权重动态调整:\\n * 如果会议主题包含明确分类关键词(如\\\"启动会\\\"、\\\"项目沟通\\\"、\\\"战略决策\\\"等),则会议主题权重提升至60%,总结内容权重调整为40%\\n * 如果会议主题过于简单或模糊(如仅包含\\\"沟通\\\"、\\\"讨论\\\"、\\\"会议\\\"等通用词),则会议主题权重降低至15%,总结内容权重提升至85%\\n- 一致性判断:如果会议主题和总结内容指向不同分类,优先采用内容总结的分类,除非会议主题非常明确且规范\\n\\n输出格式:\\n请只返回分类名称(如\\\"项目沟通会\\\"),不要包含其他内容(如生成的会议总结等)。";
String prompt = "## 角色定义\n" +
"你是一位会议分类师,擅长对各种类型会议进行分类。\n" +
"\n" +
"请对以下会议转写记录进行全面而精炼的总结(尽量控制在500字以内,字数可以根据会议转写记录长度适当进行调整),然后根据会议主题以及总结内容判断该会议属于的类型:\n" +
"\n" +
"会议主题: {subject}\n" +
"会议转写记录: {transcript}\n" +
"\n" +
"识别会议类型步骤如下:\n" +
"第一步:识别会议名称/标题(如:项目例会,数据问题讨论);如果识别不出来,标注“暂无”;\n" +
"第二步:根据识别出来的会议名称进行分类(如:数据问题讨论,讨论会)。如果会议名称如:快速会议,张三预定的会议,分类为“其他”。如果是暂无,则进入第三步:\n" +
"第三步:总结会议的主要内容和目的(尽量控制在500字以内,字数可以根据会议转写记录长度适当进行调整)。确保涵盖所有关键讨论内容、重要决策和行动项,捕捉所有实质性内容。摘要应当完整反映会议的核心目的,不遗漏重要信息。\n" +
"\n" +
"第三步:根据上述内容和目的总结,将会议分类为以下类型之一:\n" +
"1. 项目例会 - 会议有周期标签。要讨论很多件事情,每件事情讨论时间很短。\n" +
"2. 启动会 - 项目启动、活动筹备等初始会议,各方代表发言。\n" +
"3. 讨论会 - 只针对一到两个事件针对性讨论,可能是讨论方案。\n" +
"4. 其他 - 不符合上面任意一类。\n" +
"\n" +
"\n" +
"输出格式:\n" +
"{{\n" +
" \"classification\": 类型\n" +
"}}\n" +
"\n" +
"注意:\n" +
"1.请严格按照输出格式以JSON格式返回数据,所有字段都必须包含。\n" +
"2.输出必须是严格有效的JSON格式,可以直接被解析。不要带有json标记,不要包含其他内容。";
//占位符信息替换
String formatPrompt = formatMessage(prompt, subject, textContent);
String formatPrompt = formatMessage(prompt, subject, transcript);
List<Message> messages = new ArrayList<>();
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), formatPrompt);
messages.add(chatMessage);
// 调用Claude API处理文件
String ret = call_llm(apiAddr, model, token, messages, maxTokens);
return ret;
String response = call_llm(apiAddr, model, token, messages, maxTokens);
JSONObject object = JSONObject.parseObject(response);
String retStr = "通用会议";
if (object != null) {
String classification = object.getString("classification");
if (StrUtil.isNotBlank(classification)) {
retStr = classification;
}
}
return retStr;
}
private String formatMessage(String pattern, Object... args) {
......@@ -430,13 +393,14 @@ public class FileProcessTask {
/**
* 大模型生成纪要xml
* @param textContent 转录文件
* @param meetingDate 会议日期
*
* @param textContent 转录文件
* @param meetingDate 会议日期
* @param participantNames 参会人员
* @param prompt 提示词
* @param prompt 提示词
* @return
*/
private String processWithClaude(String textContent, String meetingDate,String participantNames, String prompt) {
private String processWithClaude(String textContent, String meetingDate, String participantNames, String prompt) {
// //将文件传送给大模型处理
// String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO";
// String apiAddr = llmApiAddr + "/llm/sse-invoke";
......@@ -461,20 +425,21 @@ public class FileProcessTask {
llmApiAddr + "/llm/sse-invoke",
"Bearer AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO",
20000);
LLMResult llmResult = MeetingProcess.processMeeting(prompt, textContent,meetingDate,participantNames, baseLLM, new ArrayList<>());
LLMResult llmResult = MeetingProcess.processMeeting(prompt, textContent, meetingDate, participantNames, baseLLM, new ArrayList<>());
// DebugOutputTool.println(llmResult.respond);
return llmResult.respond;
}
/**
* 保存会议纪要相关的文件
* @param content 大模型返回的不规则xml
* @param recordData 转录文本
* @param meetingInfo 会议对象
* @param toUserCode 人员工号
*
* @param content 大模型返回的不规则xml
* @param recordData 转录文本
* @param meetingInfo 会议对象
* @param toUserCode 人员工号
* @param meetingRecordTemplate 模板信息
*/
private String saveResult(String content, byte[] recordData, MeetingInfo meetingInfo,String toUserCode,MeetingRecordTemplate meetingRecordTemplate) {
private String saveResult(String content, byte[] recordData, MeetingInfo meetingInfo, String toUserCode, MeetingRecordTemplate meetingRecordTemplate) {
String meetingName;
//转录文件临时存储路径
// String recordContentPath = meetingId + "-recordContent-" + IdUtil.fastSimpleUUID() + ".txt";
......@@ -508,17 +473,17 @@ public class FileProcessTask {
});
DocResultDto docResultDto = docResultDtoList.get(0);
String previewPath = docResultDto.getPreviewPath();
recordContentPath = previewPath.replaceAll(fileDownloadPath,"");
recordContentPath = previewPath.replaceAll(fileDownloadPath, "");
meetingInfo.setTransDocId(docResultDto.getId());
}else{
processLogService.log(meetingId,subMeetingId,"填充会议纪要失败,上传转录文件到向量知识库失败");
} else {
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败,上传转录文件到向量知识库失败");
throw new RuntimeException("填充会议纪要失败");
}
//去除内容中除了xml内容以外其他的信息,格式化xml
String xml = extractXmlFromMarkdown(content);
// minioUtils.upload(recordContentPath,recordData);
minioUtils.upload(recordXmlPath,xml.getBytes(StandardCharsets.UTF_8));
minioUtils.upload(recordXmlPath, xml.getBytes(StandardCharsets.UTF_8));
//将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml);
//判断会议名称关键词,如果是用户自己定义的主题,不做修改
......@@ -527,23 +492,23 @@ public class FileProcessTask {
// 3**的周期会议
// 4**预定的网络研讨会
String[] keywords = {"预定的会议", "的快速会议", "的周期会议", "预定的网络研讨会"};
boolean hostCustomSubject = Arrays.stream(keywords).noneMatch(item->subject.contains(item));
if(hostCustomSubject){
boolean hostCustomSubject = Arrays.stream(keywords).noneMatch(item -> subject.contains(item));
if (hostCustomSubject) {
meetingName = subject;
dataModel.put("meeting_name",subject);
}else{
dataModel.put("meeting_name", subject);
} else {
meetingName = dataModel.get("meeting_name") != null ? String.valueOf(dataModel.get("meeting_name")) : subject;
meetingInfo.setSubject(meetingName);
}
meetingMinutesFileName = meetingName + "_" + meetingRecordTemplate.getName();
//追加参会人员信息
Map<String,Object> participantsMap = new ConcurrentHashMap<>();
Map<String, Object> participantsMap = new ConcurrentHashMap<>();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String meetingDate = meetingInfo.getStartTime().toLocalDate().format(df);
participantsMap.put("meeting_date",meetingDate);
participantsMap.put("meeting_location","线上腾讯会议");
participantsMap.put("meeting_date", meetingDate);
participantsMap.put("meeting_location", "线上腾讯会议");
participantsMap.put("meeting_participants", meetingInfo.getParticipantUsers());
participantsMap.put("meeting_host",meetingInfo.getHost());
participantsMap.put("meeting_host", meetingInfo.getHost());
dataModel.putAll(participantsMap);
XWPFTemplate template;
......@@ -554,27 +519,27 @@ public class FileProcessTask {
}
meetingMinutesPath = savePath + meetingMinutesFileName + ".docx";
template.writeAndClose(new FileOutputStream(meetingMinutesPath));
processLogService.log(meetingId,subMeetingId,"填充会议纪要成功");
processLogService.log(meetingId, subMeetingId, "填充会议纪要成功");
} catch (Exception e) {
log.error("填充会议纪要失败: {}", e.getMessage(), e);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(meetingId,subMeetingId,"填充会议纪要失败"+sw.toString());
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败" + sw.toString());
throw new RuntimeException("填充会议纪要失败");
}
meetingInfoMapper.update(meetingInfo,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getRecordContent,recordContentPath)
.set(MeetingInfo::getRecordXml,recordXmlPath)
.set(MeetingInfo::getParticipantUsers,meetingInfo.getParticipantUsers())
.set(MeetingInfo::getIsGenerated,Boolean.TRUE)
.set(MeetingInfo::getTemplateId,meetingRecordTemplate.getId())
.set(MeetingInfo::getTransDocId,meetingInfo.getTransDocId())
.set(MeetingInfo::getSubject,meetingInfo.getSubject())
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId)
.set(MeetingInfo::getRecordContent, recordContentPath)
.set(MeetingInfo::getRecordXml, recordXmlPath)
.set(MeetingInfo::getParticipantUsers, meetingInfo.getParticipantUsers())
.set(MeetingInfo::getIsGenerated, Boolean.TRUE)
.set(MeetingInfo::getTemplateId, meetingRecordTemplate.getId())
.set(MeetingInfo::getTransDocId, meetingInfo.getTransDocId())
.set(MeetingInfo::getSubject, meetingInfo.getSubject())
);
meetingInfo.setRecordContent(recordContentPath);
meetingInfo.setRecordXml(recordXmlPath);
......@@ -592,17 +557,17 @@ public class FileProcessTask {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(meetingId,subMeetingId,"【邮件推送】:"+sw.toString());
processLogService.log(meetingId, subMeetingId, "【邮件推送】:" + sw.toString());
throw new RuntimeException(e);
}
if(isPushed)
processLogService.log(meetingId,subMeetingId,"用户允许邮件推送,推送邮件至"+ emailPushBuilder.getToEmail());
if (isPushed)
processLogService.log(meetingId, subMeetingId, "用户允许邮件推送,推送邮件至" + emailPushBuilder.getToEmail());
meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed,isPushed)
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId)
.set(MeetingInfo::getIsPushed, isPushed)
);
}
......@@ -615,14 +580,14 @@ public class FileProcessTask {
// 正确获取节点和属性的方式
List<Map> list = new ArrayList<>();
Iterator<String> iterator = rootNode.fieldNames();
while (iterator.hasNext()){
while (iterator.hasNext()) {
String tagName = iterator.next();
JsonNode subNode = rootNode.path(tagName);
String displayName = subNode.path("label").asText();
String content = subNode.path("").asText();
list.add(new HashMap(){{
put("key",tagName + "/" + displayName);
put("value",content);
list.add(new HashMap() {{
put("key", tagName + "/" + displayName);
put("value", content);
}});
}
// 构建JSON对象
......@@ -651,23 +616,24 @@ public class FileProcessTask {
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);
}
});
try {
stringBuilder.append(new String(o.getBytes(Charset.defaultCharset())));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
});
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(meetingId,subMeetingId,"【大模型处理异常】:"+sw.toString());
processLogService.log(meetingId, subMeetingId, "【大模型处理异常】:" + sw.toString());
throw new RuntimeException(e);
}
service.shutdownExecutor();
return stringBuilder.toString();
}
public static Map<String, Object> convertXmlToMap(String xml) throws Exception {
XmlMapper xmlMapper = new XmlMapper();
......@@ -677,12 +643,12 @@ public class FileProcessTask {
Map<?, ?> xmlMap = xmlMapper.readValue(xml, Map.class);
// 转换为更标准的 Map<String, Object>
Map<String,Object> map = objectMapper.convertValue(xmlMap, Map.class);
Map<String, Object> map = objectMapper.convertValue(xmlMap, Map.class);
//特殊处理map格式
for (Map.Entry<String, Object> entry : map.entrySet()) {
Map<String,Object> value = (Map<String, Object>) entry.getValue();
Map<String, Object> value = (Map<String, Object>) entry.getValue();
//取出正确的value并设置
String realValue = String.valueOf(value.get("")).replaceAll("^\\n+","");
String realValue = String.valueOf(value.get("")).replaceAll("^\\n+", "");
//内容段首移除换行,段末追加换行(会议名称结尾不换行
entry.setValue(realValue.endsWith("\n") || "meeting_name".equals(entry.getKey()) || "meeting_purpose".equals(entry.getKey()) ? realValue : realValue + "\n");
}
......@@ -691,6 +657,7 @@ public class FileProcessTask {
/**
* markdown转xml
*
* @param markdown
* @return
*/
......@@ -698,40 +665,35 @@ public class FileProcessTask {
StringBuffer sb;
try {
int start = markdown.indexOf("<");
if(start == -1){
processLogService.log(meetingId,subMeetingId,"markdown转xml失败,未输出正确的xml格式,markdown内容:"+markdown);
if (start == -1) {
processLogService.log(meetingId, subMeetingId, "markdown转xml失败,未输出正确的xml格式,markdown内容:" + markdown);
}
int end = markdown.lastIndexOf(">") + 1;
sb = new StringBuffer();
sb.append("<root>");
String xml = markdown.substring(start, end).trim().replaceAll("\n\n","\n");
String xml = markdown.substring(start, end).trim().replaceAll("\n\n", "\n");
sb.append(xml);
sb.append("</root>");
} catch (Exception e) {
log.info("markdown转xml,markdown->{}",markdown);
log.info("markdown转xml,markdown->{}", markdown);
throw new RuntimeException(e.getMessage());
}
return sb.toString();
}
public FileProcessTask(List<String> recordFileIdList, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, String tencentAppId,
String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender, MeetingRecordTemplateMapper meetingRecordTemplateMapper,
String llmApiAddr, Boolean finalRetry, ProcessLogService processLogService,List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers,Map<String,String> tidWidRelations,
UserAdminConfig userAdminConfig, String adminToken, String applicationId,String fileDownloadPath, String permTenantId) {
public FileProcessTask(List<String> recordFileIdList, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender, MeetingRecordTemplateMapper meetingRecordTemplateMapper,
String llmApiAddr, Boolean finalRetry, ProcessLogService processLogService, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations,
UserAdminConfig userAdminConfig, String adminToken, String applicationId, String fileDownloadPath, String permTenantId) {
this.recordFileIdList = recordFileIdList;
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;
this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils;
this.redisUtils = redisUtils;
this.emailSender = emailSender;
this.meetingRecordTemplateMapper = meetingRecordTemplateMapper;
this.llmApiAddr = llmApiAddr;
......
......@@ -37,7 +37,7 @@ public class MeetingInfo implements Serializable {
* 会议主题
*/
private String subject;
/**
* 会议ID(字符串类型)
*/
......@@ -47,12 +47,12 @@ public class MeetingInfo implements Serializable {
* 子会议ID
*/
private String subMeetingId;
/**
* 会议号码
*/
private String meetingCode;
/**
* 主持人
*/
......@@ -67,14 +67,14 @@ public class MeetingInfo implements Serializable {
* 参会人员名单
*/
private String participantUsers;
/**
* 会议开始时间(时间戳)
*/
@DateTimeFormat(pattern = "yyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private LocalDateTime startTime;
/**
* 会议结束时间(时间戳)
*/
......@@ -89,6 +89,7 @@ public class MeetingInfo implements Serializable {
/**
* 推送邮件许可 为false不推送
*/
private Boolean emailGenerateAccess;
private Boolean emailPushAccess;
/**
* 是否推送邮件完成
......
......@@ -9,6 +9,7 @@ import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.UserId;
import com.cmeeting.util.MinioUtils;
import com.cmeeting.util.RedisUtils;
import com.cmeeting.util.UserAdminConfig;
import com.cmeeting.util.UserAdminTokenUtil;
import com.cmeeting.vo.TencentMeetingVO;
......@@ -29,22 +30,12 @@ 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;
@Value(value = "${llm.api-addr}")
private String llmApiAddr;
@Value(value = "${tencent.base-save-path}")
......@@ -60,6 +51,8 @@ public class FileProcessProducer {
@Resource
private MinioUtils minioUtils;
@Resource
private RedisUtils redisUtils;
@Resource
private EmailSender emailSender;
@Resource
private ProcessLogService processLogService;
......@@ -71,59 +64,56 @@ public class FileProcessProducer {
/**
* 批量提交生成纪要任务
* @param recordFiles 转录文件信息
*
* @param recordFiles 转录文件信息
* @param authorizedUsers 模板授权的人员
* @param tidWidRelations 腾会id企微id对应关系
* @param finalRetry 是否为最终重试
* @param finalRetry 是否为最终重试
* @param finalRetry
*/
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String,String> tidWidRelations, Boolean finalRetry) {
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations, Boolean finalRetry) {
List<Future<?>> futures = new ArrayList<>();
String adminToken = UserAdminTokenUtil.getUserAdminToken();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务
FileProcessTask task = new FileProcessTask(
recordFile.getRecordFileIdList(),
recordFile.getMeetingId(),
recordFile.getSubMeetingId(),
baseSavePath,
Collections.emptyMap(),
tencentAppId,
tencentSdkId,
tencentSecretId,
tencentSecretKey,
tencentAdminUserId,
meetingInfoMapper,
minioUtils,
emailSender,
meetingRecordTemplateMapper,
llmApiAddr,
finalRetry,
processLogService,
authorizedUsers,
tidWidRelations,
userAdminConfig,
adminToken,
applicationId,
fileDownloadPath,
permTenantId
recordFile.getRecordFileIdList(),
recordFile.getMeetingId(),
recordFile.getSubMeetingId(),
baseSavePath,
Collections.emptyMap(),
meetingInfoMapper,
minioUtils,
redisUtils,
emailSender,
meetingRecordTemplateMapper,
llmApiAddr,
finalRetry,
processLogService,
authorizedUsers,
tidWidRelations,
userAdminConfig,
adminToken,
applicationId,
fileDownloadPath,
permTenantId
);
// 提交任务到线程池
Future<?> future = fileProcessExecutor.submit(() -> {
task.process();
callbackHandler.onComplete(task); // 回调处理
});
futures.add(future);
}
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures);
}
// 批量提交邮箱推送重试任务
public void submitEmailPushTasks(List<TencentMeetingVO.RecordFile> recordFiles,Map<String,String> tidWidRelations) {
public void submitEmailPushTasks(List<TencentMeetingVO.RecordFile> recordFiles, Map<String, String> tidWidRelations) {
List<Future<?>> futures = new ArrayList<>();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
......@@ -151,7 +141,7 @@ public class FileProcessProducer {
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures);
}
private void monitorTaskCompletion(List<Future<?>> futures) {
new Thread(() -> {
for (Future<?> future : futures) {
......
......@@ -302,7 +302,7 @@ public class RecordTemplatePermissionServiceImpl extends ServiceImpl<RecordTempl
* @return
*/
private String extractXmlFromMarkdown(String markdown) {
StringBuffer sb;
StringBuffer sb = null;
try {
int start = markdown.indexOf("<");
int end = markdown.lastIndexOf(">") + 1;
......@@ -313,7 +313,6 @@ public class RecordTemplatePermissionServiceImpl extends ServiceImpl<RecordTempl
sb.append("</root>");
} catch (Exception e) {
log.info("markdown转xml,markdown->{}",markdown);
throw new RuntimeException(e.getMessage());
}
return sb.toString();
}
......
......@@ -51,7 +51,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TencentMeetingService {
public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, TencentMeetingUser> implements TencentMeetingService {
@Resource
private TecentMeetingMapper tecentMeetingMapper;
@Resource
......@@ -84,8 +84,6 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
private String permissionApplicationId;
@Value(value = "${permission.tenantId}")
private String permissionTenantId;
@Resource
private TencentMeetingApiUtil tencentMeetingApiUtil;
@Override
public void batchInsert(List<TencentMeetingUser> users) {
......@@ -96,7 +94,8 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
@Override
public void doUsers() {
// 获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches();;
List<TencentMeetingUser> users = fetchUsersInBatches();
;
// 检查重名并设置标志
markDuplicateNamesTecent(users);
// 批量插入数据库(分批次)
......@@ -110,19 +109,15 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
/**
* 获取会议信息以及会议对应的转录文件信息
*
* @param accessUserIds 允许生成会议纪要的人员
* @param weComUserMap 企微可见范围内现有的人员
* @param weComUserMap 企微可见范围内现有的人员
* @return
*/
@Override
public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds,Map<String, WeComUser> weComUserMap) {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds, Map<String, WeComUser> weComUserMap) {
List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>();
List<TencentMeetingVO.RecordFile> recordFileUrlList = new ArrayList<>();
// List<TencentMeetingVO.RecordFile> recordFileUrlList = new ArrayList<>();
List<MeetingInfo> meetingSaveList = new ArrayList<>();
// 查询近searchDays天的会议录制列表
try {
......@@ -139,148 +134,120 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
List<TencentMeetingVO.SimpleMeetingInfo> meetingIds = meetingInfoMapper.getAllMeetingIds();
List<TencentMeetingUser> meetingUsers = tecentMeetingMapper.getAlluser();
Map<String, String> meetingMap = meetingUsers.stream().collect(Collectors.toMap(TencentMeetingUser::getUserId, TencentMeetingUser::getUserName));
while (currentPage.intValue() <= totalPage){
while (currentPage.intValue() <= totalPage) {
CorpRecordsVO data = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, currentPage.getAndIncrement(), 20);
//设置总页数
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
List<CorpRecordsVO.RecordMeeting> meetings = data.getRecordMeetings();
for (CorpRecordsVO.RecordMeeting meeting : meetings) {
//录制文件未转码完成,跳过 1:录制中 2:转码中 3:转码完成
if(meeting.getState() != 3){
if (meeting.getState() != 3) {
continue;
}
log.info("【会议检索】转录文件的meetingId->{},recordFileId->{}",meeting.getMeetingId(),meeting.getMeetingRecordId());
log.info("【会议检索】转录文件的meetingId->{},recordFileId->{}", meeting.getMeetingId(), meeting.getMeetingRecordId());
//查询会议详情
String meetingId = meeting.getMeetingId();
String subMeetingId = null;
LocalDateTime mediaStartTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.valueOf(meeting.getMediaStartTime())), ZoneId.systemDefault());
try {
String userid = meeting.getUserid();
log.info("【周期会议扫描】:查询用户的已结束会议列表...meetingCode->{},userId->{}",meeting.getMeetingCode(), userid);
log.info("【周期会议扫描】:查询用户的已结束会议列表...meetingCode->{},userId->{}", meeting.getMeetingCode(), userid);
//获取子会议id
MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest historyMeetingRequest =
new MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest.Builder(userid)
.pageSize("20")
.page("1")
.meetingCode(meeting.getMeetingCode())
.startTime(String.valueOf(mediaStartTime.toLocalDate().atStartOfDay().atZone(ZoneId.systemDefault()).toEpochSecond()))
.endTime(String.valueOf(mediaStartTime.toLocalDate().atStartOfDay().plusDays(1).atZone(ZoneId.systemDefault()).toEpochSecond()))
.build();
MeetingsApi.ApiV1HistoryMeetingsUseridGetResponse historyMeetingResponse =
client.meetings().v1HistoryMeetingsUseridGet(historyMeetingRequest, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()))).timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
V1HistoryMeetingsUseridGet200Response historyMeetingResponseData = historyMeetingResponse.getData();
List<V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner> historyMeetingInfoList = historyMeetingResponseData.getMeetingInfoList();
if(CollectionUtils.isEmpty(historyMeetingInfoList)){
V1HistoryMeetingsUseridGet200Response v1HistoryMeetingsUseridGet200Response = TencentMeetingApiUtil.ApiV1HistoryMeetingsUseridGetRequest(userid, meeting.getMeetingCode(),
String.valueOf(mediaStartTime.toLocalDate().atStartOfDay().atZone(ZoneId.systemDefault()).toEpochSecond()),
String.valueOf(mediaStartTime.toLocalDate().atStartOfDay().plusDays(1).atZone(ZoneId.systemDefault()).toEpochSecond()));
if (v1HistoryMeetingsUseridGet200Response == null) {
continue;
}
List<V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner> historyMeetingInfoList = v1HistoryMeetingsUseridGet200Response.getMeetingInfoList();
if (CollectionUtils.isEmpty(historyMeetingInfoList)) {
log.error("会议未结束,获取子会议id信息失败");
continue;
}
V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner historyMeeting = historyMeetingInfoList.get(0);
//如果是周期会议
if(historyMeeting.getMeetingType() == 1){
if (historyMeeting.getMeetingType() == 1) {
subMeetingId = historyMeeting.getSubMeetingId();
}
//如果数据库中已有相同会议id的记录,跳过同步
String finalSubMeetingId = subMeetingId;
if(!meetingIds.stream().anyMatch(item->item.getMeetingId().equals(meetingId) && Objects.equals(item.getSubMeetingId(), finalSubMeetingId))){
log.info("【会议检索】新的会议meetingId->{}",meeting.getMeetingId());
if (meetingIds.stream().noneMatch(item -> item.getMeetingId().equals(meetingId) && Objects.equals(item.getSubMeetingId(), finalSubMeetingId))) {
log.info("【会议检索】新的会议meetingId->{}", meeting.getMeetingId());
List<CorpRecordsVO.RecordFile> recordFiles = meeting.getRecordFiles();
//按转录文件时间升序,便于后续的内容拼接
List<String> recordFileIdList = recordFiles.stream().sorted(Comparator.comparingLong(CorpRecordsVO.RecordFile::getRecordStartTime))
.map(CorpRecordsVO.RecordFile::getRecordFileId).collect(Collectors.toList());
TencentMeetingVO.RecordFile recordFileItem = TencentMeetingVO.RecordFile.builder()
.recordFileIdList(recordFileIdList).meetingId(meetingId).subMeetingId(subMeetingId).build();
// TencentMeetingVO.RecordFile recordFileItem = TencentMeetingVO.RecordFile.builder()
// .recordFileIdList(recordFileIdList).meetingId(meetingId).subMeetingId(subMeetingId).build();
String hostId;
String hostName;
//优先使用会议列表中已有的主持人字段
if(StringUtils.isNotEmpty(meeting.getHostUserId())){
if (StringUtils.isNotEmpty(meeting.getHostUserId())) {
hostId = meeting.getHostUserId();
hostName = meetingMap.containsKey(hostId) ? meetingMap.get(hostId) :null;
hostName = meetingMap.getOrDefault(hostId, null);
log.info("从会议列表中成功获取到主持人信息");
}else{
} else {
//判断主持人是否存在,如果主持人未参会,是查不到主持人的
//如果主持人未参会,使用会议详情中的创建人作为主持人
MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdGetRequest.Builder(meetingId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.instanceid("0")
.build();
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();
V1MeetingsMeetingIdGet200Response meetingResponseData = TencentMeetingApiUtil.ApiV1MeetingsMeetingIdGetRequest(meetingId);
if (meetingResponseData == null) {
continue;
}
List<V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner> meetingInfoList = meetingResponseData.getMeetingInfoList();
V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner meetingInfo = meetingInfoList.get(0);
//会议详情中有主持人信息
if(!CollectionUtils.isEmpty(meetingInfo.getCurrentHosts())){
if (!CollectionUtils.isEmpty(meetingInfo.getCurrentHosts())) {
log.info("尝试从会议详情中获取主持人信息");
hostId = meetingInfo.getCurrentHosts().get(0).getUserid();
hostName = meetingMap.containsKey(hostId) ? meetingMap.get(hostId) :null;
// processLogService.log(meeting.getMeetingId(),subMeetingId,"未找到主持人,默认会议创建人为主持人");
}else{
hostName = meetingMap.getOrDefault(hostId, null);
} else {
log.info("尝试从参会人员列表中获取主持人信息");
// 获取参会成员明细
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));
//主持人角色,以下角色都可以表示主持人
//用户角色:
//0:普通成员角色
//1:创建者角色
//2:主持人
//3:创建者+主持人
//4:游客
//5:游客+主持人
//6:联席主持人
//7:创建者+联席主持人
List<Long> hostRoleList = Arrays.asList(2L,3L,5L,6L,7L);
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse =
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
V1MeetingsMeetingIdParticipantsGet200Response participantsData = participantsResponse.getData();
// 用户角色
// 0:普通成员角色 1:创建者角色 2:主持人 3:创建者+主持人 4:游客 5:游客+主持人 6:联席主持人 7:创建者+联席主持人
// 主持人角色
List<Long> hostRoleList = Arrays.asList(2L, 3L, 5L, 6L, 7L);
V1MeetingsMeetingIdParticipantsGet200Response participantsData = TencentMeetingApiUtil.ApiV1MeetingsMeetingIdParticipantsGetRequest(meetingId, subMeetingId);
if (participantsData == null) {
continue;
}
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = participantsData.getParticipants();
Optional<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> host = participants.stream().filter(item -> hostRoleList.contains(item.getUserRole())).findFirst();
if(host.isPresent()) {
if (host.isPresent()) {
hostId = host.get().getUserid();
hostName = new String(Base64.getDecoder().decode(host.get().getUserName()));
}else{
} else {
log.error("未找到主持人,默认没有生成纪要权限");
// processLogService.log(meeting.getMeetingId(),subMeetingId,"未找到主持人,默认没有生成纪要权限");
continue;
}
}
}
//判断是否有权限生成纪要
boolean generateAccess = accessUserIds.stream().anyMatch(item -> item.getTid().equals(hostId));
if(!generateAccess){
log.error("【权限校验】主持人{}没有生成纪要权限,跳过生成",hostId);
// processLogService.log(meeting.getMeetingId(),subMeetingId,"【权限校验】主持人"+hostId+"没有生成纪要权限,跳过生成");
continue;
}
log.info("【权限校验】主持人{}允许生成纪要",hostId);
// processLogService.log(meeting.getMeetingId(),subMeetingId,"【权限校验】主持人"+hostId+"允许生成纪要");
boolean generateAccess;
boolean emailPushAccess;
// 1. 智能体需要授权
generateAccess = accessUserIds.stream().anyMatch(item -> item.getTid().equals(hostId));
log.info("智能体是否授权: {}", generateAccess);
UserDTO userDTO = accessUserIds.stream().filter(item -> item.getTid().equals(hostId)).findFirst().get();
String email = userDTO.getEmail();
Boolean emailPushAccess = weComUserMap.containsKey(userDTO.getWid())
? weComUserMap.get(userDTO.getWid()).getEmailPushAccess() : Boolean.FALSE;
// TODO 要改
// 2. 腾讯会议和企业微信信息同步过并且绑定关系
// 3. 用户自己没有关闭会议纪要功能
// 满足以上所有则emailGenerateAccess为ture
generateAccess = weComUserMap.containsKey(userDTO.getWid()) ? weComUserMap.get(userDTO.getWid()).getEmailPushAccess() : Boolean.FALSE;
// 是否推送邮箱
emailPushAccess = weComUserMap.containsKey(userDTO.getWid()) ? weComUserMap.get(userDTO.getWid()).getEmailPushAccess() : Boolean.FALSE;
log.info("推送邮箱: {}", emailPushAccess);
// 查询会议开始和结束时间
MeetingInfo startAndEndTimeByMeetingId = tencentMeetingApiUtil.getStartAndEndTimeByMeetingId(meetingId);
MeetingInfo startAndEndTimeByMeetingId = TencentMeetingApiUtil.getStartAndEndTimeByMeetingId(meetingId);
//会议基本信息保存
MeetingInfo meetingItem = MeetingInfo.builder().meetingId(meetingId).meetingCode(meeting.getMeetingCode())
.subject(meeting.getSubject())
.startTime(startAndEndTimeByMeetingId != null ? startAndEndTimeByMeetingId.getStartTime() : mediaStartTime)
.endTime(startAndEndTimeByMeetingId != null ? startAndEndTimeByMeetingId.getEndTime() : null)
.isGenerated(Boolean.FALSE).emailPushAccess(emailPushAccess).isPushed(Boolean.FALSE).syncTime(LocalDateTime.now())
.isGenerated(Boolean.FALSE).emailGenerateAccess(generateAccess)
.emailPushAccess(emailPushAccess).isPushed(Boolean.FALSE).syncTime(LocalDateTime.now())
.subMeetingId(subMeetingId).generateRetry(Boolean.FALSE).pushRetry(Boolean.FALSE)
.host(hostName)
.hostUid(hostId)
......@@ -289,17 +256,16 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
.recordFileId(String.join(",", recordFileIdList))
.email(email)
.build();
recordFileUrlList.add(recordFileItem);
// recordFileUrlList.add(recordFileItem);
meetingSaveList.add(meetingItem);
}
} catch (Exception e) {
e.printStackTrace();
continue;
log.error(e.getMessage());
}
}
}
}
if(meetingSaveList.size() > 0){
if (meetingSaveList.size() > 0) {
Map<String, List<MeetingInfo>> meetingSaveMap = meetingSaveList.stream().collect(Collectors.groupingBy(
item -> item.getMeetingId() + "_" +
(item.getSubMeetingId() != null ? item.getSubMeetingId() : "null")));
......@@ -321,7 +287,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(null,null,sw.toString());
processLogService.log(null, null, sw.toString());
throw new RuntimeException(e.getMessage());
}
return meetingFiles;
......@@ -329,12 +295,13 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
/**
* 获取有权限的人员
*
* @param userIdRelations
* @return
*/
@Override
public List<UserDTO> getAccessUserIds(Map<String,String> userIdRelations) {
try{
public List<UserDTO> getAccessUserIds(Map<String, String> userIdRelations) {
try {
//权限控制
List<CoreModulePermissions> auths = authMapper.getAuthByTargetId(permissionApplicationId, permissionTenantId);
//授权的部门,type为1的为人员,为0表示部门
......@@ -343,25 +310,25 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
for (CoreModulePermissions authDept : authDepts) {
String deptId = authDept.getRelId();
String tenantId = authDept.getTenantId();
getDeptPath(deptPath,deptId,tenantId);
getDeptPath(deptPath, deptId, tenantId);
}
//已被授权部门下的userid
List<String> accessUserIds = !CollectionUtils.isEmpty(deptPath) ? sysUserSyncMapper.getUsersByDept(deptPath,permissionTenantId) : new ArrayList<>();
List<String> accessUserIds = !CollectionUtils.isEmpty(deptPath) ? sysUserSyncMapper.getUsersByDept(deptPath, permissionTenantId) : new ArrayList<>();
//已被直接授权的人员追加进去
accessUserIds.addAll(auths.stream().filter(item -> item.getType().equals(1)).map(CoreModulePermissions::getRelId).collect(Collectors.toList()));
if(!CollectionUtils.isEmpty(accessUserIds)){
if (!CollectionUtils.isEmpty(accessUserIds)) {
//查出人员邮箱+邮件推送许可
List<UserDTO> userEmailList = sysUserSyncMapper.getUserEmail(permissionTenantId);
Map<String, UserDTO> userEmailMap = CollectionUtils.isEmpty(userEmailList) ? new HashMap<>()
: userEmailList.stream().collect(Collectors.toMap(UserDTO::getWid, Function.identity(),(existing, replacement) -> existing));
: userEmailList.stream().collect(Collectors.toMap(UserDTO::getWid, Function.identity(), (existing, replacement) -> existing));
List<UserDTO> accessUsers = new ArrayList<>();
for (String accessUserId : accessUserIds) {
if(userIdRelations.containsKey(accessUserId)){
if (userIdRelations.containsKey(accessUserId)) {
UserDTO accessUser = new UserDTO();
accessUser.setWid(accessUserId);
accessUser.setTid(userIdRelations.get(accessUserId));
if(userEmailMap.containsKey(accessUserId)){
if (userEmailMap.containsKey(accessUserId)) {
UserDTO emailMsg = userEmailMap.get(accessUserId);
accessUser.setEmail(emailMsg.getEmail());
accessUser.setEmailPushAccess(emailMsg.getEmailPushAccess());
......@@ -372,31 +339,32 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
return accessUsers;
}
return new ArrayList<>();
}catch (Exception e){
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(null,null,sw.toString());
processLogService.log(null, null, sw.toString());
throw new RuntimeException(e);
}
}
/**
* 获取部门的路径
*
* @param deptPath
* @param deptId
* @param tenantId
*/
private void getDeptPath(List<String> deptPath, String deptId, String tenantId) {
if(!deptPath.contains(deptId)) deptPath.add(deptId);
List<String> subDeptIds = sysUserSyncMapper.getSubDeptId(deptId,tenantId);
if(CollectionUtils.isEmpty(subDeptIds)) return;
if (!deptPath.contains(deptId)) deptPath.add(deptId);
List<String> subDeptIds = sysUserSyncMapper.getSubDeptId(deptId, tenantId);
if (CollectionUtils.isEmpty(subDeptIds)) return;
for (String subDeptId : subDeptIds) {
//部门id去重
if(!deptPath.contains(subDeptId)){
if (!deptPath.contains(subDeptId)) {
deptPath.add(subDeptId);
getDeptPath(deptPath,subDeptId,tenantId);
getDeptPath(deptPath, subDeptId, tenantId);
}
}
......@@ -404,6 +372,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
/**
* 拉取账户级的会议记录列表
*
* @param operatorId
* @param operatorIdType
* @param startTime
......@@ -469,10 +438,11 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
/**
* 腾讯会议通过通讯录获取员工信息
*
* @return
*/
private List<TencentMeetingUser> fetchUsersInBatches() {
if(redisUtils.hasKey("TENCENT_USER_ARRAY")){
if (redisUtils.hasKey("TENCENT_USER_ARRAY")) {
return (List<TencentMeetingUser>) redisUtils.get("TENCENT_USER_ARRAY");
}
//构造client客户端
......@@ -553,7 +523,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
throw new RuntimeException(e);
}
}
redisUtils.set("TENCENT_USER_ARRAY",resultList);
redisUtils.set("TENCENT_USER_ARRAY", resultList);
// 输出结果
System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", resultList.size());
return resultList;
......@@ -600,7 +570,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,T
return null;
}
String hostUserId = meetingInfo.getCurrentHosts().get(0).getUserid();
log.info("成功获取主持人userid: {}",hostUserId);
log.info("成功获取主持人userid: {}", hostUserId);
return hostUserId;
} catch (Exception e) {
return null;
......
......@@ -141,6 +141,15 @@ public class RedisUtils {
}
}
public boolean setnx(String key, Object value, long time) {
try {
return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
......
......@@ -11,11 +11,15 @@ 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.meetings.model.V1MeetingsMeetingIdParticipantsGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner;
import com.tencentcloudapi.wemeet.service.meetings.model.*;
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.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
......@@ -24,6 +28,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
......@@ -32,6 +37,7 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
......@@ -70,6 +76,7 @@ public class TencentMeetingApiUtil {
tencentAdminUserId = tmpAdminUserId;
client = getClient();
}
private Client getClient() {
if (client == null) {
synchronized (TencentMeetingApiUtil.class) {
......@@ -88,6 +95,7 @@ public class TencentMeetingApiUtil {
/**
* 查询会议开始结束时间
* Api: /v1/meetings/{meeting_id}
*
* @param meetingId 腾讯会议id
* @return
*/
......@@ -119,81 +127,159 @@ public class TencentMeetingApiUtil {
return null;
}
/**
* 查询用户已结束会议列表
*
* @param userId 用户id 必填
* @param meetingCode 会议码 非必填
* @param startTime 会议开始时间 非必填
* @param endTime 会议结束时间 非必填
* @return 已结束会议列表
* @throws ServiceException
* @throws ClientException
*/
public static V1HistoryMeetingsUseridGet200Response ApiV1HistoryMeetingsUseridGetRequest(String userId, String meetingCode, String startTime, String endTime) {
MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest historyMeetingRequest =
new MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest.Builder(userId)
.pageSize("20")
.page("1")
.meetingCode(meetingCode)
.startTime(startTime)
.endTime(endTime)
.build();
MeetingsApi.ApiV1HistoryMeetingsUseridGetResponse historyMeetingResponse = null;
try {
historyMeetingResponse = client.meetings().v1HistoryMeetingsUseridGet(historyMeetingRequest, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()))).timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
return historyMeetingResponse.getData();
} catch (ClientException | ServiceException e) {
log.error("ApiV1HistoryMeetingsUseridGetRequest error: {}", e.getMessage());
}
return null;
}
private void getSomething() throws Exception {
int total = 100000000;
int count = 0;
ZonedDateTime now = ZonedDateTime.now();
long startTime = now.minusDays(31).toEpochSecond();
long endTime = now.toEpochSecond();
String TENCENT_APPID = "210468336";
String TENCENT_SDKID = "28790143843";
String TENCENT_SECRETID = "0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL";
String TENCENT_SECRETKEY = "gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy";
String TENCENT_ADMIN_USERID = "woaJARCQAAftcvU6GGoOn66rdSZ4IrOA";
int i = 3;
while (i * 20 < total) {
String uri = String.format(
"/v1/corp/records?start_time=%d&end_time=%d&page=%d&page_size=%d&operator_id=%s&operator_id_type=%d",
startTime, endTime, i++, 20, "woaJARCQAAftcvU6GGoOn66rdSZ4IrOA", 1
);
String httpMethod = "GET";
String nonce = String.valueOf(new Random().nextInt(100000));
String timestamp = String.valueOf(Instant.now().getEpochSecond());
// 3. 生成签名
String signature = SignatureUtil.generateSignature(TENCENT_SECRETID, TENCENT_SECRETKEY, httpMethod, nonce, timestamp, uri, "");
// 4. 发送请求
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet("https://api.meeting.qq.com" + uri);
request.setHeader("X-TC-Key", TENCENT_SECRETID);
request.setHeader("X-TC-Timestamp", timestamp);
request.setHeader("X-TC-Nonce", nonce);
request.setHeader("X-TC-Signature", signature);
request.setHeader("AppId", TENCENT_APPID);
request.setHeader("SdkId", TENCENT_SDKID);
// 5. 解析响应
String response = EntityUtils.toString(httpClient.execute(request).getEntity());
JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject();
Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
// 将 JsonObject 转换为实体类
CorpRecordsVO corpRecords = gson.fromJson(jsonResponse, CorpRecordsVO.class);
if (corpRecords == null) {
return;
}
for (CorpRecordsVO.RecordMeeting recordMeeting : corpRecords.getRecordMeetings()) {
String subject = recordMeeting.getSubject();
if (subject.contains("启动会")) {
count++;
System.out.println("会议名: " + subject);
String meetingRecordId = recordMeeting.getMeetingRecordId();
System.out.println("meetingRecordId: " + meetingRecordId);
List<CorpRecordsVO.RecordFile> recordFiles = recordMeeting.getRecordFiles();
if (CollUtil.isEmpty(recordFiles)) {
continue;
}
System.out.print("recordFileId: ");
for (CorpRecordsVO.RecordFile recordFile : recordFiles) {
String recordFileId = recordFile.getRecordFileId();
System.out.print(recordFileId + ",");
/**
* 根据会议id查询会议详情
*
* @param meetingId
* @return
*/
public static V1MeetingsMeetingIdGet200Response ApiV1MeetingsMeetingIdGetRequest(String meetingId) {
MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdGetRequest.Builder(meetingId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.instanceid("0")
.build();
MeetingsApi.ApiV1MeetingsMeetingIdGetResponse meetingResponse = null;
try {
meetingResponse = client.meetings().v1MeetingsMeetingIdGet(meetingRequest, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
return meetingResponse.getData();
} catch (ClientException | ServiceException e) {
log.error("ApiV1MeetingsMeetingIdGetRequest error: {}", e.getMessage());
}
return null;
}
/**
* 获取参会成员明细
*
* @param meetingId 会议id
* @param subMeetingId 子会议id
* @return
*/
public static V1MeetingsMeetingIdParticipantsGet200Response ApiV1MeetingsMeetingIdParticipantsGetRequest(String meetingId, String subMeetingId) {
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));
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse;
try {
participantsResponse = client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
return participantsResponse.getData();
} catch (ClientException | ServiceException e) {
log.error("ApiV1MeetingsMeetingIdParticipantsGetRequest error: {}", e.getMessage());
}
return null;
}
/**
* 获取转录文件内容
*
* @param recordFileId 转录文件id
* @return
*/
public static String ApiV1AddressesRecordFileIdGetRequest(String recordFileId) {
//查询录制转写详情
RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse;
try {
addressResponse = client.records().v1AddressesRecordFileIdGet(addressRequest,
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
} catch (ClientException | ServiceException e) {
throw new RuntimeException(e);
}
// 处理响应
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);
if (StringUtils.isNotEmpty(recordTextContent.replaceAll("\\n", "").trim())) {
return "\n\n" + recordTextContent;
}
}
}
if (count >= 3) {
break;
}
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
return "";
}
/**
* 根据url下载文件
*
* @param url
* @return
*/
private static 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);
}
}
}
......@@ -112,7 +112,7 @@ email:
smtp-host: ${EMAIL_SMTP_HOST}
push-switch: true #邮件推送总开关,高优先级
environment: test #test推给本公司人员,prod推给用户
test-receiver: duanxincheng@chatbot.cn #用于测试的收方邮箱
test-receiver: hongdongbao@chatbot.cn #用于测试的收方邮箱
llm:
api-addr: ${LLM_API_ADDR}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论