提交 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 @@ ...@@ -44,6 +44,13 @@
<artifactId>jackson-dataformat-xml</artifactId> <artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.0</version> <version>2.13.0</version>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-dependencies</artifactId>-->
<!-- <version>Finchley.M8</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
<dependencies> <dependencies>
...@@ -352,6 +359,12 @@ ...@@ -352,6 +359,12 @@
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- yml参数加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
......
package com.cmeeting; package com.cmeeting;
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
...@@ -9,6 +10,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; ...@@ -9,6 +10,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableEncryptableProperties
@MapperScan("com.cmeeting.mapper.primary") @MapperScan("com.cmeeting.mapper.primary")
@EnableScheduling @EnableScheduling
public class TencentMeetingCallbackApplication { public class TencentMeetingCallbackApplication {
......
...@@ -33,15 +33,6 @@ public class UserController { ...@@ -33,15 +33,6 @@ public class UserController {
return userService.login(vo.getAgentId(), vo.getData(), ipAddr); 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 { ...@@ -21,7 +21,7 @@ public class SysDept implements Serializable {
/** /**
* 部门id * 部门id
*/ */
private String id; private String deptId;
/** /**
* 部门名称 * 部门名称
*/ */
......
...@@ -13,7 +13,7 @@ import java.util.List; ...@@ -13,7 +13,7 @@ import java.util.List;
public interface UserService { public interface UserService {
R login(String agentId, String data, String ip); 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); List<String> getRoleIdByUserId(String tenantId, String userId, String path);
......
...@@ -89,9 +89,7 @@ public class UserServiceImpl implements UserService { ...@@ -89,9 +89,7 @@ public class UserServiceImpl implements UserService {
} }
@Override @Override
public String auth(UserVo.Auth vo, String role) { public String auth(String userId, String nick, String role) {
String userId = vo.getId();
String nick = vo.getNick();
HashMap<String, String> stringStringHashMap = new HashMap<>(); HashMap<String, String> stringStringHashMap = new HashMap<>();
SysTenant sysTenant = iTenantService.getById(permissionTenantId); SysTenant sysTenant = iTenantService.getById(permissionTenantId);
stringStringHashMap.put("userId", userId); stringStringHashMap.put("userId", userId);
...@@ -226,9 +224,7 @@ public class UserServiceImpl implements UserService { ...@@ -226,9 +224,7 @@ public class UserServiceImpl implements UserService {
} }
} }
UserVo.Auth authParams = UserVo.Auth.builder().id(userId).nick(robotSecurityUser.getNickName()).build(); return auth(userId, robotSecurityUser.getNickName(), RecordTemplateConstant.TEMPLATE_TYPE_CUSTOM);
String token = auth(authParams, RecordTemplateConstant.TEMPLATE_TYPE_CUSTOM);
return token;
} }
@Override @Override
...@@ -263,19 +259,19 @@ public class UserServiceImpl implements UserService { ...@@ -263,19 +259,19 @@ public class UserServiceImpl implements UserService {
private R loginByAD(ApplicationUserVO.Login login) { private R loginByAD(ApplicationUserVO.Login login) {
// AD验证 // AD验证
String username = login.getUsername().trim(); String username = login.getUsername().trim();
if (!isDev) {
boolean auth = iLdapService.authenticate(username, login.getPassword().trim()); boolean auth = iLdapService.authenticate(username, login.getPassword().trim());
if (!auth) { if (auth) {
return R.error("账号/密码错误!"); // if(true){
}
}
SysUserSync sysUserSync = sysUserSysMapper.selectOne(new LambdaQueryWrapper<SysUserSync>() SysUserSync sysUserSync = sysUserSysMapper.selectOne(new LambdaQueryWrapper<SysUserSync>()
.eq(SysUserSync::getTenantId, permissionTenantId) .eq(SysUserSync::getTenantId, permissionTenantId)
.eq(SysUserSync::getUserId, username)); .eq(SysUserSync::getUserId, username));
if (StringUtils.isEmpty(adminWhiteUsers) || !Arrays.asList(adminWhiteUsers.split(",")).contains(username)) { if(StringUtils.isEmpty(adminWhiteUsers) || !Arrays.asList(adminWhiteUsers.split(",")).contains(username)){
throw new RobotBaseException("尊敬的用户 [" + sysUserSync.getName() + "],您的账户" + 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 { ...@@ -15,9 +15,9 @@ public class ThreadPoolConfig {
public ThreadPoolTaskExecutor fileProcessExecutor() { public ThreadPoolTaskExecutor fileProcessExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数 (CPU密集型任务建议核心数+1) // 核心线程数 (CPU密集型任务建议核心数+1)
executor.setCorePoolSize(2); // 固定核心线程数,避免动态获取CPU核心数 executor.setCorePoolSize(4); // 固定核心线程数,避免动态获取CPU核心数
// 最大线程数 // 最大线程数
executor.setMaxPoolSize(2); executor.setMaxPoolSize(4);
// 队列容量 // 队列容量
executor.setQueueCapacity(1000); executor.setQueueCapacity(1000);
// 线程名前缀 // 线程名前缀
...@@ -39,4 +39,34 @@ public class ThreadPoolConfig { ...@@ -39,4 +39,34 @@ public class ThreadPoolConfig {
executor.initialize(); executor.initialize();
return executor; 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; ...@@ -2,7 +2,6 @@ package com.cmeeting.controller;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cmeeting.ad.entity.RobotSecurityUser; import com.cmeeting.ad.entity.RobotSecurityUser;
import com.cmeeting.ad.util.SecurityUtil; import com.cmeeting.ad.util.SecurityUtil;
...@@ -14,34 +13,28 @@ import com.cmeeting.mapper.primary.UserIdMapper; ...@@ -14,34 +13,28 @@ import com.cmeeting.mapper.primary.UserIdMapper;
import com.cmeeting.pojo.MeetingInfo; import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate; import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.service.MeetingInfoService; import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.util.AESUtils;
import com.cmeeting.util.MinioUtils; import com.cmeeting.util.MinioUtils;
import com.cmeeting.util.R; import com.cmeeting.util.R;
import com.cmeeting.vo.EmailPush; import com.cmeeting.vo.EmailPush;
import com.cmeeting.vo.EmailStatisticsVo;
import com.cmeeting.vo.MeetingInfoVO; import com.cmeeting.vo.MeetingInfoVO;
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.XWPFTemplate;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource; import org.springframework.web.bind.annotation.*;
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 javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream; import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
...@@ -67,6 +60,8 @@ public class MeetingInfoController { ...@@ -67,6 +60,8 @@ public class MeetingInfoController {
private String permissionApplicationId; private String permissionApplicationId;
@Value(value = "${userAdmin.file-download-path}") @Value(value = "${userAdmin.file-download-path}")
private String fileDownloadPath; private String fileDownloadPath;
@Value("${aec.key}")
public String aesKey;
@OperLog(location = "历史会议纪要详情页面", operation = "修改会议纪要") @OperLog(location = "历史会议纪要详情页面", operation = "修改会议纪要")
@PostMapping("/updateRecordXml") @PostMapping("/updateRecordXml")
...@@ -106,7 +101,10 @@ public class MeetingInfoController { ...@@ -106,7 +101,10 @@ public class MeetingInfoController {
try { try {
if (StringUtils.isNotEmpty(recordXml)) { if (StringUtils.isNotEmpty(recordXml)) {
//xml转json,用于前端的表单回显 //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); String json = convertXmlToJSON(xml);
vo.setRecordJson(json); vo.setRecordJson(json);
} }
...@@ -114,8 +112,11 @@ public class MeetingInfoController { ...@@ -114,8 +112,11 @@ public class MeetingInfoController {
e.printStackTrace(); e.printStackTrace();
} }
try { try {
if (StringUtils.isNotEmpty(recordContent)) { if(StringUtils.isNotEmpty(recordContent)){
vo.setRecordContent(minioUtils.getFileText(recordContent.replaceAll(fileDownloadPath, ""))); InputStream inputStream = minioUtils.getFile(meetingInfo.getRecordContent());
// 解密
String content = AESUtils.decrypt(IoUtil.read(inputStream, StandardCharsets.UTF_8), aesKey);
vo.setRecordContent(content);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
...@@ -131,7 +132,9 @@ public class MeetingInfoController { ...@@ -131,7 +132,9 @@ public class MeetingInfoController {
public void exportMeetingRecord(@RequestBody MeetingInfoVO vo, HttpServletResponse response) { public void exportMeetingRecord(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try { try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId()); 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()); String fileName = String.format(meetingInfo.getSubject() + "_转写原文_%s.docx", DateUtil.today());
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
...@@ -167,7 +170,10 @@ public class MeetingInfoController { ...@@ -167,7 +170,10 @@ public class MeetingInfoController {
public void downloadMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) { public void downloadMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try { try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId()); 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> dataModel = convertXmlToMap(xml);
//追加参会人员信息 //追加参会人员信息
Map<String, Object> participantsMap = new ConcurrentHashMap<>(); Map<String, Object> participantsMap = new ConcurrentHashMap<>();
...@@ -212,7 +218,9 @@ public class MeetingInfoController { ...@@ -212,7 +218,9 @@ public class MeetingInfoController {
public R exportMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) { public R exportMeetingMinutes(@RequestBody MeetingInfoVO vo, HttpServletResponse response) {
try { try {
MeetingInfo meetingInfo = meetingInfoService.getById(vo.getId()); 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> dataModel = convertXmlToMap(xml);
//追加参会人员信息 //追加参会人员信息
Map<String, Object> participantsMap = new ConcurrentHashMap<>(); Map<String, Object> participantsMap = new ConcurrentHashMap<>();
...@@ -315,4 +323,11 @@ public class MeetingInfoController { ...@@ -315,4 +323,11 @@ public class MeetingInfoController {
} }
return json; 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; ...@@ -7,6 +7,7 @@ import com.azure.identity.ClientSecretCredentialBuilder;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmeeting.ad.service.UserService; import com.cmeeting.ad.service.UserService;
import com.cmeeting.ad.vo.UserVo; import com.cmeeting.ad.vo.UserVo;
import com.cmeeting.exception.RobotBaseException;
import com.cmeeting.log.service.ProcessLogService; import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.pojo.MeetEmailTemplate; import com.cmeeting.pojo.MeetEmailTemplate;
import com.cmeeting.service.MeetEmailTemplateService; import com.cmeeting.service.MeetEmailTemplateService;
...@@ -49,8 +50,8 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -49,8 +50,8 @@ import java.util.concurrent.atomic.AtomicInteger;
public class EmailSender { public class EmailSender {
@Value("${email.sender}") @Value("${email.sender}")
private String SENDER; private String SENDER;
@Value("${email.sender-pwd}") // @Value("${email.sender-pwd}")
private String EMAIL_PWD; // private String EMAIL_PWD;
@Value("${email.push-switch}") @Value("${email.push-switch}")
private Boolean pushSwitch; private Boolean pushSwitch;
@Value("${email.environment}") @Value("${email.environment}")
...@@ -70,112 +71,6 @@ public class EmailSender { ...@@ -70,112 +71,6 @@ public class EmailSender {
@Resource @Resource
private RedisUtils redisUtils; 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 { ...@@ -183,10 +78,7 @@ public class EmailSender {
* @return * @return
*/ */
public boolean sendEmailWithAttachment(EmailPush emailPushBuilder) { public boolean sendEmailWithAttachment(EmailPush emailPushBuilder) {
log.info("sendEmailWithAttachment start..."); log.info("sendEmailWithAttachment start, sender -> {}", SENDER);
// 创建会话
log.info("sender->{},email_pwd->{}",SENDER, EMAIL_PWD);
if(!pushSwitch){ if(!pushSwitch){
log.info("【邮箱推送】:应用未开启邮件推送功能"); log.info("【邮箱推送】:应用未开启邮件推送功能");
...@@ -255,8 +147,7 @@ public class EmailSender { ...@@ -255,8 +147,7 @@ public class EmailSender {
}else{ }else{
isSent = false; isSent = false;
processLogService.log(meetingId,subMeetingId,"【邮件推送异常】:邮件模板未设置"); processLogService.log(meetingId,subMeetingId,"【邮件推送异常】:邮件模板未设置");
retryCount.getAndIncrement(); throw new RobotBaseException("邮件模板未设置");
continue;
} }
message.body = body; message.body = body;
...@@ -291,7 +182,7 @@ public class EmailSender { ...@@ -291,7 +182,7 @@ public class EmailSender {
.buildRequest() .buildRequest()
.post(); .post();
log.error("邮件已成功发送: meetingId->{}", meetingId); log.info("邮件已成功发送: meetingId->{}, subMeetingId->{}", meetingId, subMeetingId);
isSent = true; isSent = true;
}catch (Exception e) { }catch (Exception e) {
retryCount.getAndIncrement(); retryCount.getAndIncrement();
......
package com.cmeeting.job; package com.cmeeting.job;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.dto.UserDTO; import com.cmeeting.dto.UserDTO;
import com.cmeeting.mapper.primary.UserIdMapper; import com.cmeeting.mapper.primary.UserIdMapper;
import com.cmeeting.pojo.MeetingInfo; import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.UserId; import com.cmeeting.pojo.UserId;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import com.cmeeting.service.*; import com.cmeeting.service.*;
import com.cmeeting.util.RedisUtils;
import com.cmeeting.vo.TencentMeetingVO; import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
...@@ -42,6 +44,8 @@ public class CmeetingJob { ...@@ -42,6 +44,8 @@ public class CmeetingJob {
private UserIdMapper userIdMapper; private UserIdMapper userIdMapper;
@Value("${isDev}") @Value("${isDev}")
private Boolean isDev; private Boolean isDev;
@Resource
private RedisUtils redisUtils;
// @PostConstruct // @PostConstruct
public void weComUserInit() { public void weComUserInit() {
...@@ -118,7 +122,10 @@ public class CmeetingJob { ...@@ -118,7 +122,10 @@ public class CmeetingJob {
if (isDev) { if (isDev) {
return; return;
} }
log.info("-------生成纪要定时任务开始-------"); if (redisUtils.setnx("Scheduled-All", "Scheduled-All", 18 * 60)){
return;
}
try {
//查出企微id和腾会id的关联关系 //查出企微id和腾会id的关联关系
List<UserId> userIdRelations = userIdMapper.selectList(null); List<UserId> userIdRelations = userIdMapper.selectList(null);
Map<String, String> widTidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getWid, UserId::getTid)); Map<String, String> widTidRelations = userIdRelations.stream().collect(Collectors.toMap(UserId::getWid, UserId::getTid));
...@@ -133,7 +140,7 @@ public class CmeetingJob { ...@@ -133,7 +140,7 @@ public class CmeetingJob {
log.info("无生成纪要权限的人员"); log.info("无生成纪要权限的人员");
return; return;
} else { } else {
log.info("生成纪要权限人员:->{}", accessUserIds.stream().map(UserDTO::getWid).collect(Collectors.joining(","))); log.info("生成纪要权限人员:->{}", accessUserIds.toString());
} }
List<TencentMeetingVO.RecordFile> meetingFiles = tencentMeetingService.getMeetingFiles(accessUserIds, weComUserMap); List<TencentMeetingVO.RecordFile> meetingFiles = tencentMeetingService.getMeetingFiles(accessUserIds, weComUserMap);
...@@ -147,28 +154,38 @@ public class CmeetingJob { ...@@ -147,28 +154,38 @@ public class CmeetingJob {
// 提交处理任务 // 提交处理任务
producer.submitBatchTasks(meetingFiles, authorizedUsers, tidWidRelations, Boolean.FALSE); 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() { public void meetingMinutesRetry() {
if (isDev) { if (isDev) {
return; return;
} }
if (redisUtils.setnx("Scheduled-retry", "Scheduled-retry", 28 * 60)){
return;
}
try { try {
log.info("-------生成纪要重试定时任务开始-------"); log.info("-------生成纪要重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
//查出所有早于一小时前的,生成失败且未重试过的会议 //查出所有早于一小时前的,生成失败且未重试过的会议
// 不能用status筛选,因为可能线程执行一般服务终止,status状态没变
List<MeetingInfo> meetingInfoList = List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>() meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated, Boolean.FALSE) .eq(MeetingInfo::getEmailPushAccess,Boolean.TRUE)
.eq(MeetingInfo::getEmailGenerateAccess, true) .eq(MeetingInfo::getEmailGenerateAccess,Boolean.TRUE)
.eq(MeetingInfo::getGenerateRetry, Boolean.FALSE) .eq(MeetingInfo::getIsGenerated,Boolean.FALSE)
.le(MeetingInfo::getSyncTime, LocalDateTime.now().minusHours(1)) .eq(MeetingInfo::getGenerateRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
); );
if (meetingInfoList == null || meetingInfoList.isEmpty()) { if (meetingInfoList == null || meetingInfoList.isEmpty()) {
...@@ -196,17 +213,22 @@ public class CmeetingJob { ...@@ -196,17 +213,22 @@ public class CmeetingJob {
log.info("-------生成纪要重试定时任务结束--------"); log.info("-------生成纪要重试定时任务结束--------");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); 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() { public void emailPushRetry() {
if (isDev) { if (isDev) {
return; return;
} }
if (redisUtils.setnx("Scheduled-email-retry", "Scheduled-email-retry", 28 * 60)){
return;
}
try { try {
log.info("-------邮件推送重试定时任务开始-------"); log.info("-------邮件推送重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
...@@ -214,11 +236,11 @@ public class CmeetingJob { ...@@ -214,11 +236,11 @@ public class CmeetingJob {
//查出所有早于一小时前的,邮件推送失败且未重试过的会议 //查出所有早于一小时前的,邮件推送失败且未重试过的会议
List<MeetingInfo> meetingInfoList = List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>() meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated, Boolean.TRUE) .eq(MeetingInfo::getEmailPushAccess,Boolean.TRUE)
.eq(MeetingInfo::getEmailPushAccess, Boolean.TRUE) .eq(MeetingInfo::getIsGenerated,Boolean.TRUE)
.eq(MeetingInfo::getIsPushed, Boolean.FALSE) .eq(MeetingInfo::getIsPushed,Boolean.FALSE)
.eq(MeetingInfo::getPushRetry, Boolean.FALSE) .eq(MeetingInfo::getPushRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime, LocalDateTime.now().minusHours(1)) .le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
); );
if (meetingInfoList == null || meetingInfoList.isEmpty()) { if (meetingInfoList == null || meetingInfoList.isEmpty()) {
...@@ -241,6 +263,8 @@ public class CmeetingJob { ...@@ -241,6 +263,8 @@ public class CmeetingJob {
log.info("-------邮件推送重试定时任务结束--------"); log.info("-------邮件推送重试定时任务结束--------");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally {
redisUtils.del("Scheduled-email-retry");
} }
} }
} }
...@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil; ...@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.email.EmailSender; import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper; import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper; import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
...@@ -54,8 +55,9 @@ public class EmailPushTask { ...@@ -54,8 +55,9 @@ public class EmailPushTask {
// 实际处理逻辑 // 实际处理逻辑
public void process() { public void process() {
boolean getKey = redisUtils.setnx("meeting_" + meetingId + subMeetingId, meetingId, 19*60); String key = "meet_process" + meetingId + "_" + (subMeetingId == null ? "" : "subMeetingId");
if (!getKey) { if (!redisUtils.setnx(key, 1, 120)) {
log.warn("key already exists in redis!, key: {}", key);
return; return;
} }
Boolean isSuccess = Boolean.FALSE; Boolean isSuccess = Boolean.FALSE;
...@@ -97,17 +99,16 @@ public class EmailPushTask { ...@@ -97,17 +99,16 @@ public class EmailPushTask {
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(dataModel); XWPFTemplate template = XWPFTemplate.compile(inputStream).render(dataModel);
template.writeAndClose(new FileOutputStream(dataNetworkMinutesPath)); template.writeAndClose(new FileOutputStream(dataNetworkMinutesPath));
byte[] recordXmlData = Files.readAllBytes(Paths.get(dataNetworkMinutesPath)); byte[] recordXmlData = Files.readAllBytes(Paths.get(dataNetworkMinutesPath));
minioUtils.upload(meetingInfo.getRecordXml(),recordXmlData);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
//邮件推送 //邮件推送
List<EmailPush.Attachment> attachments = new ArrayList<>(); List<EmailPush.Attachment> attachments = new ArrayList<>();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd");
try(InputStream is = new FileInputStream(dataNetworkMinutesPath)){ try(InputStream is = new FileInputStream(dataNetworkMinutesPath)){
byte[] meetingMinutesBytes = IOUtils.toByteArray(is); 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); attachments.add(attachment);
}catch (Exception e){ }catch (Exception e){
throw new RuntimeException(e); throw new RuntimeException(e);
...@@ -149,9 +150,10 @@ public class EmailPushTask { ...@@ -149,9 +150,10 @@ public class EmailPushTask {
.eq(MeetingInfo::getMeetingId,meetingId) .eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId) .eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed,isSuccess) .set(MeetingInfo::getIsPushed,isSuccess)
.set(MeetingInfo::getStatus, isSuccess ? MeetingState.PUSH_SUCCESS.getCode() : MeetingState.PUSH_ERROR.getCode())
.set(MeetingInfo::getPushRetry,Boolean.TRUE) .set(MeetingInfo::getPushRetry,Boolean.TRUE)
); );
redisUtils.del("meeting_" + meetingId + subMeetingId); redisUtils.del(key);
} }
...@@ -192,14 +194,15 @@ public class EmailPushTask { ...@@ -192,14 +194,15 @@ public class EmailPushTask {
public EmailPushTask(String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, public EmailPushTask(String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender, MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender,
MeetingRecordTemplateMapper meetingRecordTemplateMapper,Map<String,String> tidWidRelations) { MeetingRecordTemplateMapper meetingRecordTemplateMapper, Map<String,String> tidWidRelations) {
this.savePath = savePath; this.savePath = savePath;
this.metadata = metadata; this.metadata = metadata;
this.meetingId = meetingId; this.meetingId = meetingId;
this.subMeetingId = subMeetingId; this.subMeetingId = subMeetingId;
this.meetingInfoMapper = meetingInfoMapper; this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils; this.minioUtils = minioUtils;
this.redisUtils = redisUtils;
this.emailSender = emailSender; this.emailSender = emailSender;
this.meetingRecordTemplateMapper = meetingRecordTemplateMapper; this.meetingRecordTemplateMapper = meetingRecordTemplateMapper;
this.tidWidRelations = tidWidRelations; this.tidWidRelations = tidWidRelations;
......
package com.cmeeting.job; package com.cmeeting.job;
import cn.chatbot.meeting.DebugOutputTool;
import cn.chatbot.meeting.LLMConfig; import cn.chatbot.meeting.LLMConfig;
import cn.chatbot.meeting.LLMResult; import cn.chatbot.meeting.LLMResult;
import cn.chatbot.meeting.MeetingProcess; import cn.chatbot.meeting.MeetingProcess;
...@@ -19,10 +18,9 @@ import com.alibaba.fastjson.JSONObject; ...@@ -19,10 +18,9 @@ import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmeeting.ad.util.SecurityUtil;
import com.cmeeting.constant.KnowledgePlatformRouteConstant; import com.cmeeting.constant.KnowledgePlatformRouteConstant;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.constant.RecordTemplateConstant; import com.cmeeting.constant.RecordTemplateConstant;
import com.cmeeting.constant.UserAdminRouteConstant;
import com.cmeeting.dto.DocResultDto; import com.cmeeting.dto.DocResultDto;
import com.cmeeting.dto.MeetTypeDto; import com.cmeeting.dto.MeetTypeDto;
import com.cmeeting.dto.UserDTO; import com.cmeeting.dto.UserDTO;
...@@ -61,7 +59,6 @@ import org.apache.commons.io.IOUtils; ...@@ -61,7 +59,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor; import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
...@@ -70,11 +67,8 @@ import java.io.*; ...@@ -70,11 +67,8 @@ import java.io.*;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.time.*;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
...@@ -116,21 +110,35 @@ public class FileProcessTask { ...@@ -116,21 +110,35 @@ public class FileProcessTask {
private String applicationId; private String applicationId;
private String fileDownloadPath; private String fileDownloadPath;
private String permTenantId; private String permTenantId;
// AES加密秘钥
private String aesKey;
// 实际处理逻辑 // 实际处理逻辑
public void process() { public void process() {
// 根据meeting和subMeetingId上锁 boolean isSuccess = false;
boolean getKey = redisUtils.setnx("meeting_" + meetingId + subMeetingId, meetingId, 19 * 60); String key = "meet_process" + meetingId + "_" + (subMeetingId == null ? "" : "subMeetingId");
if (!getKey) { if (!redisUtils.setnx(key, 1, 240)) {
log.warn("key already exists in redis!, key: {}", key);
return; return;
} }
boolean isSuccess = false; log.info("线程开始------------>");
long l = System.currentTimeMillis();
Integer status = null;
while (retryCount <= MAX_RETRY && !isSuccess) { while (retryCount <= MAX_RETRY && !isSuccess) {
try { try {
//已保存的会议信息 //已保存的会议信息
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>() MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId) .eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId)); .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); String meetingDate = meetingInfo.getStartTime().toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
//下面再查一遍会议信息的意义是,为了获取会议中的子会议Id,如果是周期会议,需要保存下来(重要) //下面再查一遍会议信息的意义是,为了获取会议中的子会议Id,如果是周期会议,需要保存下来(重要)
// MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest = // MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest =
...@@ -287,9 +295,16 @@ public class FileProcessTask { ...@@ -287,9 +295,16 @@ public class FileProcessTask {
if (finalRetry) { if (finalRetry) {
meetingInfoMapper.update(null, meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>() new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId) .eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId) .eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getGenerateRetry, Boolean.TRUE)); .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 { } else {
// 指数退避 // 指数退避
...@@ -302,7 +317,8 @@ public class FileProcessTask { ...@@ -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) { private byte[] downloadFile(String url) {
...@@ -462,28 +478,31 @@ public class FileProcessTask { ...@@ -462,28 +478,31 @@ public class FileProcessTask {
* @param toUserCode 人员工号 * @param toUserCode 人员工号
* @param meetingRecordTemplate 模板信息 * @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 meetingName;
//转录文件临时存储路径 String uuid = IdUtil.fastSimpleUUID();
String recordContentPath; String today = DateUtil.today();
//生成的xml临时存储路径
String recordXmlPath = meetingId + "-recordXmlPath-" + IdUtil.fastSimpleUUID() + ".xml"; // 生成的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; String meetingMinutesFileName;
//填充后的会议纪要word文件临时路径 //填充后的会议纪要word文件临时路径
String meetingMinutesPath; String meetingMinutesPath;
boolean success = false;
try { try {
// TODO 转录文件加密并且直接上传到minio,不走layout和文档库
String subject = meetingInfo.getSubject(); String subject = meetingInfo.getSubject();
String fileName = String.format(subject + "_转写原文_%s.txt", DateUtil.today()); String fileName = String.format(subject + "_转写原文_%s.txt", DateUtil.today());
MultipartFile multipartFile = new CustomMultipartFile( MultipartFile multipartFile = new CustomMultipartFile(
"file", // 表单中的字段名 "file", // 表单中的字段名
fileName, // 原始文件名 fileName, // 原始文件名
"text/plain; charset=utf-8", // MIME类型 "text/plain; charset=utf-8", // MIME类型
recordData // 字节内容 recordData.getBytes(StandardCharsets.UTF_8) // 字节内容
); );
//将转录文件存到知识向量库--用于问答改文档 //将转录文件存到知识向量库
Map<String, String> otherParams = new HashMap<>(); Map<String, String> otherParams = new HashMap<>();
otherParams.put("userId", toUserCode); otherParams.put("userId", toUserCode);
otherParams.put("tenantId", permTenantId); otherParams.put("tenantId", permTenantId);
...@@ -494,18 +513,23 @@ public class FileProcessTask { ...@@ -494,18 +513,23 @@ public class FileProcessTask {
List<DocResultDto> docResultDtoList = JSON.parseObject(JSONObject.toJSONString(result.getData()), new TypeReference<List<DocResultDto>>() { List<DocResultDto> docResultDtoList = JSON.parseObject(JSONObject.toJSONString(result.getData()), new TypeReference<List<DocResultDto>>() {
}); });
DocResultDto docResultDto = docResultDtoList.get(0); DocResultDto docResultDto = docResultDtoList.get(0);
String previewPath = docResultDto.getPreviewPath(); // String previewPath = docResultDto.getPreviewPath();
recordContentPath = previewPath.replaceAll(fileDownloadPath, ""); // recordContentPath = previewPath.replaceAll(fileDownloadPath,"");
meetingInfo.setTransDocId(docResultDto.getId()); meetingInfo.setTransDocId(docResultDto.getId());
} else { } else {
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败,上传转录文件到向量知识库失败"); processLogService.log(meetingId, subMeetingId, "填充会议纪要失败,上传转录文件到向量知识库失败");
throw new RuntimeException("填充会议纪要失败"); throw new RuntimeException("填充会议纪要失败");
} }
// 将转录文件保存到MinIO
String encryptedRecordData = AESUtils.encrypt(recordData, aesKey);
minioUtils.upload(recordContentPath, encryptedRecordData.getBytes(StandardCharsets.UTF_8));
//去除内容中除了xml内容以外其他的信息,格式化xml //去除内容中除了xml内容以外其他的信息,格式化xml
String xml = extractXmlFromMarkdown(content); String xml = extractXmlFromMarkdown(content);
// minioUtils.upload(recordContentPath,recordData); String encryptedXml = AESUtils.encrypt(xml, aesKey);
minioUtils.upload(recordXmlPath, xml.getBytes(StandardCharsets.UTF_8)); minioUtils.upload(recordXmlPath,encryptedXml.getBytes(StandardCharsets.UTF_8));
//将xml格式的内容转换为map,用于填充模板 //将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml); Map<String, Object> dataModel = convertXmlToMap(xml);
//判断会议名称关键词,如果是用户自己定义的主题,不做修改 //判断会议名称关键词,如果是用户自己定义的主题,不做修改
...@@ -541,28 +565,31 @@ public class FileProcessTask { ...@@ -541,28 +565,31 @@ public class FileProcessTask {
} }
meetingMinutesPath = savePath + meetingMinutesFileName + ".docx"; meetingMinutesPath = savePath + meetingMinutesFileName + ".docx";
template.writeAndClose(new FileOutputStream(meetingMinutesPath)); template.writeAndClose(new FileOutputStream(meetingMinutesPath));
processLogService.log(meetingId, subMeetingId, "填充会议纪要成功"); processLogService.log(meetingId,subMeetingId,"填充会议纪要成功");
success = true;
} catch (Exception e) { } catch (Exception e) {
success = false;
log.error("填充会议纪要失败: {}", e.getMessage(), e); log.error("填充会议纪要失败: {}", e.getMessage(), e);
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw); e.printStackTrace(pw);
processLogService.log(meetingId, subMeetingId, "填充会议纪要失败" + sw.toString()); processLogService.log(meetingId, subMeetingId, "填充会议纪要失败" + sw.toString());
throw new RuntimeException("填充会议纪要失败"); throw new RuntimeException("填充会议纪要失败");
} } finally {
meetingInfoMapper.update(meetingInfo, meetingInfoMapper.update(meetingInfo,
new LambdaUpdateWrapper<MeetingInfo>() new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId) .eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId) .eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getRecordContent, recordContentPath) .set(MeetingInfo::getRecordContent,recordContentPath)
.set(MeetingInfo::getRecordXml, recordXmlPath) .set(MeetingInfo::getRecordXml,recordXmlPath)
.set(MeetingInfo::getParticipantUsers, meetingInfo.getParticipantUsers()) .set(MeetingInfo::getParticipantUsers,meetingInfo.getParticipantUsers())
.set(MeetingInfo::getIsGenerated, Boolean.TRUE) .set(MeetingInfo::getIsGenerated,Boolean.TRUE)
.set(MeetingInfo::getTemplateId, meetingRecordTemplate.getId()) .set(MeetingInfo::getTemplateId,meetingRecordTemplate.getId())
.set(MeetingInfo::getTransDocId, meetingInfo.getTransDocId()) .set(MeetingInfo::getTransDocId,meetingInfo.getTransDocId())
.set(MeetingInfo::getSubject, meetingInfo.getSubject()) .set(MeetingInfo::getSubject,meetingInfo.getSubject())
.set(MeetingInfo::getStatus, success ? MeetingState.NOTE_GENERATED.getCode() : MeetingState.GENERATE_ERROR.getCode())
); );
}
meetingInfo.setRecordContent(recordContentPath); meetingInfo.setRecordContent(recordContentPath);
meetingInfo.setRecordXml(recordXmlPath); meetingInfo.setRecordXml(recordXmlPath);
return savePath + meetingMinutesFileName + ".docx"; return savePath + meetingMinutesFileName + ".docx";
...@@ -588,8 +615,9 @@ public class FileProcessTask { ...@@ -588,8 +615,9 @@ public class FileProcessTask {
meetingInfoMapper.update(null, meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>() new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId, meetingId) .eq(MeetingInfo::getMeetingId, meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId) .eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed, isPushed) .set(MeetingInfo::getIsPushed, isPushed)
.set(MeetingInfo::getStatus, isPushed ? MeetingState.PUSH_SUCCESS.getCode() : MeetingState.PUSH_ERROR.getCode())
); );
} }
...@@ -708,7 +736,8 @@ public class FileProcessTask { ...@@ -708,7 +736,8 @@ public class FileProcessTask {
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender, MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, RedisUtils redisUtils, EmailSender emailSender,
MeetingRecordTemplateMapper meetingRecordTemplateMapper, MeetingRecordTemplateService meetingRecordTemplateService, MeetTypeService meetTypeService, UserIdMapper userIdMapper, MeetingRecordTemplateMapper meetingRecordTemplateMapper, MeetingRecordTemplateService meetingRecordTemplateService, MeetTypeService meetTypeService, UserIdMapper userIdMapper,
String llmApiAddr, Boolean finalRetry, ProcessLogService processLogService, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations, 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.recordFileIdList = recordFileIdList;
this.savePath = savePath; this.savePath = savePath;
this.metadata = metadata; this.metadata = metadata;
...@@ -732,5 +761,10 @@ public class FileProcessTask { ...@@ -732,5 +761,10 @@ public class FileProcessTask {
this.applicationId = applicationId; this.applicationId = applicationId;
this.fileDownloadPath = fileDownloadPath; this.fileDownloadPath = fileDownloadPath;
this.permTenantId = permTenantId; 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 { ...@@ -99,6 +99,10 @@ public class MeetingInfo implements Serializable {
*/ */
private Boolean isPushed; private Boolean isPushed;
/** /**
* 会议状态: 0新会议待处理 1生成纪要失败 2生成纪要成功 3推送邮件成功 4推送邮件失败 5转录文件为空
*/
private Integer status;
/**
* 会议纪要重新生成标识 * 会议纪要重新生成标识
*/ */
private Boolean generateRetry; private Boolean generateRetry;
......
...@@ -32,9 +32,12 @@ import java.util.concurrent.Future; ...@@ -32,9 +32,12 @@ import java.util.concurrent.Future;
@Slf4j @Slf4j
public class FileProcessProducer { public class FileProcessProducer {
@Autowired @Resource(name = "fileProcessExecutor")
private ThreadPoolTaskExecutor fileProcessExecutor; private ThreadPoolTaskExecutor fileProcessExecutor;
@Resource(name = "emailProcessExecutor")
private ThreadPoolTaskExecutor emailProcessExecutor;
@Value("${aec.key}")
public String aesKey;
@Autowired @Autowired
private FileProcessCallbackHandler callbackHandler; private FileProcessCallbackHandler callbackHandler;
@Value(value = "${llm.api-addr}") @Value(value = "${llm.api-addr}")
...@@ -81,6 +84,7 @@ public class FileProcessProducer { ...@@ -81,6 +84,7 @@ public class FileProcessProducer {
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations, Boolean finalRetry) { public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, List<UserDTO.TemplateAuthorizedUserDTO> authorizedUsers, Map<String, String> tidWidRelations, Boolean finalRetry) {
List<Future<?>> futures = new ArrayList<>(); List<Future<?>> futures = new ArrayList<>();
String adminToken = UserAdminTokenUtil.getUserAdminToken(); String adminToken = UserAdminTokenUtil.getUserAdminToken();
log.info("待处理会议数量: {}", recordFiles.size());
for (TencentMeetingVO.RecordFile recordFile : recordFiles) { for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务 // 为每个URL创建任务
FileProcessTask task = new FileProcessTask( FileProcessTask task = new FileProcessTask(
...@@ -106,7 +110,8 @@ public class FileProcessProducer { ...@@ -106,7 +110,8 @@ public class FileProcessProducer {
adminToken, adminToken,
applicationId, applicationId,
fileDownloadPath, fileDownloadPath,
permTenantId permTenantId,
aesKey
); );
// 提交任务到线程池 // 提交任务到线程池
...@@ -135,13 +140,14 @@ public class FileProcessProducer { ...@@ -135,13 +140,14 @@ public class FileProcessProducer {
Collections.emptyMap(), Collections.emptyMap(),
meetingInfoMapper, meetingInfoMapper,
minioUtils, minioUtils,
redisUtils,
emailSender, emailSender,
meetingRecordTemplateMapper, meetingRecordTemplateMapper,
tidWidRelations tidWidRelations
); );
// 提交任务到线程池 // 提交任务到线程池
Future<?> future = fileProcessExecutor.submit(() -> { Future<?> future = emailProcessExecutor.submit(() -> {
task.process(); task.process();
}); });
......
...@@ -18,6 +18,12 @@ public interface MeetingInfoService extends IService<MeetingInfo> { ...@@ -18,6 +18,12 @@ public interface MeetingInfoService extends IService<MeetingInfo> {
boolean regenerateXml(MeetingInfoVO vo); boolean regenerateXml(MeetingInfoVO vo);
/** /**
* 统计邮件推送情况
*/
void statisticsEmail(Integer type, Date startTime, Date endTime, HttpServletResponse response);
/**
* 统计 * 统计
* *
* @param searchValue 查询值 * @param searchValue 查询值
......
...@@ -25,6 +25,7 @@ import com.cmeeting.ad.entity.RobotSecurityUser; ...@@ -25,6 +25,7 @@ import com.cmeeting.ad.entity.RobotSecurityUser;
import com.cmeeting.ad.util.SecurityUtil; import com.cmeeting.ad.util.SecurityUtil;
import com.cmeeting.constant.RecordTemplateConstant; import com.cmeeting.constant.RecordTemplateConstant;
import com.cmeeting.exception.RobotBaseException; import com.cmeeting.exception.RobotBaseException;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.log.service.ProcessLogService; import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.mapper.primary.MeetingInfoMapper; import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper; import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
...@@ -34,6 +35,9 @@ import com.cmeeting.pojo.MeetingInfo; ...@@ -34,6 +35,9 @@ import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate; import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.pojo.UserId; import com.cmeeting.pojo.UserId;
import com.cmeeting.service.MeetingInfoService; 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.MinioUtils;
import com.cmeeting.util.page.PageUtil; import com.cmeeting.util.page.PageUtil;
import com.cmeeting.vo.MeetingInfoVO; import com.cmeeting.vo.MeetingInfoVO;
...@@ -56,6 +60,7 @@ import org.apache.commons.lang3.StringUtils; ...@@ -56,6 +60,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
...@@ -103,6 +108,17 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti ...@@ -103,6 +108,17 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti
@Autowired @Autowired
private SysUserSysMapper sysUserSysMapper; 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 @Override
public IPage<MeetingInfo> getPage(MeetingInfoVO vo) { public IPage<MeetingInfo> getPage(MeetingInfoVO vo) {
...@@ -575,4 +591,128 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti ...@@ -575,4 +591,128 @@ public class MeetingInfoServiceImpl extends ServiceImpl<MeetingInfoMapper, Meeti
throw new RuntimeException("下载文件失败", e); 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 ...@@ -126,7 +126,7 @@ public class RecordTemplatePermissionServiceImpl extends ServiceImpl<RecordTempl
? new HashMap<>() : simpleUserList.stream().collect(Collectors.toMap(SysUserSync::getUserId, SysUserSync::getName)); ? new HashMap<>() : simpleUserList.stream().collect(Collectors.toMap(SysUserSync::getUserId, SysUserSync::getName));
List<SysDept> simpleDeptList = sysUserSysMapper.getSimpleDeptList(user.getTenantId()); List<SysDept> simpleDeptList = sysUserSysMapper.getSimpleDeptList(user.getTenantId());
Map<String, String> simpleDeptMap = CollectionUtils.isEmpty(simpleDeptList) 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) { for (RecordTemplatePermission permission : permissions) {
String relName = "1".equals(permission.getRelType()) && simpleDeptMap.containsKey(permission.getRelId()) String relName = "1".equals(permission.getRelType()) && simpleDeptMap.containsKey(permission.getRelId())
? simpleDeptMap.get(permission.getRelId()) ? simpleDeptMap.get(permission.getRelId())
......
...@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; ...@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.constant.PermissionPruposeType; import com.cmeeting.constant.PermissionPruposeType;
import com.cmeeting.constant.MeetingState;
import com.cmeeting.dto.UserDTO; import com.cmeeting.dto.UserDTO;
import com.cmeeting.log.service.ProcessLogService; import com.cmeeting.log.service.ProcessLogService;
import com.cmeeting.mapper.primary.AuthMapper; import com.cmeeting.mapper.primary.AuthMapper;
...@@ -45,7 +46,10 @@ import java.io.PrintWriter; ...@@ -45,7 +46,10 @@ import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*; import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
...@@ -76,6 +80,11 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -76,6 +80,11 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
@Value(value = "${tencent.search-scope}") @Value(value = "${tencent.search-scope}")
private Integer searchDays; private Integer searchDays;
/**
* 查询会议不得早于这个时间
*/
@Value(value = "${tencent.search-end-time}")
private String searchEndTime;
@Value(value = "${tencent.appId}") @Value(value = "${tencent.appId}")
private String tencentAppId; private String tencentAppId;
@Value(value = "${tencent.sdkId}") @Value(value = "${tencent.sdkId}")
...@@ -124,24 +133,39 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -124,24 +133,39 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds, Map<String, WeComUser> weComUserMap) { public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds, Map<String, WeComUser> weComUserMap) {
List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>(); List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>();
List<MeetingInfo> meetingSaveList = new ArrayList<>(); List<MeetingInfo> meetingSaveList = new ArrayList<>();
int i = 0;
// 查询近searchDays天的会议录制列表 // 查询近searchDays天的会议录制列表
try { try {
ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime now = ZonedDateTime.now();
long startTime = now.minusDays(searchDays).toEpochSecond(); long startTime = now.minusDays(searchDays).toEpochSecond();
long endTime = now.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); AtomicInteger currentPage = new AtomicInteger(1);
//获取总页数 //获取总页数
CorpRecordsVO firstData = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, 1, 20); CorpRecordsVO firstData = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, 1, 20);
Integer totalPage = firstData.getTotalPage(); Integer totalPage = firstData.getTotalPage();
log.info("总页数: {}", totalPage);
//目前已存储的会议id //目前已存储的会议id
List<TencentMeetingVO.SimpleMeetingInfo> meetingIds = meetingInfoMapper.getAllMeetingIds(); List<TencentMeetingVO.SimpleMeetingInfo> meetingIds = meetingInfoMapper.getAllMeetingIds();
List<TencentMeetingUser> meetingUsers = tecentMeetingMapper.getAlluser(); List<TencentMeetingUser> meetingUsers = tecentMeetingMapper.getAlluser();
Map<String, String> meetingMap = meetingUsers.stream().collect(Collectors.toMap(TencentMeetingUser::getUserId, TencentMeetingUser::getUserName)); Map<String, String> meetingMap = meetingUsers.stream().collect(Collectors.toMap(TencentMeetingUser::getUserId, TencentMeetingUser::getUserName));
while (currentPage.intValue() <= totalPage){
if (i >= 150) {
while (currentPage.intValue() <= totalPage) { break;
}
CorpRecordsVO data = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, currentPage.getAndIncrement(), 20); CorpRecordsVO data = fetchMeetingRecords(tencentAdminUserId, 1, startTime, endTime, currentPage.getAndIncrement(), 20);
//设置总页数 //设置总页数
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) { if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
...@@ -218,8 +242,9 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -218,8 +242,9 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
if (host.isPresent()) { if (host.isPresent()) {
hostId = host.get().getUserid(); hostId = host.get().getUserid();
hostName = new String(Base64.getDecoder().decode(host.get().getUserName())); hostName = new String(Base64.getDecoder().decode(host.get().getUserName()));
} else { }else{
log.error("未找到主持人,默认没有生成纪要权限"); log.error("未找到主持人,默认没有生成纪要权限, meetId: {}, subId: {}", meetingId, subMeetingId);
// processLogService.log(meeting.getMeetingId(),subMeetingId,"未找到主持人,默认没有生成纪要权限");
continue; continue;
} }
} }
...@@ -269,12 +294,14 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -269,12 +294,14 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
.subMeetingId(subMeetingId).generateRetry(Boolean.FALSE).pushRetry(Boolean.FALSE) .subMeetingId(subMeetingId).generateRetry(Boolean.FALSE).pushRetry(Boolean.FALSE)
.host(hostName) .host(hostName)
.hostUid(hostId) .hostUid(hostId)
.status(MeetingState.NEW.getCode())
// .participantUsers(participants.stream() // .participantUsers(participants.stream()
// .map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、"))) // .map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、")))
.recordFileId(String.join(",", recordFileIdList)) .recordFileId(String.join(",", recordFileIdList))
.email(email) .email(email)
.build(); .build();
meetingSaveList.add(meetingItem); meetingSaveList.add(meetingItem);
i++;
} }
} catch (Exception e) { } catch (Exception e) {
if (e instanceof NoSuchElementException) { if (e instanceof NoSuchElementException) {
...@@ -296,11 +323,14 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -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()); List<String> recordFileIdList = entry.getValue().stream().flatMap(s -> Arrays.stream(s.getRecordFileId().split(","))).collect(Collectors.toList());
meetingInfo.setRecordFileId(String.join(",", recordFileIdList)); meetingInfo.setRecordFileId(String.join(",", recordFileIdList));
finalSaveList.add(meetingInfo); finalSaveList.add(meetingInfo);
if (meetingInfo.getEmailPushAccess()) {
meetingFiles.add(TencentMeetingVO.RecordFile.builder() meetingFiles.add(TencentMeetingVO.RecordFile.builder()
.meetingId(meetingInfo.getMeetingId()) .meetingId(meetingInfo.getMeetingId())
.subMeetingId(meetingInfo.getSubMeetingId()) .subMeetingId(meetingInfo.getSubMeetingId())
.recordFileIdList(recordFileIdList).build()); .recordFileIdList(recordFileIdList).build());
} }
}
meetingInfoMapper.batchInsert(finalSaveList); meetingInfoMapper.batchInsert(finalSaveList);
} }
} catch (Exception e) { } catch (Exception e) {
...@@ -415,7 +445,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -415,7 +445,7 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
// 3. 生成签名 // 3. 生成签名
String signature = SignatureUtil.generateSignature(tencentSecretId, tencentSecretKey, httpMethod, nonce, timestamp, uri, ""); String signature = SignatureUtil.generateSignature(tencentSecretId, tencentSecretKey, httpMethod, nonce, timestamp, uri, "");
log.info("fetchMeetingRecords params: {}, {}", page, pageSize);
// 4. 发送请求 // 4. 发送请求
try (CloseableHttpClient httpClient = HttpClients.createDefault()) { try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet("https://api.meeting.qq.com" + uri); HttpGet request = new HttpGet("https://api.meeting.qq.com" + uri);
...@@ -597,4 +627,5 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper, ...@@ -597,4 +627,5 @@ public class TencentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,
return null; 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 { ...@@ -140,6 +140,14 @@ public class RedisUtils {
return false; 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) { public boolean setnx(String key, Object value, long time) {
try { 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 ...@@ -30,7 +30,7 @@ CORP_SECRET: 7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0
############################################################## email ############################################################## email
EMAIL_SENDER: cmeeting_assistant@cimc.com 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 EMAIL_SMTP_HOST: smtp.office365.com
############################################################## permission ############################################################## permission
...@@ -105,10 +105,11 @@ tencent: ...@@ -105,10 +105,11 @@ tencent:
aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
base-save-path: E:/save/ #会议纪要临时文件存储路径 base-save-path: E:/save/ #会议纪要临时文件存储路径
search-scope: 2 #腾会转录文件拉取时间范围,查询过去n天的记录 search-scope: 2 #腾会转录文件拉取时间范围,查询过去n天的记录
search-end-time: '2025-07-30 23:59:59' #此时间之前的数据不拉取
email: email:
sender: ${EMAIL_SENDER} sender: ${EMAIL_SENDER}
sender-pwd: ${EMAIL_SENDER_PWD} # sender-pwd: ${EMAIL_SENDER_PWD}
smtp-host: ${EMAIL_SMTP_HOST} smtp-host: ${EMAIL_SMTP_HOST}
push-switch: true #邮件推送总开关,高优先级 push-switch: true #邮件推送总开关,高优先级
environment: test #test推给本公司人员,prod推给用户 environment: test #test推给本公司人员,prod推给用户
...@@ -152,6 +153,15 @@ permission: ...@@ -152,6 +153,15 @@ permission:
tenantId: ${PERMISSION_TENANT_ID} tenantId: ${PERMISSION_TENANT_ID}
admin-white_users: ${PERMISSION_ADMIN_WHITE_USERS} 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 record-template-path-default: /2025-07-21/a12465sdfsa2gas.docx
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<insert id="batchInsert" parameterType="list"> <insert id="batchInsert" parameterType="list">
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host, host_uid, participant_users, start_time, 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, 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 VALUES
<foreach collection="meetingSaveList" item="meeting" separator=","> <foreach collection="meetingSaveList" item="meeting" separator=",">
( (
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#{meeting.generateRetry}, #{meeting.generateRetry},
#{meeting.pushRetry}, #{meeting.pushRetry},
#{meeting.recordFileId}, #{meeting.recordFileId},
#{meeting.status},
#{meeting.email} #{meeting.email}
) )
</foreach> </foreach>
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
</select> </select>
<select id="getSimpleDeptList" resultType="com.cmeeting.ad.entity.SysDept"> <select id="getSimpleDeptList" resultType="com.cmeeting.ad.entity.SysDept">
SELECT SELECT
susc.id, susc.dept_id,
susc.`name` susc.`name`
FROM FROM
sys_user_sync_category AS susc sys_user_sync_category AS susc
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论