提交 a8de9001 作者: 洪东保

Merge branch 'old'

# Conflicts:
#	pom.xml
#	src/main/java/com/cmeeting/TencentMeetingCallbackApplication.java
#	src/main/java/com/cmeeting/ad/controller/UserController.java
#	src/main/java/com/cmeeting/ad/service/UserService.java
#	src/main/java/com/cmeeting/ad/service/impl/UserServiceImpl.java
#	src/main/java/com/cmeeting/constant/RecordTemplateConstant.java
#	src/main/java/com/cmeeting/controller/MeetingInfoController.java
#	src/main/java/com/cmeeting/email/EmailSender.java
#	src/main/java/com/cmeeting/job/CmeetingJob.java
#	src/main/java/com/cmeeting/job/EmailPushTask.java
#	src/main/java/com/cmeeting/job/FileProcessTask.java
#	src/main/java/com/cmeeting/service/FileProcessProducer.java
#	src/main/java/com/cmeeting/service/MeetingInfoService.java
#	src/main/java/com/cmeeting/service/impl/MeetingInfoServiceImpl.java
#	src/main/java/com/cmeeting/service/impl/TencentMeetingServiceImpl.java
#	src/main/resources/application.yml
#	src/main/resources/mapper/primary/MeetingInfoMapper.xml
......@@ -44,6 +44,13 @@
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.0</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-dependencies</artifactId>-->
<!-- <version>Finchley.M8</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
</dependencies>
</dependencyManagement>
<dependencies>
......@@ -352,6 +359,12 @@
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- yml参数加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies>
<build>
......
package com.cmeeting;
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
......@@ -9,6 +10,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableEncryptableProperties
@MapperScan("com.cmeeting.mapper.primary")
@EnableScheduling
public class TencentMeetingCallbackApplication {
......
......@@ -33,15 +33,6 @@ public class UserController {
return userService.login(vo.getAgentId(), vo.getData(), ipAddr);
}
/**
* 超管登录
* @param vo
* @return
*/
@PostMapping(value = "/auth")
public R auth(@Validated @RequestBody UserVo.Auth vo) {
return R.ok(userService.auth(vo, RecordTemplateConstant.TEMPLATE_TYPE_SYSTEM));
}
/**
* 角色扮演进入系统接口
......
......@@ -21,7 +21,7 @@ public class SysDept implements Serializable {
/**
* 部门id
*/
private String id;
private String deptId;
/**
* 部门名称
*/
......
......@@ -13,7 +13,7 @@ import java.util.List;
public interface UserService {
R login(String agentId, String data, String ip);
String auth(UserVo.Auth vo, String role);
String auth(String userId, String nick, String role);
List<String> getRoleIdByUserId(String tenantId, String userId, String path);
......
......@@ -89,9 +89,7 @@ public class UserServiceImpl implements UserService {
}
@Override
public String auth(UserVo.Auth vo, String role) {
String userId = vo.getId();
String nick = vo.getNick();
public String auth(String userId, String nick, String role) {
HashMap<String, String> stringStringHashMap = new HashMap<>();
SysTenant sysTenant = iTenantService.getById(permissionTenantId);
stringStringHashMap.put("userId", userId);
......@@ -226,9 +224,7 @@ public class UserServiceImpl implements UserService {
}
}
UserVo.Auth authParams = UserVo.Auth.builder().id(userId).nick(robotSecurityUser.getNickName()).build();
String token = auth(authParams, RecordTemplateConstant.TEMPLATE_TYPE_CUSTOM);
return token;
return auth(userId, robotSecurityUser.getNickName(), RecordTemplateConstant.TEMPLATE_TYPE_CUSTOM);
}
@Override
......@@ -263,19 +259,19 @@ public class UserServiceImpl implements UserService {
private R loginByAD(ApplicationUserVO.Login login) {
// AD验证
String username = login.getUsername().trim();
if (!isDev) {
boolean auth = iLdapService.authenticate(username, login.getPassword().trim());
if (!auth) {
return R.error("账号/密码错误!");
}
}
if (auth) {
// if(true){
SysUserSync sysUserSync = sysUserSysMapper.selectOne(new LambdaQueryWrapper<SysUserSync>()
.eq(SysUserSync::getTenantId, permissionTenantId)
.eq(SysUserSync::getUserId, username));
if (StringUtils.isEmpty(adminWhiteUsers) || !Arrays.asList(adminWhiteUsers.split(",")).contains(username)) {
throw new RobotBaseException("尊敬的用户 [" + sysUserSync.getName() + "],您的账户" + username + "未被授权访问此系统,请联系系统管理员。");
if(StringUtils.isEmpty(adminWhiteUsers) || !Arrays.asList(adminWhiteUsers.split(",")).contains(username)){
throw new RobotBaseException("尊敬的用户 ["+sysUserSync.getName()+"],您的账户"+username+"未被授权访问此系统,请联系系统管理员。");
}
return R.ok(auth(sysUserSync.getUserId(), sysUserSync.getName(), RecordTemplateConstant.TEMPLATE_TYPE_SYSTEM));
} else {
return R.error("账号/密码错误!");
}
return R.ok(sysUserSync);
}
/**
......
......@@ -15,9 +15,9 @@ public class ThreadPoolConfig {
public ThreadPoolTaskExecutor fileProcessExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数 (CPU密集型任务建议核心数+1)
executor.setCorePoolSize(2); // 固定核心线程数,避免动态获取CPU核心数
executor.setCorePoolSize(4); // 固定核心线程数,避免动态获取CPU核心数
// 最大线程数
executor.setMaxPoolSize(2);
executor.setMaxPoolSize(4);
// 队列容量
executor.setQueueCapacity(1000);
// 线程名前缀
......@@ -39,4 +39,34 @@ public class ThreadPoolConfig {
executor.initialize();
return executor;
}
@Bean("emailProcessExecutor")
public ThreadPoolTaskExecutor emailProcessExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数 (CPU密集型任务建议核心数+1)
executor.setCorePoolSize(1); // 固定核心线程数,避免动态获取CPU核心数
// 最大线程数
executor.setMaxPoolSize(5);
// 队列容量
executor.setQueueCapacity(1000);
// 线程名前缀
executor.setThreadNamePrefix("email-process-");
// 明确设置所有必要属性
executor.setAllowCoreThreadTimeOut(false); // 核心线程不允许超时
executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅关闭
executor.setAwaitTerminationSeconds(60); // 等待任务完成的最大时间
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化前打印配置检查
log.info("Initializing ThreadPool: core={}, max={}",
executor.getCorePoolSize(),
executor.getMaxPoolSize());
executor.initialize();
return executor;
}
}
\ No newline at end of file
package com.cmeeting.constant;
/**
* 会议处理状态枚举类
*/
public enum MeetingState {
/**
* 会议状态枚举类型
*/
// 新会议待处理
NEW(0, "新会议待处理"),
// 处理中
GENERATE_ERROR(1, "生成纪要失败"),
// 生成纪要成功
NOTE_GENERATED(2, "生成纪要成功"),
// 推送纪要成功
PUSH_SUCCESS(3, "推送纪要邮件成功"),
PUSH_ERROR(4, "推送纪要邮件失败"),
EMPTY(5, "转录文件为空")
;
// 状态码
private final int code;
// 状态描述
private final String description;
/**
* 枚举构造方法
* @param code 状态码
* @param description 状态描述
*/
private MeetingState(int code, String description) {
this.code = code;
this.description = description;
}
// 获取状态码
public int getCode() {
return code;
}
// 获取状态描述
public String getDescription() {
return description;
}
/**
* 根据状态码获取状态枚举值
* @param code 状态码
* @return 对应的枚举值,若不存在则返回null
*/
public static MeetingState fromCode(int code) {
for (MeetingState state : values()) {
if (state.code == code) {
return state;
}
}
return null;
}
/**
* 扩展方法示例:获取状态类型描述
* @return 返回状态类型描述
*/
public String getTypeDescription() {
return "会议处理状态";
}
}
\ No newline at end of file
......@@ -2,7 +2,6 @@ package com.cmeeting.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cmeeting.ad.entity.RobotSecurityUser;
import com.cmeeting.ad.util.SecurityUtil;
......@@ -14,34 +13,28 @@ import com.cmeeting.mapper.primary.UserIdMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.util.AESUtils;
import com.cmeeting.util.MinioUtils;
import com.cmeeting.util.R;
import com.cmeeting.vo.EmailPush;
import com.cmeeting.vo.EmailStatisticsVo;
import com.cmeeting.vo.MeetingInfoVO;
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter;
......@@ -67,6 +60,8 @@ public class MeetingInfoController {
private String permissionApplicationId;
@Value(value = "${userAdmin.file-download-path}")
private String fileDownloadPath;
@Value("${aec.key}")
public String aesKey;
@OperLog(location = "历史会议纪要详情页面", operation = "修改会议纪要")
@PostMapping("/updateRecordXml")
......@@ -106,7 +101,10 @@ public class MeetingInfoController {
try {
if (StringUtils.isNotEmpty(recordXml)) {
//xml转json,用于前端的表单回显
String xml = minioUtils.getFileText(recordXml);
InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordXml());
// 解密
String xml = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
String json = convertXmlToJSON(xml);
vo.setRecordJson(json);
}
......@@ -114,8 +112,11 @@ public class MeetingInfoController {
e.printStackTrace();
}
try {
if (StringUtils.isNotEmpty(recordContent)) {
vo.setRecordContent(minioUtils.getFileText(recordContent.replaceAll(fileDownloadPath, "")));
if(StringUtils.isNotEmpty(recordContent)){
InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordContent());
// 解密
String content = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
vo.setRecordContent(content);
}
} catch (Exception e) {
e.printStackTrace();
......@@ -131,7 +132,9 @@ public class MeetingInfoController {
public void exportMeetingRecord(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId());
String content = minioUtils.getFileText(meetingInfo.getRecordContent());
InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordContent());
// 解密
String content = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
String fileName = String.format(meetingInfo.getSubject() + "_转写原文_%s.docx", DateUtil.today());
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
......@@ -167,7 +170,10 @@ public class MeetingInfoController {
public void downloadMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId());
String xml = minioUtils.getFileText(meetingInfo.getRecordXml());
InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordXml());
// 解密
String xml = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
Map<String, Object> dataModel = convertXmlToMap(xml);
//追加参会人员信息
Map<String, Object> participantsMap = new ConcurrentHashMap<>();
......@@ -212,7 +218,9 @@ public class MeetingInfoController {
public R exportMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId());
String xml = minioUtils.getFileText(meetingInfo.getRecordXml());
InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordXml());
// 解密
String xml = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
Map<String, Object> dataModel = convertXmlToMap(xml);
//追加参会人员信息
Map<String, Object> participantsMap = new ConcurrentHashMap<>();
......@@ -315,4 +323,11 @@ public class MeetingInfoController {
}
return json;
}
@PostMapping("/statistics")
public void statisticsEmail(@RequestBody EmailStatisticsVo vo, HttpServletResponse response) {
meetingInfoService.statisticsEmail(vo.getType(), vo.getStartTime(), vo.getEndTime(), response);
}
}
......@@ -7,6 +7,7 @@ import com.azure.identity.ClientSecretCredentialBuilder;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmeeting.ad.service.UserService;
import com.cmeeting.ad.vo.UserVo;
import com.cmeeting.exception.RobotBaseException;
import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.pojo.MeetEmailTemplate;
import com.cmeeting.service.MeetEmailTemplateService;
......@@ -49,8 +50,8 @@ import java.util.concurrent.atomic.AtomicInteger;
public class EmailSender {
@Value("${email.sender}")
private String SENDER;
@Value("${email.sender-pwd}")
private String EMAIL_PWD;
// @Value("${email.sender-pwd}")
// private String EMAIL_PWD;
@Value("${email.push-switch}")
private Boolean pushSwitch;
@Value("${email.environment}")
......@@ -70,112 +71,6 @@ public class EmailSender {
@Resource
private RedisUtils redisUtils;
/**
* @param toEmail 收件人
* @param subject 邮件主题
* @param attachmentPath 附件路径
* @param meetingId 会议id
* @return
*/
// public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String meetingId) {
// log.info("sendEmailWithAttachment start...");
// // 邮件服务器配置
// Properties props = new Properties();
// props.put("mail.smtp.host", SMTP_HOST);
// props.put("mail.smtp.auth", "true");
//
// props.put("mail.smtp.port", "587");
// props.put("mail.smtp.starttls.enable", "true"); // 使用TLS
// props.put("mail.smtp.starttls.required", "true");
// props.put("mail.debug", "true");
// props.put("mail.smtp.connectiontimeout", "5000"); // 连接超时时间(毫秒)
// props.put("mail.smtp.timeout", "5000"); // 读取超时时间(毫秒)
// props.remove("mail.smtp.user");
// props.remove("mail.user");
// props.remove("mail.smtp.ssl.enable");
//
// // 创建会话
// log.info("sender->{},email_pwd->{}",SENDER, EMAIL_PWD);
//
// System.getProperties().stringPropertyNames().stream()
// .filter(name -> name.startsWith("mail."))
// .forEach(name -> log.info(name + " = " + System.getProperty(name)));
//
// Session session = Session.getInstance(props); // 不传递 Authenticator
// String body = "您好:\n" +
// "\n" +
// " 附件为您本次会议的会议纪要,烦请下载查看";
//
// AtomicInteger retryCount = new AtomicInteger(0);
// boolean isSent = false;
// if(StringUtils.isEmpty(toEmail)){
// log.error("收件邮箱为空,推送失败");
// processLogService.log(meetingId,null,"收件邮箱为空,推送失败");
// return false;
// }
// log.info("准备开始邮件推送...");
// while (retryCount.intValue() < MAX_RETRY && !isSent){
// try {
// Transport transport = session.getTransport("smtp");
// // 创建邮件消息
// 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);
//
// // 发送邮件
// log.info("Transport.send...");
//// Transport.send(message);
// transport.connect("smtp.office365.com", 587, "cmeeting_assistant@cimc.com", "scyou@xih45g6@xih4");
// transport.sendMessage(message, message.getAllRecipients());
// transport.close();
// log.error("邮件已成功发送: meetingId->{}", meetingId);
// isSent = true;
// }catch (Exception e) {
// retryCount.getAndIncrement();
// // 异常处理
// StringWriter sw = new StringWriter();
// PrintWriter pw = new PrintWriter(sw);
// e.printStackTrace(pw);
// processLogService.log(meetingId,null,"【邮件推送异常】:"+sw.toString());
// if (retryCount.intValue() > MAX_RETRY) {
// log.error("邮件发送达到最大重试次数: meetingId->{}", meetingId);
// 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 isSent;
// }
/**
* 发送邮件,带附件
......@@ -183,10 +78,7 @@ public class EmailSender {
* @return
*/
public boolean sendEmailWithAttachment(EmailPush emailPushBuilder) {
log.info("sendEmailWithAttachment start...");
// 创建会话
log.info("sender->{},email_pwd->{}",SENDER, EMAIL_PWD);
log.info("sendEmailWithAttachment start, sender -> {}", SENDER);
if(!pushSwitch){
log.info("【邮箱推送】:应用未开启邮件推送功能");
......@@ -255,8 +147,7 @@ public class EmailSender {
}else{
isSent = false;
processLogService.log(meetingId,subMeetingId,"【邮件推送异常】:邮件模板未设置");
retryCount.getAndIncrement();
continue;
throw new RobotBaseException("邮件模板未设置");
}
message.body = body;
......@@ -291,7 +182,7 @@ public class EmailSender {
.buildRequest()
.post();
log.error("邮件已成功发送: meetingId->{}", meetingId);
log.info("邮件已成功发送: meetingId->{}, subMeetingId->{}", meetingId, subMeetingId);
isSent = true;
}catch (Exception e) {
retryCount.getAndIncrement();
......
package com.cmeeting.job;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.mapper.primary.UserIdMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.UserId;
import com.cmeeting.pojo.WeComUser;
import com.cmeeting.service.*;
import com.cmeeting.util.RedisUtils;
import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -42,6 +44,8 @@ public class CmeetingJob {
private UserIdMapper userIdMapper;
@Value("${isDev}")
private Boolean isDev;
@Resource
private RedisUtils redisUtils;
// @PostConstruct
public void weComUserInit() {
......@@ -118,7 +122,10 @@ public class CmeetingJob {
if (isDev) {
return;
}
log.info("-------生成纪要定时任务开始-------");
if (redisUtils.setnx("Scheduled-All", "Scheduled-All", 18 * 60)){
return;
}
try {
//查出企微id和腾会id的关联关系
List<UserId> userIdRelations = userIdMapper.selectList(null);
Map<String, String> widTidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getWid, UserId::getTid));
......@@ -133,7 +140,7 @@ public class CmeetingJob {
log.info("无生成纪要权限的人员");
return;
} else {
log.info("生成纪要权限人员:->{}", accessUserIds.stream().map(UserDTO::getWid).collect(Collectors.joining(",")));
log.info("生成纪要权限人员:->{}", accessUserIds.toString());
}
List<TencentMeetingVO.RecordFile> meetingFiles = tencentMeetingService.getMeetingFiles(accessUserIds, weComUserMap);
......@@ -147,28 +154,38 @@ public class CmeetingJob {
// 提交处理任务
producer.submitBatchTasks(meetingFiles, authorizedUsers, tidWidRelations, Boolean.FALSE);
} catch (Exception e){
e.printStackTrace();
} finally {
redisUtils.del("Scheduled-All");
}
}
/**
* 定时扫描早于一小时之前的,所有未重试过的会议,重新生成纪要
*/
@Scheduled(fixedRate = 30 * 60 * 1000, initialDelay = 10 * 60 * 1000)
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 10 * 60 * 1000)
public void meetingMinutesRetry() {
if (isDev) {
return;
}
if (redisUtils.setnx("Scheduled-retry", "Scheduled-retry", 28 * 60)){
return;
}
try {
log.info("-------生成纪要重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
//查出所有早于一小时前的,生成失败且未重试过的会议
// 不能用status筛选,因为可能线程执行一般服务终止,status状态没变
List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated, Boolean.FALSE)
.eq(MeetingInfo::getEmailGenerateAccess, true)
.eq(MeetingInfo::getGenerateRetry, Boolean.FALSE)
.le(MeetingInfo::getSyncTime, LocalDateTime.now().minusHours(1))
.eq(MeetingInfo::getEmailPushAccess,Boolean.TRUE)
.eq(MeetingInfo::getEmailGenerateAccess,Boolean.TRUE)
.eq(MeetingInfo::getIsGenerated,Boolean.FALSE)
.eq(MeetingInfo::getGenerateRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
......@@ -196,17 +213,22 @@ public class CmeetingJob {
log.info("-------生成纪要重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
} finally {
redisUtils.del("Scheduled-retry");
}
}
/**
* 定时扫描早于一小时之前的,所有邮件推送未重试过的会议,重新推送邮件
*/
@Scheduled(fixedRate = 30 * 60 * 1000, initialDelay = 15 * 60 * 1000)
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 15 * 60 * 1000)
public void emailPushRetry() {
if (isDev) {
return;
}
if (redisUtils.setnx("Scheduled-email-retry", "Scheduled-email-retry", 28 * 60)){
return;
}
try {
log.info("-------邮件推送重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
......@@ -214,11 +236,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::getEmailPushAccess,Boolean.TRUE)
.eq(MeetingInfo::getIsGenerated,Boolean.TRUE)
.eq(MeetingInfo::getIsPushed,Boolean.FALSE)
.eq(MeetingInfo::getPushRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
......@@ -241,6 +263,8 @@ public class CmeetingJob {
log.info("-------邮件推送重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
} finally {
redisUtils.del("Scheduled-email-retry");
}
}
}
......@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
......@@ -54,8 +55,9 @@ public class EmailPushTask {
// 实际处理逻辑
public void process() {
boolean getKey = redisUtils.setnx("meeting_" + meetingId + subMeetingId, meetingId, 19*60);
if (!getKey) {
String key = "meet_process" + meetingId + "_" + (subMeetingId == null ? "" : "subMeetingId");
if (!redisUtils.setnx(key, 1, 120)) {
log.warn("key already exists in redis!, key: {}", key);
return;
}
Boolean isSuccess = Boolean.FALSE;
......@@ -97,17 +99,16 @@ public class EmailPushTask {
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(dataModel);
template.writeAndClose(new FileOutputStream(dataNetworkMinutesPath));
byte[] recordXmlData = Files.readAllBytes(Paths.get(dataNetworkMinutesPath));
minioUtils.upload(meetingInfo.getRecordXml(),recordXmlData);
} catch (IOException e) {
e.printStackTrace();
}
//邮件推送
List<EmailPush.Attachment> attachments = new ArrayList<>();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd");
try(InputStream is = new FileInputStream(dataNetworkMinutesPath)){
byte[] meetingMinutesBytes = IOUtils.toByteArray(is);
EmailPush.Attachment attachment = EmailPush.Attachment.builder().name(meetingInfo.getSubject() + "会议纪要_" + fmt.format(meetingInfo.getStartTime())).bytes(meetingMinutesBytes).build();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd");
EmailPush.Attachment attachment = EmailPush.Attachment.builder().name(meetingInfo.getSubject()+"会议纪要_" + fmt.format(meetingInfo.getStartTime())).bytes(meetingMinutesBytes).build();
attachments.add(attachment);
}catch (Exception e){
throw new RuntimeException(e);
......@@ -149,9 +150,10 @@ public class EmailPushTask {
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed,isSuccess)
.set(MeetingInfo::getStatus, isSuccess ? MeetingState.PUSH_SUCCESS.getCode() : MeetingState.PUSH_ERROR.getCode())
.set(MeetingInfo::getPushRetry,Boolean.TRUE)
);
redisUtils.del("meeting_" + meetingId + subMeetingId);
redisUtils.del(key);
}
......@@ -192,14 +194,15 @@ public class EmailPushTask {
public EmailPushTask(String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender,
MeetingRecordTemplateMapper meetingRecordTemplateMapper,Map<String,String> tidWidRelations) {
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender,
MeetingRecordTemplateMapper meetingRecordTemplateMapper, Map<String,String> tidWidRelations) {
this.savePath = savePath;
this.metadata = metadata;
this.meetingId = meetingId;
this.subMeetingId = subMeetingId;
this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils;
this.redisUtils = redisUtils;
this.emailSender = emailSender;
this.meetingRecordTemplateMapper = meetingRecordTemplateMapper;
this.tidWidRelations = tidWidRelations;
......
package com.cmeeting.job;
import cn.chatbot.meeting.DebugOutputTool;
import cn.chatbot.meeting.LLMConfig;
import cn.chatbot.meeting.LLMResult;
import cn.chatbot.meeting.MeetingProcess;
......@@ -19,10 +18,9 @@ import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmeeting.ad.util.SecurityUtil;
import com.cmeeting.constant.KnowledgePlatformRouteConstant;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.constant.RecordTemplateConstant;
import com.cmeeting.constant.UserAdminRouteConstant;
import com.cmeeting.dto.DocResultDto;
import com.cmeeting.dto.MeetTypeDto;
import com.cmeeting.dto.UserDTO;
......@@ -61,7 +59,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
......@@ -70,11 +67,8 @@ import java.io.*;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
......@@ -116,21 +110,35 @@ public class FileProcessTask {
private String applicationId;
private String fileDownloadPath;
private String permTenantId;
// AES加密秘钥
private String aesKey;
// 实际处理逻辑
public void process() {
// 根据meeting和subMeetingId上锁
boolean getKey = redisUtils.setnx("meeting_" + meetingId + subMeetingId, meetingId, 19 * 60);
if (!getKey) {
boolean isSuccess = false;
String key = "meet_process" + meetingId + "_" + (subMeetingId == null ? "" : "subMeetingId");
if (!redisUtils.setnx(key, 1, 240)) {
log.warn("key already exists in redis!, key: {}", key);
return;
}
boolean isSuccess = false;
log.info("线程开始------------>");
long l = System.currentTimeMillis();
Integer status = null;
while (retryCount <= MAX_RETRY && !isSuccess) {
try {
//已保存的会议信息
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId));
if (meetingInfo.getIsGenerated()) {
log.warn("Generating is down, meetingId: {}, subMeetingId: {}", meetingInfo.getMeetingId(), meetingInfo.getSubMeetingId());
return;
}
if (!meetingInfo.getEmailPushAccess()) {
log.warn("会议主持人没有推送邮件权限, userId: {}", meetingInfo.getHostUid());
return;
}
String meetingDate = meetingInfo.getStartTime().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
//下面再查一遍会议信息的意义是,为了获取会议中的子会议Id,如果是周期会议,需要保存下来(重要)
// MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest =
......@@ -287,9 +295,16 @@ public class FileProcessTask {
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 {
meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getStatus, status != null ? status : MeetingState.GENERATE_ERROR.getCode())
);
}
} else {
// 指数退避
......@@ -302,7 +317,8 @@ public class FileProcessTask {
}
}
}
redisUtils.del("meeting_" + meetingId + subMeetingId);
redisUtils.del(key);
log.info("线程结束, 耗时: {} ms", System.currentTimeMillis() - l);
}
private byte[] downloadFile(String url) {
......@@ -462,28 +478,31 @@ public class FileProcessTask {
* @param toUserCode 人员工号
* @param meetingRecordTemplate 模板信息
*/
private String saveResult(String content, byte[] recordData, MeetingInfo meetingInfo, String toUserCode, MeetingRecordTemplate meetingRecordTemplate) {
private String saveResult(String content, String recordData, MeetingInfo meetingInfo, String toUserCode, MeetingRecordTemplate meetingRecordTemplate) {
String meetingName;
//转录文件临时存储路径
String recordContentPath;
//生成的xml临时存储路径
String recordXmlPath = meetingId + "-recordXmlPath-" + IdUtil.fastSimpleUUID() + ".xml";
String uuid = IdUtil.fastSimpleUUID();
String today = DateUtil.today();
// 生成的xml临时存储路径
String recordXmlPath = String.format("%s/%s", today, meetingId + "-recordXmlPath-" + uuid + ".xml");
// 腾讯会议转录文件存储路径
String recordContentPath = String.format("%s/%s", today, meetingId + "-recordContent-" + uuid + ".txt") ;
//填充后的会议纪要名称
String meetingMinutesFileName;
//填充后的会议纪要word文件临时路径
String meetingMinutesPath;
boolean success = false;
try {
// TODO 转录文件加密并且直接上传到minio,不走layout和文档库
String subject = meetingInfo.getSubject();
String fileName = String.format(subject + "_转写原文_%s.txt", DateUtil.today());
MultipartFile multipartFile = new CustomMultipartFile(
"file", // 表单中的字段名
fileName, // 原始文件名
"text/plain; charset=utf-8", // MIME类型
recordData // 字节内容
recordData.getBytes(StandardCharsets.UTF_8) // 字节内容
);
//将转录文件存到知识向量库--用于问答改文档
//将转录文件存到知识向量库
Map<String, String> otherParams = new HashMap<>();
otherParams.put("userId", toUserCode);
otherParams.put("tenantId", permTenantId);
......@@ -494,18 +513,23 @@ public class FileProcessTask {
List<DocResultDto> docResultDtoList = JSON.parseObject(JSONObject.toJSONString(result.getData()), new TypeReference<List<DocResultDto>>() {
});
DocResultDto docResultDto = docResultDtoList.get(0);
String previewPath = docResultDto.getPreviewPath();
recordContentPath = previewPath.replaceAll(fileDownloadPath, "");
// String previewPath = docResultDto.getPreviewPath();
// recordContentPath = previewPath.replaceAll(fileDownloadPath,"");
meetingInfo.setTransDocId(docResultDto.getId());
} else {
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败,上传转录文件到向量知识库失败");
throw new RuntimeException("填充会议纪要失败");
}
// 将转录文件保存到MinIO
String encryptedRecordData = AESUtils.encrypt(recordData, aesKey);
minioUtils.upload(recordContentPath, encryptedRecordData.getBytes(StandardCharsets.UTF_8));
//去除内容中除了xml内容以外其他的信息,格式化xml
String xml = extractXmlFromMarkdown(content);
// minioUtils.upload(recordContentPath,recordData);
minioUtils.upload(recordXmlPath, xml.getBytes(StandardCharsets.UTF_8));
String encryptedXml = AESUtils.encrypt(xml, aesKey);
minioUtils.upload(recordXmlPath,encryptedXml.getBytes(StandardCharsets.UTF_8));
//将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml);
//判断会议名称关键词,如果是用户自己定义的主题,不做修改
......@@ -541,28 +565,31 @@ public class FileProcessTask {
}
meetingMinutesPath = savePath + meetingMinutesFileName + ".docx";
template.writeAndClose(new FileOutputStream(meetingMinutesPath));
processLogService.log(meetingId, subMeetingId, "填充会议纪要成功");
processLogService.log(meetingId,subMeetingId,"填充会议纪要成功");
success = true;
} catch (Exception e) {
success = false;
log.error("填充会议纪要失败: {}", e.getMessage(), e);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败" + sw.toString());
throw new RuntimeException("填充会议纪要失败");
}
} finally {
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())
.set(MeetingInfo::getStatus, success ? MeetingState.NOTE_GENERATED.getCode() : MeetingState.GENERATE_ERROR.getCode())
);
}
meetingInfo.setRecordContent(recordContentPath);
meetingInfo.setRecordXml(recordXmlPath);
return savePath + meetingMinutesFileName + ".docx";
......@@ -588,8 +615,9 @@ public class FileProcessTask {
meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed, isPushed)
.set(MeetingInfo::getStatus, isPushed ? MeetingState.PUSH_SUCCESS.getCode() : MeetingState.PUSH_ERROR.getCode())
);
}
......@@ -708,7 +736,8 @@ public class FileProcessTask {
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender,
MeetingRecordTemplateMapper meetingRecordTemplateMapper, MeetingRecordTemplateService meetingRecordTemplateService, MeetTypeService meetTypeService, UserIdMapper userIdMapper,
String llmApiAddr, Boolean finalRetry, ProcessLogService processLogService, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations,
UserAdminConfig userAdminConfig, String adminToken, String applicationId, String fileDownloadPath, String permTenantId) {
UserAdminConfig userAdminConfig, String adminToken, String applicationId, String fileDownloadPath, String permTenantId,
String aesKey) {
this.recordFileIdList = recordFileIdList;
this.savePath = savePath;
this.metadata = metadata;
......@@ -732,5 +761,10 @@ public class FileProcessTask {
this.applicationId = applicationId;
this.fileDownloadPath = fileDownloadPath;
this.permTenantId = permTenantId;
this.aesKey = aesKey;
}
public String getId(){
return this.meetingId + (this.subMeetingId == null ? "" : this.subMeetingId);
}
}
\ No newline at end of file
......@@ -99,6 +99,10 @@ public class MeetingInfo implements Serializable {
*/
private Boolean isPushed;
/**
* 会议状态: 0新会议待处理 1生成纪要失败 2生成纪要成功 3推送邮件成功 4推送邮件失败 5转录文件为空
*/
private Integer status;
/**
* 会议纪要重新生成标识
*/
private Boolean generateRetry;
......
......@@ -32,9 +32,12 @@ import java.util.concurrent.Future;
@Slf4j
public class FileProcessProducer {
@Autowired
@Resource(name = "fileProcessExecutor")
private ThreadPoolTaskExecutor fileProcessExecutor;
@Resource(name = "emailProcessExecutor")
private ThreadPoolTaskExecutor emailProcessExecutor;
@Value("${aec.key}")
public String aesKey;
@Autowired
private FileProcessCallbackHandler callbackHandler;
@Value(value = "${llm.api-addr}")
......@@ -81,6 +84,7 @@ public class FileProcessProducer {
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();
log.info("待处理会议数量: {}", recordFiles.size());
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务
FileProcessTask task = new FileProcessTask(
......@@ -106,7 +110,8 @@ public class FileProcessProducer {
adminToken,
applicationId,
fileDownloadPath,
permTenantId
permTenantId,
aesKey
);
// 提交任务到线程池
......@@ -135,13 +140,14 @@ public class FileProcessProducer {
Collections.emptyMap(),
meetingInfoMapper,
minioUtils,
redisUtils,
emailSender,
meetingRecordTemplateMapper,
tidWidRelations
);
// 提交任务到线程池
Future<?> future = fileProcessExecutor.submit(() -> {
Future<?> future = emailProcessExecutor.submit(() -> {
task.process();
});
......
......@@ -18,6 +18,12 @@ public interface MeetingInfoService extends IService<MeetingInfo> {
boolean regenerateXml(MeetingInfoVO vo);
/**
* 统计邮件推送情况
*/
void statisticsEmail(Integer type, Date startTime, Date endTime, HttpServletResponse response);
/**
* 统计
*
* @param searchValue 查询值
......
......@@ -25,6 +25,7 @@ import com.cmeeting.ad.entity.RobotSecurityUser;
import com.cmeeting.ad.util.SecurityUtil;
import com.cmeeting.constant.RecordTemplateConstant;
import com.cmeeting.exception.RobotBaseException;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
......@@ -34,6 +35,9 @@ import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.pojo.UserId;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.service.MeetingRecordTemplateService;
import com.cmeeting.service.TencentMeetingService;
import com.cmeeting.service.WeComService;
import com.cmeeting.util.MinioUtils;
import com.cmeeting.util.page.PageUtil;
import com.cmeeting.vo.MeetingInfoVO;
......@@ -56,6 +60,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
......@@ -103,6 +108,17 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti
@Autowired
private SysUserSysMapper sysUserSysMapper;
@Autowired
private WeComService weComService;
@Autowired
private TencentMeetingService tencentMeetingService;
@Autowired
private MeetingRecordTemplateService meetingRecordTemplateService;
private String CLIENT_ID = "c06fe7cf-2a89-4099-9805-ce03031938f8";
private String CLIENT_SECRET = "wsu8Q~GxYxPLf2akioQZDRG8NR1EzCAHIAQRVc6u";
private String TENANT_ID = "18653b3e-03c7-499e-8baf-42ef06a814ef";
@Override
public IPage<MeetingInfo> getPage(MeetingInfoVO vo) {
......@@ -575,4 +591,128 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti
throw new RuntimeException("下载文件失败", e);
}
}
/**
* 统计邮件推送情况
*/
@Override
public void statisticsEmail(Integer type, Date startTime, Date endTime, HttpServletResponse response) {
DateTime now = DateUtil.date();
if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) {
if (1== type) {
startTime = DateUtil.beginOfDay(DateUtil.offset(now, DateField.DAY_OF_MONTH, -2));
endTime = DateUtil.offset(now, DateField.HOUR_OF_DAY, -2);
} else {
startTime = DateUtil.beginOfDay(now);
endTime = now;
}
}
List<MeetingInfo> meetingInfoList = meetingInfoMapper.selectList(new LambdaQueryWrapper<MeetingInfo>()
// .eq(MeetingInfo::getEmailPushAccess, true)
.between(MeetingInfo::getStartTime, startTime, endTime));
Map<String, List<MeetingInfo>> hostUidMeetingInfoMap = meetingInfoList.stream().collect(Collectors.groupingBy(MeetingInfo::getHostUid));
Map<String, String> hostMap = new HashMap<>();
meetingInfoList.forEach(meetingInfo -> {
if (!hostMap.containsKey(meetingInfo.getHostUid())) {
hostMap.put(meetingInfo.getHostUid(), meetingInfo.getHost());
}
});
// 总计
Map<String, Object> totalMap = new LinkedHashMap<>();
totalMap.put("host", "总计");
List<Map<String, Object>> mapList = new ArrayList<>();
for (Map.Entry<String, List<MeetingInfo>> entry : hostUidMeetingInfoMap.entrySet()) {
// 使用linkedHashMap,保证输出到excel中的顺序
Map<String, Object> map = new LinkedHashMap<>();
String host = hostMap.get(entry.getKey());
List<MeetingInfo> meetingList = entry.getValue();
Integer totalNum = meetingList.size();
// 没有转录文件会议,需要生成会议纪要的会议, 待处理, 处理中, 纪要生成成功, 推送成功, 推送失败
Integer emptyNum=0, emailPushAccessNum=0, newNum=0, generatErrorNum=0, noteGeneratedNum=0, pushSuccessNum=0, pushErrorNum=0;
for (MeetingInfo meetingInfo : meetingList) {
if (meetingInfo.getStatus() == MeetingState.NEW.getCode()) {
newNum++;
} else if (meetingInfo.getStatus() == MeetingState.GENERATE_ERROR.getCode()) {
generatErrorNum++;
} else if (meetingInfo.getStatus() == MeetingState.PUSH_SUCCESS.getCode()) {
pushSuccessNum++;
} else if (meetingInfo.getStatus() == MeetingState.PUSH_ERROR.getCode()) {
pushErrorNum++;
} else if (meetingInfo.getStatus() == MeetingState.EMPTY.getCode()) {
emptyNum++;
}
if (meetingInfo.getIsGenerated() || meetingInfo.getStatus() == MeetingState.NOTE_GENERATED.getCode()) {
noteGeneratedNum++;
}
// 有转录文件 且 email_push_access为true,
if (meetingInfo.getStatus() != MeetingState.EMPTY.getCode() && meetingInfo.getEmailPushAccess()) {
emailPushAccessNum++;
}
}
Integer avaliableNum = totalNum - emptyNum;
map.put("host", host);
map.put("totalNum", totalNum);
map.put("avaliableNum", avaliableNum);
map.put("emptyNum", emptyNum);
map.put("emailPushAccessNum", emailPushAccessNum);
map.put("newNum", newNum);
map.put("generatErrorNum", generatErrorNum);
map.put("noteGeneratedNum", noteGeneratedNum);
map.put("pushErrorNum", pushErrorNum);
map.put("pushSuccessNum", pushSuccessNum);
mapList.add(map);
// 计算总计
totalMap.put("totalNum", Integer.valueOf(totalMap.getOrDefault("totalNum", 0).toString()) + totalNum);
totalMap.put("avaliableNum", Integer.valueOf(totalMap.getOrDefault("avaliableNum", 0).toString()) + avaliableNum);
totalMap.put("emptyNum", Integer.valueOf(totalMap.getOrDefault("emptyNum", 0).toString()) + emptyNum);
totalMap.put("emailPushAccessNum", Integer.valueOf(totalMap.getOrDefault("emailPushAccessNum", 0).toString()) + emailPushAccessNum);
totalMap.put("newNum", Integer.valueOf(totalMap.getOrDefault("newNum", 0).toString()) + newNum);
totalMap.put("generatErrorNum", Integer.valueOf(totalMap.getOrDefault("generatErrorNum", 0).toString()) + generatErrorNum);
totalMap.put("noteGeneratedNum", Integer.valueOf(totalMap.getOrDefault("noteGeneratedNum", 0).toString()) + noteGeneratedNum);
totalMap.put("pushErrorNum", Integer.valueOf(totalMap.getOrDefault("pushErrorNum", 0).toString()) + pushErrorNum);
totalMap.put("pushSuccessNum", Integer.valueOf(totalMap.getOrDefault("pushSuccessNum", 0).toString()) + pushSuccessNum);
}
mapList.add(totalMap);
// 导出
OutputStream outputStream = null;
ExcelWriter writer = ExcelUtil.getWriter(true);
try {
String title = String.format("会议纪要推送统计表_%s-%s", DateUtil.format(startTime, "yyyyMMdd"), DateUtil.format(endTime, "yyyyMMdd"));
outputStream = response.getOutputStream();
String fileName = String.format("%s.xlsx", title);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf8"));
writer.addHeaderAlias("host", "主持人");
writer.addHeaderAlias("totalNum", "总会议");
writer.addHeaderAlias("avaliableNum", "有效会议");
writer.addHeaderAlias("emptyNum", "没有转录文件");
writer.addHeaderAlias("emailPushAccessNum", "需生成纪要的会议总数");
writer.addHeaderAlias("newNum", "待处理");
writer.addHeaderAlias("generatErrorNum", "处理中");
writer.addHeaderAlias("noteGeneratedNum", "纪要生成成功");
writer.addHeaderAlias("pushSuccessNum", "纪要推送成功");
writer.addHeaderAlias("pushErrorNum", "纪要推送失败");
// 合并单元格后的标题行,使用默认标题样式
writer.merge(writer.getHeaderAlias().size() - 1, title);
// 一次性写出内容,使用默认样式,强制输出标题
writer.write(mapList, true);
writer.flush(outputStream);
} catch (Exception e) {
e.printStackTrace();
} finally {
writer.close();
IoUtil.close(outputStream);
}
}
}
......@@ -126,7 +126,7 @@ public class RecordTemplatePermissionServiceImpl extends ServiceImpl<RecordTempl
? new HashMap<>() : simpleUserList.stream().collect(Collectors.toMap(SysUserSync::getUserId, SysUserSync::getName));
List<SysDept> simpleDeptList = sysUserSysMapper.getSimpleDeptList(user.getTenantId());
Map<String, String> simpleDeptMap = CollectionUtils.isEmpty(simpleDeptList)
? new HashMap<>() : simpleDeptList.stream().collect(Collectors.toMap(SysDept::getId, SysDept::getName));
? new HashMap<>() : simpleDeptList.stream().collect(Collectors.toMap(SysDept::getDeptId, SysDept::getName));
for (RecordTemplatePermission permission : permissions) {
String relName = "1".equals(permission.getRelType()) && simpleDeptMap.containsKey(permission.getRelId())
? simpleDeptMap.get(permission.getRelId())
......
......@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.constant.PermissionPruposeType;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.mapper.primary.AuthMapper;
......@@ -45,7 +46,10 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
......@@ -76,6 +80,11 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
@Value(value = "${tencent.search-scope}")
private Integer searchDays;
/**
* 查询会议不得早于这个时间
*/
@Value(value = "${tencent.search-end-time}")
private String searchEndTime;
@Value(value = "${tencent.appId}")
private String tencentAppId;
@Value(value = "${tencent.sdkId}")
......@@ -124,24 +133,39 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds, Map<String, WeComUser> weComUserMap) {
List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>();
List<MeetingInfo> meetingSaveList = new ArrayList<>();
int i = 0;
// 查询近searchDays天的会议录制列表
try {
ZonedDateTime now = ZonedDateTime.now();
long startTime = now.minusDays(searchDays).toEpochSecond();
long endTime = now.toEpochSecond();
try {
long end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(searchEndTime).getTime() / 1000;
//仅查询2025-07-30 23:59:59之后的会议
if (end > endTime) {
return new ArrayList<>();
}
if (end > startTime) {
startTime = end;
}
} catch (Exception e) {
log.error("参数searchEndTime格式错误!");
}
AtomicInteger currentPage = new AtomicInteger(1);
//获取总页数
CorpRecordsVO firstData = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, 1, 20);
Integer totalPage = firstData.getTotalPage();
log.info("总页数: {}", totalPage);
//目前已存储的会议id
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){
if (i >= 150) {
break;
}
CorpRecordsVO data = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, currentPage.getAndIncrement(), 20);
//设置总页数
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
......@@ -218,8 +242,9 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
if (host.isPresent()) {
hostId = host.get().getUserid();
hostName = new String(Base64.getDecoder().decode(host.get().getUserName()));
} else {
log.error("未找到主持人,默认没有生成纪要权限");
}else{
log.error("未找到主持人,默认没有生成纪要权限, meetId: {}, subId: {}", meetingId, subMeetingId);
// processLogService.log(meeting.getMeetingId(),subMeetingId,"未找到主持人,默认没有生成纪要权限");
continue;
}
}
......@@ -269,12 +294,14 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
.subMeetingId(subMeetingId).generateRetry(Boolean.FALSE).pushRetry(Boolean.FALSE)
.host(hostName)
.hostUid(hostId)
.status(MeetingState.NEW.getCode())
// .participantUsers(participants.stream()
// .map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、")))
.recordFileId(String.join(",", recordFileIdList))
.email(email)
.build();
meetingSaveList.add(meetingItem);
i++;
}
} catch (Exception e) {
if (e instanceof NoSuchElementException) {
......@@ -296,11 +323,14 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
List<String> recordFileIdList = entry.getValue().stream().flatMap(s -> Arrays.stream(s.getRecordFileId().split(","))).collect(Collectors.toList());
meetingInfo.setRecordFileId(String.join(",", recordFileIdList));
finalSaveList.add(meetingInfo);
if (meetingInfo.getEmailPushAccess()) {
meetingFiles.add(TencentMeetingVO.RecordFile.builder()
.meetingId(meetingInfo.getMeetingId())
.subMeetingId(meetingInfo.getSubMeetingId())
.recordFileIdList(recordFileIdList).build());
}
}
meetingInfoMapper.batchInsert(finalSaveList);
}
} catch (Exception e) {
......@@ -415,7 +445,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
// 3. 生成签名
String signature = SignatureUtil.generateSignature(tencentSecretId, tencentSecretKey, httpMethod, nonce, timestamp, uri, "");
log.info("fetchMeetingRecords params: {}, {}", page, pageSize);
// 4. 发送请求
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet("https://api.meeting.qq.com" + uri);
......@@ -597,4 +627,5 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
return null;
}
}
}
package com.cmeeting.util;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
public class JasyptUtilTest {
public static String password = "EbfYkitulv23I2p1mXI50JMXoaxZTKJ7";
public static String algorithm = "PBEWITHMD5ANDDES";
/**
* 加密方法
*
* @param value 需要加密的值
* @return
*/
public static String encyptPwd(String value) {
//1.创建加密工具实例
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
//2.加解密配置
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(password);
config.setAlgorithm(algorithm);
//3.为了减少配置文件的书写,以下都是jasyp 3.x版本,配置文件默认配种
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor.encrypt(value);
}
public static String decryptPwd(String value) {
//1.创建加密工具实例
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
//2.加解密配置
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(password);
config.setAlgorithm(algorithm);
//3.为了减少配置文件的书写,以下都是jasyp 3.x版本,配置文件默认配种
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor.decrypt(value);
}
public static void main(String[] args) {
String en = encyptPwd("scyou@xih45g6@xih4");
System.out.println(en);
}
}
......@@ -140,6 +140,14 @@ public class RedisUtils {
return false;
}
}
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;
}
}
public boolean setnx(String key, Object value, long time) {
try {
......
package com.cmeeting.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @Description
* @Author zhang kaishi
* @Date 2025/7/28 18:22
**/
@Data
public class EmailStatisticsVo {
/**
* 统计类型
*/
private Integer type;
@DateTimeFormat(pattern = "yyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private Date startTime;
@DateTimeFormat(pattern = "yyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private Date endTime;
}
......@@ -30,7 +30,7 @@ CORP_SECRET: 7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0
############################################################## email
EMAIL_SENDER: cmeeting_assistant@cimc.com
EMAIL_SENDER_PWD: scyou@xih45g6@xih4
#EMAIL_SENDER_PWD: ENC(bH585hgTtUY5KpMgFqFpNRyL4kI/G7czZUk/7+U+LZY=)
EMAIL_SMTP_HOST: smtp.office365.com
############################################################## permission
......@@ -105,10 +105,11 @@ tencent:
aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
base-save-path: E:/save/ #会议纪要临时文件存储路径
search-scope: 2 #腾会转录文件拉取时间范围,查询过去n天的记录
search-end-time: '2025-07-30 23:59:59' #此时间之前的数据不拉取
email:
sender: ${EMAIL_SENDER}
sender-pwd: ${EMAIL_SENDER_PWD}
# sender-pwd: ${EMAIL_SENDER_PWD}
smtp-host: ${EMAIL_SMTP_HOST}
push-switch: true #邮件推送总开关,高优先级
environment: test #test推给本公司人员,prod推给用户
......@@ -152,6 +153,15 @@ permission:
tenantId: ${PERMISSION_TENANT_ID}
admin-white_users: ${PERMISSION_ADMIN_WHITE_USERS}
jasypt:
encryptor:
password: EbfYkitulv23I2p1mXI50JMXoaxZTKJ7
algorithm: PBEWITHMD5ANDDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
property:
prefix: ENC(
suffix: )
# 默认模板
record-template-path-default: /2025-07-21/a12465sdfsa2gas.docx
......
......@@ -4,7 +4,7 @@
<insert id="batchInsert" parameterType="list">
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host, host_uid, participant_users, start_time,
end_time, is_generated, email_generate_access, email_push_access, is_pushed, sync_time, sub_meeting_id, record_content, record_xml, generate_retry,
push_retry, record_file_id,email)
push_retry, record_file_id, status, email)
VALUES
<foreach collection="meetingSaveList" item="meeting" separator=",">
(
......@@ -27,6 +27,7 @@
#{meeting.generateRetry},
#{meeting.pushRetry},
#{meeting.recordFileId},
#{meeting.status},
#{meeting.email}
)
</foreach>
......
......@@ -48,7 +48,7 @@
</select>
<select id="getSimpleDeptList" resultType="com.cmeeting.ad.entity.SysDept">
SELECT
susc.id,
susc.dept_id,
susc.`name`
FROM
sys_user_sync_category AS susc
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论