提交 b87c127c 作者: duanxincheng

纪要生成服务优化

父级 8a23a94c
......@@ -122,6 +122,11 @@
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.0</version> <!-- 与你的 Spring Boot 版本保持一致 -->
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
......@@ -313,6 +318,24 @@
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>3.0.10</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
......
......@@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@MapperScan("com.cmeeting.mapper.primary")
public class TencentMeetingCallbackApplication {
public static void main(String[] args) {
SpringApplication.run(TencentMeetingCallbackApplication.class, args);
......
package com.cmeeting.config;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
// 主数据源配置
@Configuration
@MapperScan(basePackages = "com.cmeeting.mapper.primary", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
@Primary
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml"));
return bean.getObject();
}
@Primary
@Bean(name = "masterSqlSessionTemplate")
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
\ No newline at end of file
package com.cmeeting.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(
basePackages = "com.cmeeting.mapper.primary", // 主数据源的 Mapper 包路径
sqlSessionFactoryRef = "primarySqlSessionFactory"
)
public class PrimaryDataSourceConfig {
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary") // 绑定主数据源配置
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 可额外配置 MyBatis 的 mapperLocations、typeAliases 等
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
\ No newline at end of file
package com.cmeeting.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(
basePackages = "com.cmeeting.mapper.secondary", // 次数据源的 Mapper 包路径
sqlSessionFactoryRef = "secondarySqlSessionFactory"
)
public class SecondaryDataSourceConfig {
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary") // 绑定次数据源配置
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 可额外配置 MyBatis 的 mapperLocations、typeAliases 等
// 指定次数据源的 XML 映射文件路径
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/secondary/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "secondaryTransactionManager")
public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
\ No newline at end of file
package com.cmeeting.config;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
// 从数据源配置
@Configuration
@MapperScan(basePackages = "com.cmeeting.mapper.secondary", sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml"));
return bean.getObject();
}
@Bean(name = "slaveSqlSessionTemplate")
public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
......@@ -25,7 +25,7 @@ public class TencentMeetingController {
}
@GetMapping("/getMeetingFiles")
public void getMeetingFiles(TencentMeetingVO.TencentMeetingRequest requestVO){
tecentMeetingService.getMeetingFiles(requestVO);
public void getMeetingFiles(){
tecentMeetingService.getMeetingFiles();
}
}
......@@ -2,6 +2,7 @@ package com.cmeeting.email;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.activation.DataHandler;
import javax.activation.DataSource;
......@@ -13,12 +14,13 @@ import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
public class EmailSender {
@Value("${email.sender}")
private String SENDER;
@Value("${email.sender.pwd}")
@Value("${email.sender-pwd}")
private String EMAIL_PWD;
@Value("${email.smtp.host}")
@Value("${email.smtp-host}")
private String SMTP_HOST;
private static final Integer MAX_RETRY = 3;
......@@ -30,10 +32,10 @@ public class EmailSender {
* @param recordFileId 转录文件ID
* @return
*/
public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String recordFileId) {
public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String recordFileId) throws Exception {
// 邮件服务器配置
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.office365.com");
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.auth", "true");
// props.put("mail.smtp.port", "465");
......@@ -50,7 +52,8 @@ public class EmailSender {
Session session = Session.getInstance(props,
new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("cmeeting_assistant@cimc.com", "scyou@xih45g6@xih4");
// return new PasswordAuthentication("cmeeting_assistant@cimc.com", "scyou@xih45g6@xih4");
return new PasswordAuthentication(SENDER, EMAIL_PWD);
}
});
String body = "您好:\n" +
......@@ -58,60 +61,60 @@ public class EmailSender {
" 附件为您本次会议的会议纪要,烦请下载查看";
AtomicInteger retryCount = new AtomicInteger(0);
try {
boolean isSent = false;
while (retryCount.intValue() < MAX_RETRY && !isSent){
// 创建邮件消息
Message message = new MimeMessage(session);
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);
}
// 设置完整消息内容
message.setContent(multipart);
try {
// 创建邮件消息
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER));
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);
}
// 发送邮件
Transport.send(message);
log.error("邮件已成功发送: recordFileId->{}", recordFileId);
isSent = true;
}
} catch (MessagingException e) {
//todo 邮件失败记录
// 异常处理
retryCount.getAndIncrement();
if (retryCount.intValue() > MAX_RETRY) {
log.error("邮件发送达到最大重试次数: recordFileId->{}", recordFileId);
throw new RuntimeException(e);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("邮件发送重试失败", ie);
// 设置完整消息内容
message.setContent(multipart);
// 发送邮件
Transport.send(message);
log.error("邮件已成功发送: recordFileId->{}", recordFileId);
isSent = true;
} catch (MessagingException e) {
//todo 邮件失败记录
// 异常处理
retryCount.getAndIncrement();
if (retryCount.intValue() > MAX_RETRY) {
log.error("邮件发送达到最大重试次数: recordFileId->{}", recordFileId);
throw new RuntimeException(e);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("邮件发送重试失败", ie);
}
return false;
}
}
return false;
}
return true;
}
}
\ No newline at end of file
......@@ -90,18 +90,7 @@ public class CmeetingJob {
logger.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp);
logger.info("----------------------------------");
// dojob(beforeDayTimestamp, nowTimestamp);
AtomicInteger currentPage = new AtomicInteger(1);
TencentMeetingVO.TencentMeetingRequest request = TencentMeetingVO.TencentMeetingRequest.builder()
.page(1)
.pageSize(10)
.startTime(beforeDayTimestamp)
.endTime(nowTimestamp)
.operatorId(tencentAdminUserId)
.operatorIdType(1)
.build();
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles(request);
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles();
if (meetingFiles == null || meetingFiles.isEmpty()) {
......
......@@ -5,8 +5,18 @@ 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 cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.util.MinioUtils;
import com.deepoove.poi.XWPFTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.tencentcloudapi.wemeet.Client;
......@@ -27,6 +37,7 @@ import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.stereotype.Service;
import java.io.*;
import java.math.BigInteger;
......@@ -46,6 +57,7 @@ import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@Slf4j
@Service
public class FileProcessTask {
private String recordFileId;
private String meetingId;
......@@ -62,136 +74,139 @@ public class FileProcessTask {
private String tencentSecretKey;
private String tencentAdminUserId;
private MeetingInfoMapper meetingInfoMapper;
private MinioUtils minioUtils;
private EmailSender emailSender;
// 实际处理逻辑
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();
boolean isSuccess = false;
while (retryCount <= MAX_RETRY && !isSuccess) {
try {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
if ("txt".equalsIgnoreCase(fileType)) {
log.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
// 获取参会成员明细
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);
// 1. 下载文件
byte[] fileData = downloadFile(downloadUrl);
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()))).distinct().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);
}
// 2. 将二进制文件转换为文本
String recordTextContent = new String(fileData);
//查询录制转写详情
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;
}
if(StringUtils.isEmpty(recordTextContent.replaceAll("\\n","").trim())){
log.info("获取的转录文本为空,跳过纪要生成,URL:{},fileRecordId:{}", fileType, downloadUrl,recordFileId);
continue;
}
// 3. 处理文件 (调用Claude API等)
String processedResult = processWithClaude(recordTextContent);
// 3. 处理文件 (调用Claude API等)
String processedResult = processWithClaude(recordTextContent);
// 4. 保存结果
saveResult(savePath, processedResult, participantsMap);
}
// 4. 保存结果
saveResult(savePath, processedResult, participantsMap, fileData);
}
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
log.warn("Empty response for record file: {}", recordFileId);
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
isSuccess = true;
}
} catch (Exception e) {
// 异常处理
retryCount++;
if (retryCount > MAX_RETRY) {
log.error("达到最大重试次数: {}", recordFileId);
throw new RuntimeException(e);
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试失败", ie);
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) {
......@@ -238,7 +253,7 @@ public class FileProcessTask {
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");
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 label=\\\"会议名称\\\">会议名称</meeting_name>\\n<meeting_purpose label=\\\"会议目的\\\">会议目的</meeting_purpose>\\n<meeting_key_points label=\\\"会议要点\\\">会议要点</meeting_key_points>\\n<meeting_content label=\\\"会议内容\\\">会议内容</meeting_content>\\n<meeting_communication label=\\\"会议沟通内容\\\">会议沟通内容</meeting_communication>\\n<meeting_follow_up label=\\\"会后跟踪事项\\\">\\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会议记录如下:");
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议记录");
messages.add(chatMessage);
......@@ -251,36 +266,88 @@ public class FileProcessTask {
return ret;
}
private void saveResult(String path, String content, Map<String,Object> participantsMap) {
private void saveResult(String path, String content, Map<String,Object> participantsMap, byte[] recordData) {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss"));
String targetPath = path + nowTime + ".md"; // 改为.md扩展名;
String targetPath = path + nowTime + ".json"; // 改为.json扩展名;
String targetFileName;
String meetingName;
// 保存处理结果
try {
// 将字符串写入Markdown文件
Files.write(Paths.get(targetPath), content.getBytes());
log.info("Markdown文件已保存到: {}", targetPath);
String xml = extractXmlFromMarkdown(content);
// convert(targetPath,targetWordPath);
String markdownContent = new String(Files.readAllBytes(Paths.get(targetPath)));
Map<String, Object> dataModel = convertXmlToMap(markdownContent);
// 将xml格式的内容转换为特殊json
String json = convertXmlToJSON(xml);
String recordContentPath = meetingId + "-recordContent-" + IdUtil.fastSimpleUUID() + "txt";
minioUtils.upload(recordContentPath,recordData);
String recordXmlPath = meetingId + "-recordXmlPath-" + IdUtil.fastSimpleUUID() + "xml";
try {
//写入json文件,这里的json文件用于用户可编辑
Files.write(Paths.get(targetPath), json.getBytes());
log.info("json文件已保存到: {}", targetPath);
//将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml);
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"));
byte[] recordXmlData = Files.readAllBytes(Paths.get(path + targetFileName + ".docx"));
minioUtils.upload(recordXmlPath,recordXmlData);
} catch (Exception e) {
log.error("填充会议纪要失败: {}", e.getMessage(), e);
throw new RuntimeException("填充会议纪要失败");
}
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>().eq(MeetingInfo::getMeetingId,meetingId));
//邮件推送
EmailSender emailSender = new EmailSender();
emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
Boolean isPushed;
try {
//邮件推送
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);
isPushed = Boolean.TRUE;
} catch (Exception e) {
isPushed = Boolean.FALSE;
e.printStackTrace();
log.error(e.getMessage());
}
meetingInfoMapper.update(meetingInfo,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.set(MeetingInfo::getRecordContent,recordContentPath)
.set(MeetingInfo::getRecordXml,recordXmlPath)
.set(MeetingInfo::getIsGenerated,Boolean.TRUE)
.set(MeetingInfo::getIsPushed,isPushed)
);
}
private String convertXmlToJSON(String xml) {
String json;
try {
XmlMapper xmlMapper = new XmlMapper();
JsonNode rootNode = xmlMapper.readTree(xml.getBytes());
// 正确获取节点和属性的方式
List<Map> list = new ArrayList<>();
Iterator<String> iterator = rootNode.fieldNames();
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);
}});
}
// 构建JSON对象
ObjectMapper jsonMapper = new ObjectMapper();
json = jsonMapper.writeValueAsString(list);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
return json;
}
public static String call_llm(String apiAddr, String model, String token, List<Message> messages, int maxTokens) {
......@@ -312,17 +379,23 @@ public class FileProcessTask {
service.shutdownExecutor();
return stringBuilder.toString();
}
public static Map<String, Object> convertXmlToMap(String markdownContent) throws Exception {
String xmlContent = extractXmlFromMarkdown(markdownContent);
public static Map<String, Object> convertXmlToMap(String xml) throws Exception {
XmlMapper xmlMapper = new XmlMapper();
ObjectMapper objectMapper = new ObjectMapper();
// 先将 XML 读取为 Map
Map<?, ?> xmlMap = xmlMapper.readValue(xmlContent, Map.class);
Map<?, ?> xmlMap = xmlMapper.readValue(xml, Map.class);
// 转换为更标准的 Map<String, Object>
return 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();
Object realValue = value.get("");
entry.setValue(realValue);
}
return map;
}
/**
......@@ -348,7 +421,8 @@ public class FileProcessTask {
public FileProcessTask(String recordFileId, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, String tencentAppId,
String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId) {
String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender) {
this.recordFileId = recordFileId;
this.savePath = savePath;
this.metadata = metadata;
......@@ -359,5 +433,8 @@ public class FileProcessTask {
this.tencentAdminUserId = tencentAdminUserId;
this.meetingId = meetingId;
this.subMeetingId = subMeetingId;
this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils;
this.emailSender = emailSender;
}
}
\ No newline at end of file
package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.MeetingInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
public interface MeetingInfoMapper extends BaseMapper<MeetingInfo> {
void batchInsert(@Param("meetingSaveList")List<MeetingInfo> meetingSaveList);
List<String> getAllMeetingIds();
}
\ No newline at end of file
package com.cmeeting.pojo;
import java.util.Objects;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 会议信息实体类
*/
public class MeetingInfo {
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("cmt_meeting_info")
public class MeetingInfo implements Serializable {
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 会议主题
*/
......@@ -15,6 +34,11 @@ public class MeetingInfo {
* 会议ID(字符串类型)
*/
private String meetingId;
/**
* 子会议ID
*/
private String subMeetingId;
/**
* 会议号码
......@@ -22,153 +46,49 @@ public class MeetingInfo {
private String meetingCode;
/**
* 用户ID
* 主持人
*/
private String userId;
private String hostId;
/**
* 用户昵称
* 参会人员名单
*/
private String nickName;
private String participantUserIds;
/**
* 会议开始时间(时间戳)
*/
private Long startTime;
private LocalDateTime startTime;
/**
* 会议结束时间(时间戳)
*/
private Long endTime;
private LocalDateTime endTime;
/**
* 参会人数
* 是否生成会议纪要完成
*/
private Integer participantsNum;
private Boolean isGenerated;
/**
* 会议总时长(秒)
* 推送邮件许可 为false不推送
*/
private Integer meetingDuration;
private Boolean emailPushAccess;
/**
* 用户参会时长(秒)
* 是否推送邮件完成
*/
private Integer userMeetingDuration;
// 构造方法
public MeetingInfo() {
}
// Getter 和 Setter 方法
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getMeetingId() {
return meetingId;
}
public void setMeetingId(String meetingId) {
this.meetingId = meetingId;
}
public String getMeetingCode() {
return meetingCode;
}
public void setMeetingCode(String meetingCode) {
this.meetingCode = meetingCode;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public Long getStartTime() {
return startTime;
}
public void setStartTime(Long startTime) {
this.startTime = startTime;
}
public Long getEndTime() {
return endTime;
}
public void setEndTime(Long endTime) {
this.endTime = endTime;
}
public Integer getParticipantsNum() {
return participantsNum;
}
public void setParticipantsNum(Integer participantsNum) {
this.participantsNum = participantsNum;
}
public Integer getMeetingDuration() {
return meetingDuration;
}
public void setMeetingDuration(Integer meetingDuration) {
this.meetingDuration = meetingDuration;
}
public Integer getUserMeetingDuration() {
return userMeetingDuration;
}
public void setUserMeetingDuration(Integer userMeetingDuration) {
this.userMeetingDuration = userMeetingDuration;
}
// toString 方法
@Override
public String toString() {
return "MeetingInfo{" +
"subject='" + subject + '\'' +
", meetingId='" + meetingId + '\'' +
", meetingCode='" + meetingCode + '\'' +
", userId='" + userId + '\'' +
", nickName='" + nickName + '\'' +
", startTime=" + startTime +
", endTime=" + endTime +
", participantsNum=" + participantsNum +
", meetingDuration=" + meetingDuration +
", userMeetingDuration=" + userMeetingDuration +
'}';
}
private Boolean isPushed;
/**
* 同步时间
*/
private LocalDateTime syncTime;
// equals 和 hashCode 方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MeetingInfo that = (MeetingInfo) o;
return Objects.equals(meetingId, that.meetingId);
}
/**
* 转录文本
*/
private String recordContent;
@Override
public int hashCode() {
return Objects.hash(meetingId);
}
/**
* 纪要xml
*/
private String recordXml;
}
\ No newline at end of file
package com.cmeeting.service;
import com.cmeeting.email.EmailSender;
import com.cmeeting.job.FileProcessTask;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.util.MinioUtils;
import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -8,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
......@@ -33,6 +37,12 @@ public class FileProcessProducer {
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Resource
private MeetingInfoMapper meetingInfoMapper;
@Resource
private MinioUtils minioUtils;
@Resource
private EmailSender emailSender;
// 批量提交任务
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, String baseSavePath) {
......@@ -50,7 +60,10 @@ public class FileProcessProducer {
tencentSdkId,
tencentSecretId,
tencentSecretKey,
tencentAdminUserId
tencentAdminUserId,
meetingInfoMapper,
minioUtils,
emailSender
);
// 提交任务到线程池
......
package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.MeetingInfo;
public interface MeetingInfoService extends IService<MeetingInfo> {
}
......@@ -11,6 +11,6 @@ public interface TecentMeetingService extends IService<TencentMeetingUser> {
void doUsers();
List<TencentMeetingVO.RecordFile> getMeetingFiles(TencentMeetingVO.TencentMeetingRequest requestVO);
List<TencentMeetingVO.RecordFile> getMeetingFiles();
}
package com.cmeeting.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.service.MeetingInfoService;
import org.springframework.stereotype.Service;
@Service
public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, MeetingInfo> implements MeetingInfoService {
}
package com.cmeeting.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.TecentMeetingMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.vo.TencentMeetingVO;
......@@ -11,6 +14,7 @@ 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.V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
......@@ -35,6 +39,7 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
......@@ -42,6 +47,7 @@ import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.fetchUsersInBatches;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNamesTecent;
......@@ -51,6 +57,8 @@ import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNamesTecent;
public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TecentMeetingService {
@Resource
private TecentMeetingMapper tecentMeetingMapper;
@Resource
private MeetingInfoMapper meetingInfoMapper;
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'};
......@@ -93,98 +101,114 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
}
@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)
public List<TencentMeetingVO.RecordFile> getMeetingFiles() {
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.查询近两天的会议录制列表
List<MeetingInfo> meetingSaveList = new ArrayList<>();
// 查询近两天的会议录制列表
try {
ZonedDateTime now = ZonedDateTime.now();
String startTime = String.valueOf(now.minusDays(2).toEpochSecond());
String endTime = String.valueOf(now.toEpochSecond());
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();
Long totalPage = 1L;
//录制状态:
//1:录制中
//2:转码中
//3:转码完成
if(meetings.stream().allMatch(item->item.getState() != 3)){
return null;
}
for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) {
if(meeting.getState() != 3) continue;
//目前已存储的会议id
List<String> meetingIds = meetingInfoMapper.getAllMeetingIds();
while (currentPage.intValue() <= totalPage.intValue()){
RecordsApi.ApiV1RecordsGetRequest request =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.startTime(startTime)
.endTime(endTime)
.pageSize("20")
.page(String.valueOf(currentPage.getAndIncrement()))
.mediaSetType("0")
.queryRecordType("0")
.build();
RecordsApi.ApiV1RecordsGetResponse response =
client.records().v1RecordsGet(request, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
V1RecordsGet200Response data = response.getData();
//设置总页数
totalPage = data.getTotalPage();
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
List<V1RecordsGet200ResponseRecordMeetingsInner> meetings = data.getRecordMeetings();
//查询会议详情
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);
//录制状态:
//1:录制中
//2:转码中
//3:转码完成
if(meetings.stream().allMatch(item->item.getState() != 3)){
return null;
}
for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) {
if(meeting.getState() != 3) continue;
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);
//查询会议详情
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();
}
//如果数据库中已有相同会议id的记录,跳过同步
if(!meetingIds.contains(meetingId)){
MeetingInfo meetingItem = MeetingInfo.builder().meetingId(meetingId).meetingCode(meetingInfo.getMeetingCode())
.subject(meetingInfo.getSubject())
.startTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getStartTime())), ZoneId.systemDefault()))
.endTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getEndTime())), ZoneId.systemDefault()))
// .hostId(meetingInfo.getHosts().stream().map(V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner::getUserid).collect(Collectors.joining(",")))
// .participantUserIds(meetingInfo.getParticipants().stream().map(V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner::getUserid).collect(Collectors.joining(",")))
.isGenerated(Boolean.FALSE).emailPushAccess(Boolean.TRUE).isPushed(Boolean.FALSE).syncTime(LocalDateTime.now())
.subMeetingId(subMeetingId)
.build();
meetingSaveList.add(meetingItem);
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 (ClientException e) {
throw new RuntimeException(e);
} catch (ServiceException e) {
throw new RuntimeException(e);
}
}
}
}
if(meetingSaveList.size() > 0){
meetingInfoMapper.batchInsert(meetingSaveList);
}
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
......
package com.cmeeting.util;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@Component
public class MinioUtils {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.bucketName}")
private String bucketName;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
private MinioClient minioClient;
public synchronized MinioClient getMinioClient() {
try {
if(minioClient == null){
minioClient = new MinioClient(endpoint, accessKey, secretKey);
}
return minioClient;
}catch (Exception e){
throw new RuntimeException(e);
}
}
public InputStream getFile(MinioClient minioClient, String fileName){
try{
InputStream inputStream = minioClient.getObject(bucketName, fileName);
return inputStream;
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void upload(String path,InputStream inputStream) throws Exception{
getMinioClient().putObject(bucketName, path, inputStream, "application/octet-stream" );
}
public void upload(String path,byte[] bytes) {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
// 自动管理流的生命周期,无需手动close
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
upload(path,inputStream);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Boolean isFileExist(MinioClient minioClient, String fileName){
boolean found = false;
try{
InputStream inputStream = minioClient.getObject(bucketName, fileName);
found= true;
return found;
}catch (Exception e){
found=false;
return false;
}
}
}
server.port=8080
# ????????
tencent.meeting.token=QQZNb7xWQB47MpZF4C2DFAkv8
tencent.meeting.aesKey=agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
# application.yml
# spring.datasource.url=jdbc:mysql://192.168.10.155:3306/cmeeting?useSSL=false&characterEncoding=utf8
# spring.datasource.username=root
# spring.datasource.password=qizhi123
# spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# ?????primary?
spring.datasource.primary.jdbc-url=jdbc:mysql://192.168.10.155:3306/robot-standard-enterprise-aigc?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.primary.username=root
spring.datasource.primary.password=qizhi123
spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver
# ?????secondary?
spring.datasource.secondary.jdbc-url=jdbc:mysql://192.168.10.155:3306/user-admin?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.secondary.username=root
spring.datasource.secondary.password=qizhi123
spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
# MyBatis ??
# mybatis.mapper-locations=classpath:mapper/primary/*.xml
mybatis.type-aliases-package=com.cmeeting.pojo\
# ??????
mybatis.configuration.map-underscore-to-camel-case: true
logging.level.com.zaxxer.hikari=INFO
# 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
server:
port: 8080
# ????????
# application.yml
# spring.datasource.url=jdbc:mysql://192.168.10.155:3306/cmeeting?useSSL=false&characterEncoding=utf8
# spring.datasource.username=root
# spring.datasource.password=qizhi123
# spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# ?????primary?
#spring.datasource.primary.jdbc-url=jdbc:mysql://192.168.10.154:3307/aigc-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
##spring.datasource.primary.username=root
##spring.datasource.primary.password=123456
##spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver
##
### ?????secondary?
##spring.datasource.secondary.jdbc-url=jdbc:mysql://192.168.10.154:3307/useradmin-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
##spring.datasource.secondary.username=root
##spring.datasource.secondary.password=123456
##spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
spring:
datasource:
# 主数据源
master:
jdbc-url: jdbc:mysql://192.168.10.154:3307/aigc-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
# 从数据源
slave:
jdbc-url: jdbc:mysql://192.168.10.154:3307/useradmin-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
# MyBatis ??
# mybatis.mapper-locations=classpath:mapper/primary/*.xml
mybatis.type-aliases-package: com.cmeeting.pojo\
# ??????
mybatis:
configuration:
map-underscore-to-camel-case: true
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
# 也建议开启
global-config:
db-config:
column-underline: true
#logging:
# level:
# com:
# zaxxer:
# hikari: INFO
############################################################## minIO
MINIO_ADDRESS: http://192.168.10.154:9000
MINIO_BUCKET: zhongji
MINIO_USERNAME: minio
MINIO_PASSWORD: minio123
#Minio服务所在地址
minio.endpoint: ${MINIO_ADDRESS}
#存储桶名称
minio.bucketName: ${MINIO_BUCKET}
#访问的key
minio.accessKey: ${MINIO_USERNAME}
#访问的秘钥
minio.secretKey: ${MINIO_PASSWORD}
############################################################## minIO
############################################################## 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
sdkId: 28790143843
secretId: 0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL
secretKey: gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy
admin.userId: woaJARCQAAftcvU6GGoOn66rdSZ4IrOA
meeting:
token: QQZNb7xWQB47MpZF4C2DFAkv8
aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
email:
sender: cmeeting_assistant@cimc.com
sender-pwd: scyou@xih45g6@xih4
smtp-host: smtp.office365.com
############################################################## tencent meeting
logging:
level:
com.cmeeting.mapper.primary: TRACE
com.cmeeting.mapper.secondary: TRACE
root: DEBUG
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmeeting.mapper.primary.MeetingInfoMapper">
<insert id="batchInsert" parameterType="list">
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host_id, participant_user_ids, start_time,
end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id, record_content, record_xml)
VALUES
<foreach collection="meetingSaveList" item="meeting" separator=",">
(
#{meeting.subject},
#{meeting.meetingId},
#{meeting.meetingCode},
#{meeting.hostId},
#{meeting.participantUserIds},
#{meeting.startTime},
#{meeting.endTime},
#{meeting.isGenerated},
#{meeting.emailPushAccess},
#{meeting.isPushed},
#{meeting.syncTime},
#{meeting.subMeetingId},
#{meeting.recordContent},
#{meeting.recordXml}
)
</foreach>
</insert>
<select id="getAllMeetingIds" resultType="java.lang.String">
select meeting_id from cmt_meeting_info
</select>
</mapper>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论