提交 e0b7d23d 作者: duanxincheng

二次重试机制;模板创建与维护;会议纪要生成权限控制

父级 b87c127c
......@@ -330,11 +330,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.0</version> <!-- 与你的Spring Boot版本一致 -->
</dependency>
</dependencies>
......
......@@ -146,7 +146,7 @@ public class TencentMeetingCallbackController {
//通过智能体id查询该id下的部门和用户
String targetId = "1815393211829587968";//职能体id
String tenantId = "1806976109082972160";//租户id
List<CoreModulePermissions> auths = authMapper.getAuthByTargrtId(targetId, tenantId);
List<CoreModulePermissions> auths = authMapper.getAuthByTargetId(targetId, tenantId);
// 创建两个集合分别存储type=0(部门)和type=1(员工)的数据
List<CoreModulePermissions> type0List = new ArrayList<>();
List<CoreModulePermissions> type1List = new ArrayList<>();
......
package com.cmeeting;
import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.WeComUser;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.user_manager.api.UserManagerApi;
import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200Response;
import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200ResponseUsersInner;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class WeComAndTencentMeeting {
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
private static final String ACCESS_TOKEN = "wyYAmCbeVIPvj_Gpdd8ixp_td_hPv2OtTU8gpTGSpB7TaftbpNps5fc_JdCRzpgy_tUv-aGueJpRV6XJeYAG-rqkzCfDV-rcpJbqB0LypjvJUtxPeTmPqEtQuCMwH6euGGVaJZXoc33ATrS70U0T4_4WIrATqpsN2Ed2XsNGyN5N6aG8mq6AyLMG9TokVWx1qd2qF1zVTwve6aqRMWHwXg";
public static void main(String[] args) {
try {
// 示例:获取部门ID为6的用户列表
int departmentId = 6;
JsonObject result = getUserListByDepartment(departmentId);
// 处理返回结果
if (result.get("errcode").getAsInt() == 0) {
System.out.println("企微获取用户列表成功:");
JsonArray userList = result.getAsJsonArray("userlist");
for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject();
System.out.println("姓名: " + user.get("name").getAsString() +
", UserID: " + user.get("userid").getAsString());
}
} else {
System.out.println("企微获取用户列表失败: " + result.get("errmsg").getAsString());
}
} catch (Exception e) {
e.printStackTrace();
}
/**
* 腾讯会议通过通讯录获取员工信息
*/
// 1. 构造client客户端
Client client = new Client.Builder()
.withAppId("211153201").withSdkId("28370276340")
.withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
.build();
// 2. 开始循环获取用户列表
fetchUsersInBatches(client, 3);
}
/**
* 将企业微信返回的JSON转换为WeComUser列表
*/
public static List<WeComUser> convertJsonToWeComUsers(JsonObject json) {
List<WeComUser> users = new ArrayList<>();
if (json.has("userlist") && json.get("userlist").isJsonArray()) {
JsonArray userList = json.getAsJsonArray("userlist");
Gson gson = new Gson();
for (JsonElement element : userList) {
JsonObject userJson = element.getAsJsonObject();
WeComUser user = new WeComUser();
// 根据企业微信API实际返回字段调整
user.setUserId(userJson.get("userid").getAsString());
user.setUserName(userJson.get("name").getAsString());
// 其他字段设置...
users.add(user);
}
}
return users;
}
/**
* 标记企业微信重名用户
*/
public static void markDuplicateNames(List<WeComUser> users) {
// 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(WeComUser::getUserName, Collectors.counting()));
// 设置是否重名标志
users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsRepeatName("1"); // 重名
} else {
user.setIsRepeatName("0"); // 不重名
}
});
}
/**
* 标记企业微信重名用户
*/
public static void markDuplicateNamesTecent(List<TencentMeetingUser> users) {
// 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(TencentMeetingUser::getUserName, Collectors.counting()));
// 设置是否重名标志
users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsrepeatName("1"); // 重名
} else {
user.setIsrepeatName("0"); // 不重名
}
});
}
/**
* 根据企业微信部门ID获取用户列表
*
* @param departmentId 部门ID
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public static JsonObject getUserListByDepartment(int departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
}
/**
* 腾讯会议获取员工信息
*/
/**
* 分批次获取用户列表
*
* @param client 客户端实例
* @param pageSize 每页大小
* @return
*/
public static List<TencentMeetingUser> fetchUsersInBatches(Client client, int pageSize) {
int currentPage = 1;
boolean hasMore = true;
int totalUsers = 0;
List<TencentMeetingUser> userList = new ArrayList<>(); // 创建集合存储用户数据
while (hasMore) {
try {
// 1. 构造请求参数
UserManagerApi.ApiV1UsersListGetRequest request =
new UserManagerApi.ApiV1UsersListGetRequest.Builder()
.page(String.valueOf(currentPage))
.pageSize(String.valueOf(pageSize))
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1")
.build();
// 2. 构造JWT鉴权器
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
// 3. 发送请求
UserManagerApi.ApiV1UsersListGetResponse response =
client.user_manager().v1UsersListGet(request, authenticatorBuilder);
// 4. 处理响应并转换为TencentMeetingUser对象
V1UsersListGet200Response responseData = response.getData();
List<V1UsersListGet200ResponseUsersInner> users = responseData.getUsers();
int currentSize = users.size();
totalUsers += currentSize;
System.out.printf("第 %d 页,获取到 %d 个用户:\n", currentPage, currentSize);
for (V1UsersListGet200ResponseUsersInner user : users) {
// 创建TencentMeetingUser对象并设置属性
TencentMeetingUser meetingUser = new TencentMeetingUser();
meetingUser.setUserId(user.getUserid());
meetingUser.setUserName(user.getUsername());
meetingUser.setIsrepeatName("0"); // 默认设为不重名,可根据实际业务调整
userList.add(meetingUser); // 添加到集合
System.out.printf("用户ID: %s, 用户名: %s\n", user.getUserid(), user.getUsername());
}
// 5. 检查是否还有更多数据
if (currentSize == 0 || currentSize < pageSize) {
hasMore = false;
System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", totalUsers);
} else {
currentPage++;
}
} catch (ClientException e) {
System.out.printf("客户端错误: %s\n", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
System.out.printf("服务端错误: %s\n", e);
System.out.printf("完整响应: %s\n", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
}
return userList; // 返回用户集合
}
}
//package com.cmeeting;
//
//import com.cmeeting.pojo.TencentMeetingUser;
//import com.cmeeting.pojo.WeComUser;
//import com.google.gson.Gson;
//import com.google.gson.JsonArray;
//import com.google.gson.JsonElement;
//import com.google.gson.JsonObject;
//
//import java.io.BufferedReader;
//import java.io.InputStreamReader;
//import java.net.HttpURLConnection;
//import java.net.URL;
//
//import com.tencentcloudapi.wemeet.Client;
//import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
//import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
//import com.tencentcloudapi.wemeet.core.exception.ClientException;
//import com.tencentcloudapi.wemeet.core.exception.ServiceException;
//import com.tencentcloudapi.wemeet.service.user_manager.api.UserManagerApi;
//import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200Response;
//import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200ResponseUsersInner;
//import org.springframework.stereotype.Service;
//
//import java.math.BigInteger;
//import java.security.SecureRandom;
//import java.util.ArrayList;
//import java.util.List;
//import java.util.Map;
//import java.util.stream.Collectors;
//@Service
//public class WeComAndTencentMeeting {
// private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
// private static final String ACCESS_TOKEN = "wyYAmCbeVIPvj_Gpdd8ixp_td_hPv2OtTU8gpTGSpB7TaftbpNps5fc_JdCRzpgy_tUv-aGueJpRV6XJeYAG-rqkzCfDV-rcpJbqB0LypjvJUtxPeTmPqEtQuCMwH6euGGVaJZXoc33ATrS70U0T4_4WIrATqpsN2Ed2XsNGyN5N6aG8mq6AyLMG9TokVWx1qd2qF1zVTwve6aqRMWHwXg";
// public static void main(String[] args) {
// try {
// // 示例:获取部门ID为6的用户列表
// int departmentId = 6;
// JsonObject result = getUserListByDepartment(departmentId);
// // 处理返回结果
// if (result.get("errcode").getAsInt() == 0) {
// System.out.println("企微获取用户列表成功:");
// JsonArray userList = result.getAsJsonArray("userlist");
// for (int i = 0; i < userList.size(); i++) {
// JsonObject user = userList.get(i).getAsJsonObject();
// System.out.println("姓名: " + user.get("name").getAsString() +
// ", UserID: " + user.get("userid").getAsString());
// }
// } else {
// System.out.println("企微获取用户列表失败: " + result.get("errmsg").getAsString());
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// /**
// * 腾讯会议通过通讯录获取员工信息
// */
// // 1. 构造client客户端
// Client client = new Client.Builder()
// .withAppId("211153201").withSdkId("28370276340")
// .withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
// .build();
//
// // 2. 开始循环获取用户列表
// fetchUsersInBatches(client, 3);
// }
// /**
// * 将企业微信返回的JSON转换为WeComUser列表
// */
// public static List<WeComUser> convertJsonToWeComUsers(JsonObject json) {
// List<WeComUser> users = new ArrayList<>();
//
// if (json.has("userlist") && json.get("userlist").isJsonArray()) {
// JsonArray userList = json.getAsJsonArray("userlist");
// Gson gson = new Gson();
//
// for (JsonElement element : userList) {
// JsonObject userJson = element.getAsJsonObject();
// WeComUser user = new WeComUser();
//
// // 根据企业微信API实际返回字段调整
// user.setUserId(userJson.get("userid").getAsString());
// user.setUserName(userJson.get("name").getAsString());
//
// // 其他字段设置...
// users.add(user);
// }
// }
//
// return users;
// }
//
// /**
// * 标记企业微信重名用户
// */
// public static void markDuplicateNames(List<WeComUser> users) {
// // 按姓名分组,统计每个名字出现的次数
// Map<String, Long> nameCountMap = users.stream()
// .collect(Collectors.groupingBy(WeComUser::getUserName, Collectors.counting()));
//
// // 设置是否重名标志
// users.forEach(user -> {
// if (nameCountMap.get(user.getUserName()) > 1) {
// user.setIsRepeatName("1"); // 重名
// } else {
// user.setIsRepeatName("0"); // 不重名
// }
// });
// }
//
// /**
// * 标记企业微信重名用户
// */
// public static void markDuplicateNamesTecent(List<TencentMeetingUser> users) {
// // 按姓名分组,统计每个名字出现的次数
// Map<String, Long> nameCountMap = users.stream()
// .collect(Collectors.groupingBy(TencentMeetingUser::getUserName, Collectors.counting()));
//
// // 设置是否重名标志
// users.forEach(user -> {
// if (nameCountMap.get(user.getUserName()) > 1) {
// user.setIsrepeatName("1"); // 重名
// } else {
// user.setIsrepeatName("0"); // 不重名
// }
// });
// }
//
// /**
// * 根据企业微信部门ID获取用户列表
// *
// * @param departmentId 部门ID
// * @return 包含用户列表的JsonObject
// * @throws Exception
// */
// public static JsonObject getUserListByDepartment(int departmentId) throws Exception {
// String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
//
// URL url = new URL(urlStr);
// HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// conn.setRequestMethod("GET");
//
// int responseCode = conn.getResponseCode();
// if (responseCode == HttpURLConnection.HTTP_OK) {
// BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
// String inputLine;
// StringBuilder response = new StringBuilder();
//
// while ((inputLine = in.readLine()) != null) {
// response.append(inputLine);
// }
// in.close();
//
// // 使用Gson解析JSON响应
// Gson gson = new Gson();
// return gson.fromJson(response.toString(), JsonObject.class);
// } else {
// throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
// }
// }
//
// /**
// * 腾讯会议获取员工信息
// */
// /**
// * 分批次获取用户列表
// *
// * @param client 客户端实例
// * @param pageSize 每页大小
// * @return
// */
// public static List<TencentMeetingUser> fetchUsersInBatches(Client client, int pageSize) {
// int currentPage = 1;
// boolean hasMore = true;
// int totalUsers = 0;
// List<TencentMeetingUser> userList = new ArrayList<>(); // 创建集合存储用户数据
//
// while (hasMore) {
// try {
// // 1. 构造请求参数
// UserManagerApi.ApiV1UsersListGetRequest request =
// new UserManagerApi.ApiV1UsersListGetRequest.Builder()
// .page(String.valueOf(currentPage))
// .pageSize(String.valueOf(pageSize))
// .operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
// .operatorIdType("1")
// .build();
//
// // 2. 构造JWT鉴权器
// BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
// AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
// new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
//
// // 3. 发送请求
// UserManagerApi.ApiV1UsersListGetResponse response =
// client.user_manager().v1UsersListGet(request, authenticatorBuilder);
//
// // 4. 处理响应并转换为TencentMeetingUser对象
// V1UsersListGet200Response responseData = response.getData();
// List<V1UsersListGet200ResponseUsersInner> users = responseData.getUsers();
// int currentSize = users.size();
// totalUsers += currentSize;
//
// System.out.printf("第 %d 页,获取到 %d 个用户:\n", currentPage, currentSize);
//
// for (V1UsersListGet200ResponseUsersInner user : users) {
// // 创建TencentMeetingUser对象并设置属性
// TencentMeetingUser meetingUser = new TencentMeetingUser();
// meetingUser.setUserId(user.getUserid());
// meetingUser.setUserName(user.getUsername());
// meetingUser.setIsrepeatName("0"); // 默认设为不重名,可根据实际业务调整
//
// userList.add(meetingUser); // 添加到集合
// System.out.printf("用户ID: %s, 用户名: %s\n", user.getUserid(), user.getUsername());
// }
//
// // 5. 检查是否还有更多数据
// if (currentSize == 0 || currentSize < pageSize) {
// hasMore = false;
// System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", totalUsers);
// } else {
// currentPage++;
// }
//
// } catch (ClientException e) {
// System.out.printf("客户端错误: %s\n", e);
// throw new RuntimeException(e);
// } catch (ServiceException e) {
// System.out.printf("服务端错误: %s\n", e);
// System.out.printf("完整响应: %s\n", new String(e.getApiResp().getRawBody()));
// throw new RuntimeException(e);
// }
// }
//
// return userList; // 返回用户集合
//
// }
//}
......@@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController
@RequestMapping("/tencent")
......@@ -24,8 +23,8 @@ public class TencentMeetingController {
tecentMeetingService.doUsers();
}
@GetMapping("/getMeetingFiles")
public void getMeetingFiles(){
tecentMeetingService.getMeetingFiles();
}
// @GetMapping("/getMeetingFiles")
// public void getMeetingFiles(){
// tecentMeetingService.getMeetingFiles();
// }
}
......@@ -127,32 +127,32 @@ public class UnifiedController {
//取消预约会议
}
//todo 待测试
@GetMapping("/sameNameInsertTid")
public void sameNameInsertTid(String corpid, String corpsecret) throws IOException {
String weComToken = getWeComToken(corpid,corpsecret);
List<WeComUser> sameNameUsers = weComUserMapper.getSameName();
List<UserId> userIds = new ArrayList<>();
for (WeComUser user : sameNameUsers) {
Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getUserId(), weComToken);
String meetingId = meetingCodeAndMeetingid.get("meetingid");
String meetingCode = meetingCodeAndMeetingid.get("meeting_code");
if (meetingCode == null || meetingCode.isEmpty()) {
System.err.println("会议创建失败,跳过用户: " + user.getUserId());
continue;
}
// 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid)
String hostUserId = getMeetingHost(meetingCode);
if (hostUserId == null || hostUserId.isEmpty()) {
System.err.println("获取主持人失败,跳过会议: " + meetingCode);
cancelMeeting(meetingId, weComToken); // 尝试取消无效会议
continue;
}
UserId userId = new UserId(user.getUserName(), user.getUserId(), hostUserId);
userIds.add(userId);
}
userIdMapper.insertUsers(userIds);
}
// //todo 待测试
// @GetMapping("/sameNameInsertTid")
// public void sameNameInsertTid(String corpid, String corpsecret) throws IOException {
// String weComToken = getWeComToken(corpid,corpsecret);
// List<WeComUser> sameNameUsers = weComUserMapper.getSameName();
// List<UserId> userIds = new ArrayList<>();
// for (WeComUser user : sameNameUsers) {
// Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getUserId(), weComToken);
// String meetingId = meetingCodeAndMeetingid.get("meetingid");
// String meetingCode = meetingCodeAndMeetingid.get("meeting_code");
// if (meetingCode == null || meetingCode.isEmpty()) {
// System.err.println("会议创建失败,跳过用户: " + user.getUserId());
// continue;
// }
// // 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid)
// String hostUserId = getMeetingHost(meetingCode);
// if (hostUserId == null || hostUserId.isEmpty()) {
// System.err.println("获取主持人失败,跳过会议: " + meetingCode);
// cancelMeeting(meetingId, weComToken); // 尝试取消无效会议
// continue;
// }
// UserId userId = new UserId(user.getUserName(), user.getUserId(), hostUserId);
// userIds.add(userId);
// }
// userIdMapper.insertUsers(userIds);
// }
/**
* 通过企业微信接口取消预约会议
......
......@@ -18,7 +18,6 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController
@RequestMapping("/wecom")
......
// vo/LoginVO.java
package com.cmeeting.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
/**
* 员工姓名
*/
private String userName;
/**
* 企业微信userid
*/
private String wid;
/**
* 腾讯会议userid
*/
private String tid;
/**
* 邮箱
*/
private String email;
}
\ No newline at end of file
package com.cmeeting.email;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
......@@ -29,10 +30,10 @@ public class EmailSender {
* @param toEmail 收件人
* @param subject 邮件主题
* @param attachmentPath 附件路径
* @param recordFileId 转录文件ID
* @param meetingId 会议id
* @return
*/
public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String recordFileId) throws Exception {
public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String meetingId) {
// 邮件服务器配置
Properties props = new Properties();
props.put("mail.smtp.host", SMTP_HOST);
......@@ -61,60 +62,63 @@ public class EmailSender {
" 附件为您本次会议的会议纪要,烦请下载查看";
AtomicInteger retryCount = new AtomicInteger(0);
boolean isSent = false;
while (retryCount.intValue() < MAX_RETRY && !isSent){
try {
// 创建邮件消息
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(toEmail));
message.setSubject(subject);
// 创建消息体部分(正文)
MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText(body);
// 创建多部分消息
Multipart multipart = new MimeMultipart();
// 添加文本部分
multipart.addBodyPart(messageBodyPart);
// 添加附件部分
if (attachmentPath != null && !attachmentPath.isEmpty()) {
MimeBodyPart attachmentPart = new MimeBodyPart();
DataSource source = new FileDataSource(attachmentPath);
attachmentPart.setDataHandler(new DataHandler(source));
attachmentPart.setFileName(new File(attachmentPath).getName());
multipart.addBodyPart(attachmentPart);
}
boolean isSent = false;
if(StringUtils.isEmpty(toEmail)){
log.error("收件邮箱为空,推送失败");
return false;
}
while (retryCount.intValue() < MAX_RETRY && !isSent){
try {
// 创建邮件消息
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(toEmail));
message.setSubject(subject);
// 创建消息体部分(正文)
MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText(body);
// 创建多部分消息
Multipart multipart = new MimeMultipart();
// 添加文本部分
multipart.addBodyPart(messageBodyPart);
// 添加附件部分
if (attachmentPath != null && !attachmentPath.isEmpty()) {
MimeBodyPart attachmentPart = new MimeBodyPart();
DataSource source = new FileDataSource(attachmentPath);
attachmentPart.setDataHandler(new DataHandler(source));
attachmentPart.setFileName(new File(attachmentPath).getName());
multipart.addBodyPart(attachmentPart);
}
// 设置完整消息内容
message.setContent(multipart);
// 发送邮件
Transport.send(message);
log.error("邮件已成功发送: recordFileId->{}", recordFileId);
isSent = true;
} catch (MessagingException e) {
//todo 邮件失败记录
// 异常处理
retryCount.getAndIncrement();
if (retryCount.intValue() > MAX_RETRY) {
log.error("邮件发送达到最大重试次数: recordFileId->{}", recordFileId);
throw new RuntimeException(e);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("邮件发送重试失败", ie);
}
return false;
// 设置完整消息内容
message.setContent(multipart);
// 发送邮件
Transport.send(message);
log.error("邮件已成功发送: meetingId->{}", meetingId);
isSent = true;
} catch (MessagingException e) {
//todo 邮件失败记录
// 异常处理
retryCount.getAndIncrement();
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 true;
}
return isSent;
}
}
\ No newline at end of file
package com.cmeeting.job;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.FileProcessProducer;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.service.WeComService;
import com.cmeeting.vo.TencentMeetingVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
public class CmeetingJob {
private static final Logger logger = LoggerFactory.getLogger(CmeetingJob.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Set<String> processedMeetingIds = Collections.synchronizedSet(new HashSet<>());
@Autowired
private WeComService weComService;
@Autowired
private TecentMeetingService tecentMeetingService;
@Value(value = "${tencent.appId}")
private String tencentAppId;
@Value(value = "${tencent.sdkId}")
private String tencentSdkId;
@Value(value = "${tencent.secretId}")
private String tencentSecretId;
@Value(value = "${tencent.secretKey}")
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Autowired
private MeetingInfoService meetingInfoService;
@Autowired
private UserIdMapper userIdMapper;
@Autowired
private FileProcessProducer producer;
/**
* 企微人员定时同步
*/
// @Scheduled(fixedRate = 5 * 60 * 1000)
@Scheduled(cron = "0 30 6 * * ?")
public void weComUserSync() {
try {
logger.info("-------企微人员定时同步任务开始-------");
logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
log.info("-------企微人员定时同步任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
weComService.doUsers();
logger.info("-------企微人员定时同步任务结束--------");
log.info("-------企微人员定时同步任务结束--------");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 企微人员定时同步
* 腾讯会议人员定时同步
*/
@Scheduled(cron = "0 0 7 * * ?")
// @Scheduled(fixedRate = 5 * 60 * 1000)
public void TencentUserSync() {
try {
logger.info("-------腾讯会议人员定时同步任务开始-------");
logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
log.info("-------腾讯会议人员定时同步任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
tecentMeetingService.doUsers();
logger.info("-------腾讯会议人员定时同步任务结束--------");
log.info("-------腾讯会议人员定时同步任务结束--------");
} catch (Exception e) {
e.printStackTrace();
}
}
@Scheduled(fixedRate = 5 * 60 * 1000)
/**
* 绑定企微和腾会人员关系
*/
// @Scheduled(fixedRate = 5 * 60 * 1000)
public void userBind() {
String weComToken = weComService.getToken();
//查出没有建立关联的企微人员
List<WeComUser> noBindUsers = weComService.noBindUsers();
List<UserId> userIds = new ArrayList<>();
// List<WeComUser> sameNameUsers = noBindUsers.stream().filter(item -> "1".equals(item.getIsRepeatName())).collect(Collectors.toList());
// for (WeComUser user : sameNameUsers) {
// Map<String, String> meetingCodeAndMeetingid = weComService.createTempMeeting(user.getUserId(), weComToken);
// String meetingId = meetingCodeAndMeetingid.get("meetingid");
// String meetingCode = meetingCodeAndMeetingid.get("meeting_code");
// if (meetingCode == null || meetingCode.isEmpty()) {
// System.err.println("会议创建失败,跳过用户: " + user.getUserId());
// continue;
// }
// // 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid)
// String hostUserId = tecentMeetingService.getMeetingHost(meetingCode);
// if (hostUserId == null || hostUserId.isEmpty()) {
// System.err.println("获取主持人失败,跳过会议: " + meetingCode);
// weComService.cancelMeeting(meetingId, weComToken); // 尝试取消无效会议
// continue;
// }
// UserId userId = new UserId(user.getUserName(), user.getUserId(), hostUserId);
// userIds.add(userId);
// }
// List<WeComUser> normalUsers = noBindUsers.stream().filter(item -> "0".equals(item.getIsRepeatName())).collect(Collectors.toList());
for (WeComUser user : noBindUsers) {
Map<String, String> meetingCodeAndMeetingid = weComService.createTempMeeting(user.getUserId(), weComToken);
String meetingId = meetingCodeAndMeetingid.get("meetingid");
String meetingCode = meetingCodeAndMeetingid.get("meeting_code");
if (meetingCode == null || meetingCode.isEmpty()) {
System.err.println("会议创建失败,跳过用户: " + user.getUserId());
continue;
}
// 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid)
String hostUserId = tecentMeetingService.getMeetingHost(meetingCode);
if (hostUserId == null || hostUserId.isEmpty()) {
System.err.println("获取主持人失败,跳过会议: " + meetingCode);
weComService.cancelMeeting(meetingId, weComToken); // 尝试取消无效会议
continue;
}
UserId userId = UserId.builder().userName(user.getUserName()).wid(user.getUserId()).tid(hostUserId).build();
userIds.add(userId);
}
userIdMapper.insertUsers(userIds);
}
@Scheduled(fixedRate = 5 * 60 * 1000,initialDelay = 2 * 60 * 1000)
// @Scheduled(fixedRate = 5 * 60 * 1000)
public void execute() {
// 定义时间格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
......@@ -86,19 +137,101 @@ public class CmeetingJob {
Long nowTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond();
Long beforeDayTimestamp = beforeDay.atZone(ZoneId.systemDefault()).toEpochSecond();
//日志记录
logger.info("起始时间: " + beforeDay.format(formatter) + " | Unix 时间戳: " + beforeDayTimestamp);
logger.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp);
logger.info("----------------------------------");
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles();
log.info("起始时间: " + beforeDay.format(formatter) + " | Unix 时间戳: " + beforeDayTimestamp);
log.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp);
log.info("----------------------------------");
List<UserDTO> accessUserIds = tecentMeetingService.getAccessUserIds();
if (CollectionUtils.isEmpty(accessUserIds)) {
log.info("无生成纪要权限的人员");
return;
}
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles(accessUserIds);
if (meetingFiles == null || meetingFiles.isEmpty()) {
logger.info("没有录制文件需要处理");
log.info("没有录制文件需要处理");
return;
}
// 提交处理任务
producer.submitBatchTasks(meetingFiles, "/");
producer.submitBatchTasks(meetingFiles, "/save/",Boolean.FALSE);
}
/**
* 定时扫描早于一小时之前的,所有未重试过的会议,重新生成纪要
*/
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 10 * 60 * 1000)
public void meetingMinutesRetry() {
try {
log.info("-------生成纪要重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
//查出所有早于一小时前的,生成失败且未重试过的会议
List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated,Boolean.FALSE)
.eq(MeetingInfo::getGenerateRetry,Boolean.FALSE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
log.info("没有生成失败的会议需要重试");
return;
}
List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>();
for (MeetingInfo meetingInfo : meetingInfoList) {
TencentMeetingVO.RecordFile recordFile = TencentMeetingVO.RecordFile.builder()
.meetingId(meetingInfo.getMeetingId())
.subMeetingId(meetingInfo.getSubMeetingId())
.recordFileIdList(Arrays.asList(meetingInfo.getRecordFileId().split(","))).build();
meetingFiles.add(recordFile);
}
// List<UserDTO> accessUserIds = tecentMeetingService.getAccessUserIds();
// 提交处理任务
producer.submitBatchTasks(meetingFiles, "/save/", Boolean.TRUE);
log.info("-------生成纪要重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 定时扫描早于一小时之前的,所有邮件推送未重试过的会议,重新推送邮件
*/
@Scheduled(fixedRate = 30 * 60 * 1000,initialDelay = 15 * 60 * 1000)
public void emailPushRetry() {
try {
log.info("-------邮件推送重试定时任务开始-------");
log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
//查出所有早于一小时前的,邮件推送失败且未重试过的会议
List<MeetingInfo> meetingInfoList =
meetingInfoService.list(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getIsGenerated,Boolean.TRUE)
.eq(MeetingInfo::getEmailPushAccess,Boolean.TRUE)
.le(MeetingInfo::getSyncTime,LocalDateTime.now().minusHours(1))
);
if (meetingInfoList == null || meetingInfoList.isEmpty()) {
log.info("没有推送失败的邮件需要重试");
return;
}
List<TencentMeetingVO.RecordFile> meetingFiles = new ArrayList<>();
for (MeetingInfo meetingInfo : meetingInfoList) {
TencentMeetingVO.RecordFile recordFile = TencentMeetingVO.RecordFile.builder()
.meetingId(meetingInfo.getMeetingId())
.subMeetingId(meetingInfo.getSubMeetingId()).build();
meetingFiles.add(recordFile);
}
// 提交处理任务
producer.submitEmailPushTasks(meetingFiles, "/save/");
log.info("-------邮件推送重试定时任务结束--------");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.cmeeting.job;
import cn.chatbot.openai.completion.chat.ChatCompletionRequest;
import cn.chatbot.openai.completion.chat.ChatMessage;
import cn.chatbot.openai.completion.chat.ChatMessageRole;
import cn.chatbot.openai.completion.chat.Message;
import cn.chatbot.openai.service.LLMService;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.util.MinioUtils;
import com.deepoove.poi.XWPFTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdParticipantsGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.V1AddressesRecordFileIdGet200Response;
import com.tencentcloudapi.wemeet.service.records.model.V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.stereotype.Service;
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.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@Slf4j
@Service
public class EmailPushTask {
private String meetingId;
private String subMeetingId;
private String savePath;
private Map<String, Object> metadata;
private static final int MAX_RETRY = 3;
private MeetingInfoMapper meetingInfoMapper;
private MinioUtils minioUtils;
private EmailSender emailSender;
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
// 实际处理逻辑
public void process() {
Boolean isSuccess = Boolean.FALSE;
AtomicInteger retryCount = new AtomicInteger(0);
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId));
while (retryCount.intValue() <= MAX_RETRY && !isSuccess) {
try {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss"));
String targetFileName;
String meetingName;
String recordXmlPath = meetingId + "-recordXmlPath-" + IdUtil.fastSimpleUUID() + "xml";
String xml;
try(InputStream is = minioUtils.getFile(recordXmlPath)){
xml = convertInputStreamToString(is);
}catch (Exception e){
log.error(e.getMessage());
continue;
}
//将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml);
//追加参会人员信息
Map<String,Object> participantsMap = new ConcurrentHashMap<>();
participantsMap.put("meeting_date",meetingInfo.getStartTime().toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
participantsMap.put("meeting_location","线上腾讯会议");
participantsMap.put("meeting_participants", meetingInfo.getParticipantUsers());
participantsMap.put("meeting_host",meetingInfo.getHost());
dataModel.putAll(participantsMap);
String path = Thread.currentThread().getContextClassLoader().getResource("").getPath();
XWPFTemplate template = XWPFTemplate.compile(path + "template/data_net_template.docx").render(dataModel);
meetingName = dataModel.get("meeting_name") != null ? String.valueOf(dataModel.get("meeting_name")) : "腾讯会议纪要";
targetFileName = meetingName + "_" + nowTime;
template.writeAndClose(new FileOutputStream(savePath + targetFileName + ".docx"));
byte[] recordXmlData = Files.readAllBytes(Paths.get(savePath + targetFileName + ".docx"));
minioUtils.upload(recordXmlPath,recordXmlData);
//邮件推送
isSuccess = emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,savePath + targetFileName + ".docx",meetingId);
} catch (Exception e) {
// 异常处理
int currentRetryCount = retryCount.getAndIncrement();
if (currentRetryCount > MAX_RETRY) {
log.error("达到最大重试次数:meetingId {}", meetingId);
}else{
// 指数退避
try {
Thread.sleep((long) Math.pow(2, currentRetryCount) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.error("重试失败: {}", ie.getMessage());
}
}
}
}
meetingInfoMapper.update(meetingInfo,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getIsPushed,isSuccess)
.set(MeetingInfo::getPushRetry,Boolean.TRUE)
);
}
public static Map<String, Object> convertXmlToMap(String xml) throws Exception {
XmlMapper xmlMapper = new XmlMapper();
ObjectMapper objectMapper = new ObjectMapper();
// 先将 XML 读取为 Map
Map<?, ?> xmlMap = xmlMapper.readValue(xml, Map.class);
// 转换为更标准的 Map<String, Object>
Map<String,Object> map = objectMapper.convertValue(xmlMap, Map.class);
//特殊处理map格式
for (Map.Entry<String, Object> entry : map.entrySet()) {
Map<String,Object> value = (Map<String, Object>) entry.getValue();
Object realValue = value.get("");
entry.setValue(realValue);
}
return map;
}
public static String convertInputStreamToString(InputStream inputStream) throws IOException {
StringBuilder result = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
// 如果需要保留换行符,可添加:result.append("\n");
}
}
return result.toString();
}
public EmailPushTask(String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender, MeetingRecordTemplateMapper meetingRecordTemplateMapper) {
this.savePath = savePath;
this.metadata = metadata;
this.meetingId = meetingId;
this.subMeetingId = subMeetingId;
this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils;
this.emailSender = emailSender;
this.meetingRecordTemplateMapper = meetingRecordTemplateMapper;
}
}
\ No newline at end of file
......@@ -10,9 +10,13 @@ import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.pojo.UserId;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.util.MinioUtils;
import com.deepoove.poi.XWPFTemplate;
......@@ -37,11 +41,13 @@ import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
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;
......@@ -59,7 +65,7 @@ import java.util.stream.Collectors;
@Slf4j
@Service
public class FileProcessTask {
private String recordFileId;
private List<String> recordFileIdList;
private String meetingId;
private String subMeetingId;
private String savePath;
......@@ -73,136 +79,113 @@ public class FileProcessTask {
private String tencentSecretId;
private String tencentSecretKey;
private String tencentAdminUserId;
private String llmApiAddr;
private Boolean finalRetry; //表示是兜底重试机制
private MeetingInfoMapper meetingInfoMapper;
private MinioUtils minioUtils;
private EmailSender emailSender;
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
// 实际处理逻辑
public void process() {
boolean isSuccess = false;
while (retryCount <= MAX_RETRY && !isSuccess) {
try {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
// 获取参会成员明细
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest participantsRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest
.Builder(meetingId).subMeetingId(subMeetingId).operatorId(tencentAdminUserId).operatorIdType("1").build();
AuthenticatorBuilder<JWTAuthenticator> participantsAuthenticatorBuilder =
new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
Map<String,Object> participantsMap = new ConcurrentHashMap<>();
//主持人角色,以下角色都可以表示主持人
//用户角色:
//0:普通成员角色
//1:创建者角色
//2:主持人
//3:创建者+主持人
//4:游客
//5:游客+主持人
//6:联席主持人
//7:创建者+联席主持人
List<Long> hostRoleList = Arrays.asList(2L,3L,5L,6L,7L);
try {
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse =
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
V1MeetingsMeetingIdParticipantsGet200Response data = participantsResponse.getData();
String scheduleStartTime = data.getScheduleStartTime();
String scheduleEndTime = data.getScheduleEndTime();
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String zonedDateTimeStart = Instant.ofEpochSecond(Long.valueOf(scheduleStartTime)).atZone(zoneId).format(df);
// String zonedDateTimeEnd = Instant.ofEpochSecond(Long.valueOf(scheduleEndTime)).atZone(zoneId).format(df);
participantsMap.put("meeting_date",zonedDateTimeStart);
participantsMap.put("meeting_location","线上腾讯会议");
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = data.getParticipants();
participantsMap.put("meeting_participants",
participants.stream()
.map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、")));
Optional<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> host = participants.stream().filter(item -> hostRoleList.contains(item.getUserRole())).findFirst();
participantsMap.put("meeting_host",host.isPresent() ? new String(Base64.getDecoder().decode(host.get().getUserName())) : "未知主持人");
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
throw new RuntimeException(e);
}
//查询录制转写详情
RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse =
client.records().v1AddressesRecordFileIdGet(addressRequest,
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
// 处理响应
if (addressResponse != null && addressResponse.getData() != null) {
log.info("Successfully got address for record file {}", recordFileId);
V1AddressesRecordFileIdGet200Response addressData = addressResponse.getData();
// 获取AI会议转录文件
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
addressData.getAiMeetingTranscripts();
if (transcripts != null && !transcripts.isEmpty()) {
log.info("Found {} AI meeting transcripts for record file {}",
transcripts.size(), recordFileId);
// 处理每个转录文件
for (V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner transcript : transcripts) {
String fileType = transcript.getFileType();
String downloadUrl = transcript.getDownloadAddress();
if ("txt".equalsIgnoreCase(fileType)) {
log.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
// 1. 下载文件
byte[] fileData = downloadFile(downloadUrl);
// 2. 将二进制文件转换为文本
String recordTextContent = new String(fileData);
// String recordTextContent = new String(Files.readAllBytes(Paths.get("/20250321145249-天达物流AIGC维修助手需要沟通-转写原文版-1.txt")));
if(StringUtils.isEmpty(recordTextContent.replaceAll("\\n","").trim())){
log.info("获取的转录文本为空,跳过纪要生成,URL:{},fileRecordId:{}", fileType, downloadUrl,recordFileId);
continue;
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId));
//每场会议可能会分段录制,查出每个文件的转录记录后拼接
StringBuffer recordTextBuffer = new StringBuffer();
for (String recordFileId : recordFileIdList) {
//查询录制转写详情
RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse =
client.records().v1AddressesRecordFileIdGet(addressRequest,
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
// 处理响应
if (addressResponse != null && addressResponse.getData() != null) {
log.info("Successfully got address for record file {}", recordFileId);
V1AddressesRecordFileIdGet200Response addressData = addressResponse.getData();
// 获取AI会议转录文件
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
addressData.getAiMeetingTranscripts();
if (transcripts != null && !transcripts.isEmpty()) {
log.info("Found {} AI meeting transcripts for record file {}",
transcripts.size(), recordFileId);
// 处理每个转录文件
for (V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner transcript : transcripts) {
String fileType = transcript.getFileType();
String downloadUrl = transcript.getDownloadAddress();
if ("txt".equalsIgnoreCase(fileType)) {
log.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
// 1. 下载文件
byte[] fileData = downloadFile(downloadUrl);
// 2. 将二进制文件转换为文本
String recordTextContent = new String(fileData);
// byte[] fileData = Files.readAllBytes(Paths.get("/20250514132555-中集AIGC项目例会-转写智能优化版.txt"));
// String recordTextContent = new String(fileData);
if(StringUtils.isNotEmpty(recordTextContent.replaceAll("\\n","").trim())){
recordTextBuffer.append("\n\n");
recordTextBuffer.append(recordTextContent);
}
}
}
// 3. 处理文件 (调用Claude API等)
String processedResult = processWithClaude(recordTextContent);
// 4. 保存结果
saveResult(savePath, processedResult, participantsMap, fileData);
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
if(StringUtils.isEmpty(recordTextBuffer.toString().replaceAll("\\n","").trim())){
log.info("获取的转录文本为空,跳过纪要生成,meetingId:{},fileRecordId:{}",meetingId,recordFileIdList.toString());
throw new RuntimeException("获取的转录文本为空,跳过纪要生成");
}
// 3. 处理文件 (调用Claude API等)
String processedResult = processWithClaude(recordTextBuffer.toString());
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
isSuccess = true;
// 4. 保存结果
saveResult(savePath, processedResult, recordTextBuffer.toString().getBytes(StandardCharsets.UTF_8),meetingInfo);
isSuccess = true;
} catch (Exception e) {
// 异常处理
retryCount++;
if (retryCount > MAX_RETRY) {
log.error("达到最大重试次数: {}", recordFileId);
throw new RuntimeException(e);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试失败", ie);
log.error("达到最大重试次数:meetingId {}", meetingId);
//如果是兜底重试,最终还是失败了,设置会议的重试状态为已重试
if(finalRetry){
meetingInfoMapper.update(null,
new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getGenerateRetry,Boolean.TRUE));
}
}else{
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试失败", ie);
}
}
}
}
......@@ -248,12 +231,13 @@ public class FileProcessTask {
private String processWithClaude(String textContent) {
//将文件传送给大模型处理
String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO";
String apiAddr = "https://bedrock.chatbot.cn/llm/sse-invoke";
String apiAddr = llmApiAddr + "/llm/sse-invoke";
String model = "anthropic.claude-3-5-sonnet-20240620-v1:0";
int maxTokens = 5000;
//获取系统模板
MeetingRecordTemplate meetingRecordTemplate = meetingRecordTemplateMapper.selectById(1);
List<Message> messages = new ArrayList<>();
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "你是会议纪要助手,基于提供的会议记录,生成一份详细的会议纪要,并严格按照指定的XML格式输出。\\n\\n要求:\\n1. 会议名称:应简洁明了地反映会议主题,不超过30个字\\n2. 会议目的:需清晰阐述召开此次会议的原因和期望达成的目标,不超过50字\\n3. 会议要点:需全面概括会议内容,让读者理解会议讨论的核心问题,不要遗漏任何有价值的信息,每个要点需用序号标注并另起一行\\n4. 会议内容:需详细记录会议的背景信息、主要讨论内容、各参会人员的发言要点、达成的共识和决策等。内容应有逻辑性,分段清晰,重点突出,不可遗漏关键信息\\n5. 会议沟通内容:需按照一定的顺序组织内容,详细描述每个事项的内容,不要使用流水账形式,应包含具体的沟通过程、各方观点和达成的一致意见\\n6. 会后跟踪事项:详细描述会议结束之后,每个后续事项的责任人、事项内容、完成时间(若会议记录中没有提及,则填写待定)等信息,事项要具体到责任人\\n\\n注意事项:\\n1. 所有内容需基于原始会议记录,不要添加虚构信息\\n2. 会议纪要内容需详细充分,建议总字数不少于3000字,实际字数可根据原始会议记录的丰富程度进行适当调整\\n\\n输出格式必须严格按照以下XML结构,不允许有任何嵌套标签:\\n\\n<meeting_name label=\\\"会议名称\\\">会议名称</meeting_name>\\n<meeting_purpose label=\\\"会议目的\\\">会议目的</meeting_purpose>\\n<meeting_key_points label=\\\"会议要点\\\">会议要点</meeting_key_points>\\n<meeting_content label=\\\"会议内容\\\">会议内容</meeting_content>\\n<meeting_communication label=\\\"会议沟通内容\\\">会议沟通内容</meeting_communication>\\n<meeting_follow_up label=\\\"会后跟踪事项\\\">\\n在此填写会后跟踪事项,格式为:\\n1. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n2. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n...\\n</meeting_follow_up>\\n\\n完成XML格式的会议纪要后,必须在XML之外单独添加猜您想问部分,生成3-4条与会议内容相关的推荐问题。\\n\\n会议记录如下:");
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), meetingRecordTemplate.getPrompt());
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议记录");
messages.add(chatMessage);
......@@ -266,7 +250,7 @@ public class FileProcessTask {
return ret;
}
private void saveResult(String path, String content, Map<String,Object> participantsMap, byte[] recordData) {
private void saveResult(String path, String content, byte[] recordData, MeetingInfo meetingInfo) {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss"));
String targetPath = path + nowTime + ".json"; // 改为.json扩展名;
String targetFileName;
......@@ -284,36 +268,51 @@ public class FileProcessTask {
log.info("json文件已保存到: {}", targetPath);
//将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml);
//追加参会人员信息
Map<String,Object> participantsMap = new ConcurrentHashMap<>();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String meetingDate = meetingInfo.getStartTime().toLocalDate().format(df);
participantsMap.put("meeting_date",meetingDate);
participantsMap.put("meeting_location","线上腾讯会议");
participantsMap.put("meeting_participants", meetingInfo.getParticipantUsers());
participantsMap.put("meeting_host",meetingInfo.getHost());
dataModel.putAll(participantsMap);
XWPFTemplate template = XWPFTemplate.compile("/data_net_template.docx").render(dataModel);
ClassPathResource resource = new ClassPathResource("template/data_net_template.docx");
Properties properties = new Properties();
meetingName = dataModel.get("meeting_name") != null ? String.valueOf(dataModel.get("meeting_name")) : "腾讯会议纪要";
targetFileName = meetingName + "_" + nowTime;
template.writeAndClose(new FileOutputStream(path + targetFileName + ".docx"));
byte[] recordXmlData = Files.readAllBytes(Paths.get(path + targetFileName + ".docx"));
minioUtils.upload(recordXmlPath,recordXmlData);
try (InputStream inputStream = resource.getInputStream()) {
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(dataModel);
template.writeAndClose(new FileOutputStream(path + targetFileName + ".docx"));
byte[] recordXmlData = Files.readAllBytes(Paths.get(path + targetFileName + ".docx"));
minioUtils.upload(recordXmlPath,recordXmlData);
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
log.error("填充会议纪要失败: {}", e.getMessage(), e);
throw new RuntimeException("填充会议纪要失败");
}
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>().eq(MeetingInfo::getMeetingId,meetingId));
Boolean isPushed;
try {
log.info("开始邮件推送------");
if(meetingInfo.getEmailPushAccess()){
log.info("用户允许邮件推送,准备推送邮件至{}------",meetingInfo.getEmail());
//邮件推送
emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
// emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
// emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",recordFileId);
isPushed = Boolean.TRUE;
} catch (Exception e) {
isPushed = emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",meetingId);
// emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",meetingId);
// emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",meetingId);
}else{
log.info("用户关闭了邮件推送,推送终止------");
isPushed = Boolean.FALSE;
e.printStackTrace();
log.error(e.getMessage());
}
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::getIsGenerated,Boolean.TRUE)
......@@ -420,10 +419,11 @@ public class FileProcessTask {
}
public FileProcessTask(String recordFileId, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, String tencentAppId,
public FileProcessTask(List<String> recordFileIdList, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, String tencentAppId,
String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender) {
this.recordFileId = recordFileId;
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender, MeetingRecordTemplateMapper meetingRecordTemplateMapper,
String llmApiAddr, Boolean finalRetry) {
this.recordFileIdList = recordFileIdList;
this.savePath = savePath;
this.metadata = metadata;
this.tencentAppId = tencentAppId;
......@@ -436,5 +436,8 @@ public class FileProcessTask {
this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils;
this.emailSender = emailSender;
this.meetingRecordTemplateMapper = meetingRecordTemplateMapper;
this.llmApiAddr = llmApiAddr;
this.finalRetry = finalRetry;
}
}
\ No newline at end of file
......@@ -8,5 +8,6 @@ import java.util.List;
@Mapper
public interface AuthMapper {
List<CoreModulePermissions> getAuthByTargrtId(@Param("targetId") String targetId, @Param("tenantId") String tenantId);
List<CoreModulePermissions> getAuthByTargetId(@Param("targetId") String targetId, @Param("tenantId") String tenantId);
}
package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.MeetingRecordTemplate;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MeetingRecordTemplateMapper extends BaseMapper<MeetingRecordTemplate> {
}
\ No newline at end of file
package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.UserId;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserIdMapper {
public interface UserIdMapper extends BaseMapper<UserId> {
void insertUsers (List<UserId> userIds);
List<UserId> getUsers();
......
......@@ -7,7 +7,7 @@ import java.util.List;
public interface WeComUserMapper extends BaseMapper<WeComUser> {
List<WeComUser> getSameName() ;
List<WeComUser> noBindUsers() ;
WeComUser selectById(Integer id);
int insert(WeComUser user);
......
package com.cmeeting.mapper.secondary;
import com.cmeeting.dto.UserDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface SysUserSysMapper {
String getCompanyEmail(String wid, String tenantId);
String getParentDeptId(String deptId, String tenantId);
List<String> getSubDeptId(@Param(value = "deptId")String deptId,@Param(value = "tenantId") String tenantId);
List<String> getUsersByDept(@Param(value = "deptIds") List<String> deptIds,@Param(value = "tenantId") String tenantId);
List<UserDTO> getUserEmail(@Param(value = "tenantId")String tenantId);
}
......@@ -20,6 +20,8 @@ import java.time.LocalDateTime;
@Accessors(chain = true)
@TableName("cmt_meeting_info")
public class MeetingInfo implements Serializable {
private static final long serialVersionUID = -26238487532381000L;
/**
* 主键id
*/
......@@ -48,12 +50,17 @@ public class MeetingInfo implements Serializable {
/**
* 主持人
*/
private String hostId;
private String host;
/**
* 主持人uid
*/
private String hostUid;
/**
* 参会人员名单
*/
private String participantUserIds;
private String participantUsers;
/**
* 会议开始时间(时间戳)
......@@ -78,6 +85,14 @@ public class MeetingInfo implements Serializable {
*/
private Boolean isPushed;
/**
* 会议纪要重新生成标识
*/
private Boolean generateRetry;
/**
* 邮件推送重试标识
*/
private Boolean pushRetry;
/**
* 同步时间
*/
private LocalDateTime syncTime;
......@@ -91,4 +106,12 @@ public class MeetingInfo implements Serializable {
* 纪要xml
*/
private String recordXml;
/**
* 每个会议对应的转录文件id,多个用逗号分隔,按参会前后升序
*/
private String recordFileId;
/**
* 邮箱
*/
private String email;
}
\ No newline at end of file
package com.cmeeting.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 会议纪要模板实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("cmt_meeting_record_template")
public class MeetingRecordTemplate implements Serializable {
private static final long serialVersionUID = -26238487532381000L;
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 模板名称
*/
private String name;
/**
* 模板类型(系统模板/自定义模板)
*/
private String type;
/**
* 会议类型
*/
private String meetingType;
/**
* 会议类型描述
*/
private String typeDetail;
/**
* 提示词内容
*/
private String prompt;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 创建时间
*/
private LocalDateTime updateTime;
/**
* 创建人
*/
private String createUser;
/**
* 更新人
*/
private String updateUser;
}
\ No newline at end of file
package com.cmeeting.pojo;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import lombok.experimental.Accessors;
@Data
import java.io.Serializable;
public class TencentMeetingUser {
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user_tencentmeeting")
public class TencentMeetingUser implements Serializable {
private static final long serialVersionUID = -26238487532381000L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
......@@ -25,26 +37,4 @@ public class TencentMeetingUser {
* 是否是重名用户(1:重名, 0:不重名)
*/
private String isrepeatName;
// 无参构造方法
public TencentMeetingUser() {
}
// 全参构造方法
public TencentMeetingUser(Integer id, String userName, String userId, String isrepeatName) {
this.id = id;
this.userName = userName;
this.userId = userId;
this.isrepeatName = isrepeatName;
}
@Override
public String toString() {
return "TencentMeetingUser{" +
"id=" + id +
", userName='" + userName + '\'' +
", userId='" + userId + '\'' +
", isrepeatName='" + isrepeatName + '\'' +
'}';
}
}
package com.cmeeting.pojo;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("userid")
public class UserId {
/**
* 主键ID
......@@ -24,23 +32,4 @@ public class UserId {
*/
private String tid;
// 无参构造方法
public UserId() {
}
// 全参构造方法
public UserId(Integer id, String userName, String wid, String tid) {
this.id = id;
this.userName = userName;
this.wid = wid;
this.tid = tid;
}
// 全参构造方法
public UserId( String userName, String wid, String tid) {
this.userName = userName;
this.wid = wid;
this.tid = tid;
}
}
package com.cmeeting.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
public class WeComUser extends Model implements Serializable {
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user_wecom")
public class WeComUser implements Serializable {
private static final long serialVersionUID = -26238487532381000L;
@TableId(type = IdType.AUTO)
private Integer id;
private String userName;
private String userId;
private String isRepeatName;
private String email;
public WeComUser() {
}
public WeComUser(Integer id, String userName, String userId, String isRepeatName) {
this.id = id;
this.userName = userName;
this.userId = userId;
this.isRepeatName = isRepeatName;
}
@Override
public String toString() {
return "WeComUser{" +
"id=" + id +
", userName='" + userName + '\'' +
", userId='" + userId + '\'' +
", isRepeatName='" + isRepeatName + '\'' +
", email='" + email + '\'' +
'}';
}
}
......@@ -11,7 +11,7 @@ public class FileProcessCallbackHandler {
// 单个任务完成回调
public void onComplete(FileProcessTask task) {
// 可以记录任务状态、发送通知等
log.info("任务处理完成: {}", task.getRecordFileId());
log.info("任务处理完成: meetingId {}", task.getMeetingId());
// 更新数据库状态等
// taskRepository.updateStatus(task.getId(), "COMPLETED");
......
package com.cmeeting.service;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.email.EmailSender;
import com.cmeeting.job.EmailPushTask;
import com.cmeeting.job.FileProcessTask;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.UserId;
import com.cmeeting.util.MinioUtils;
import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j;
......@@ -37,24 +41,33 @@ public class FileProcessProducer {
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Value(value = "${llm.api-addr}")
private String llmApiAddr;
@Resource
private MeetingInfoMapper meetingInfoMapper;
@Resource
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
@Resource
private MinioUtils minioUtils;
@Resource
private EmailSender emailSender;
// 批量提交任务
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, String baseSavePath) {
/**
* 批量提交生成纪要任务
* @param recordFiles 转录文件信息
* @param baseSavePath 存储临时路径
* @param finalRetry
*/
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, String baseSavePath, Boolean finalRetry) {
List<Future<?>> futures = new ArrayList<>();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务
FileProcessTask task = new FileProcessTask(
recordFile.getRecordFileId(),
recordFile.getRecordFileIdList(),
recordFile.getMeetingId(),
recordFile.getSubMeetingId(),
"/save/",
baseSavePath,
Collections.emptyMap(),
tencentAppId,
tencentSdkId,
......@@ -63,7 +76,10 @@ public class FileProcessProducer {
tencentAdminUserId,
meetingInfoMapper,
minioUtils,
emailSender
emailSender,
meetingRecordTemplateMapper,
llmApiAddr,
finalRetry
);
// 提交任务到线程池
......@@ -78,6 +94,35 @@ public class FileProcessProducer {
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures);
}
// 批量提交邮箱推送重试任务
public void submitEmailPushTasks(List<TencentMeetingVO.RecordFile> recordFiles, String baseSavePath) {
List<Future<?>> futures = new ArrayList<>();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务
EmailPushTask task = new EmailPushTask(
recordFile.getMeetingId(),
recordFile.getSubMeetingId(),
baseSavePath,
Collections.emptyMap(),
meetingInfoMapper,
minioUtils,
emailSender,
meetingRecordTemplateMapper
);
// 提交任务到线程池
Future<?> future = fileProcessExecutor.submit(() -> {
task.process();
});
futures.add(future);
}
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures);
}
private void monitorTaskCompletion(List<Future<?>> futures) {
new Thread(() -> {
......
package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.MeetingRecordTemplate;
public interface MeetingRecordTemplateService extends IService<MeetingRecordTemplate> {
}
package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.UserId;
import com.cmeeting.vo.TencentMeetingVO;
import java.util.List;
......@@ -11,6 +13,10 @@ public interface TecentMeetingService extends IService<TencentMeetingUser> {
void doUsers();
List<TencentMeetingVO.RecordFile> getMeetingFiles();
List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds);
List<UserDTO> getAccessUserIds();
String getMeetingHost(String meetingCode);
}
......@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.WeComUser;
import java.util.List;
import java.util.Map;
public interface WeComService extends IService<WeComUser> {
......@@ -12,4 +13,10 @@ public interface WeComService extends IService<WeComUser> {
void doUsers() throws Exception;
String getToken();
List<WeComUser> noBindUsers();
Map<String, String> createTempMeeting(String wid, String token);
void cancelMeeting(String meetingId, String weComToken);
}
package com.cmeeting.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.service.MeetingRecordTemplateService;
import org.springframework.stereotype.Service;
@Service
public class MeetingRecordTemplateServiceImpl extends ServiceImpl<MeetingRecordTemplateMapper, MeetingRecordTemplate> implements MeetingRecordTemplateService {
}
\ No newline at end of file
package com.cmeeting.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.mapper.primary.AuthMapper;
import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.TecentMeetingMapper;
import com.cmeeting.mapper.primary.UserIdMapper;
import com.cmeeting.mapper.secondary.SysUserSysMapper;
import com.cmeeting.pojo.CoreModulePermissions;
import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.UserId;
import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.util.RedisUtils;
import com.cmeeting.vo.TencentMeetingVO;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
......@@ -14,44 +20,35 @@ import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.meetings.model.*;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.*;
import com.tencentcloudapi.wemeet.service.user_manager.api.UserManagerApi;
import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200Response;
import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200ResponseUsersInner;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.time.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.fetchUsersInBatches;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNamesTecent;
@Service
@Slf4j
public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TecentMeetingService {
......@@ -59,9 +56,18 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
private TecentMeetingMapper tecentMeetingMapper;
@Resource
private MeetingInfoMapper meetingInfoMapper;
@Resource
private AuthMapper authMapper;
@Resource
private SysUserSysMapper sysUserSyncMapper;
@Resource
private UserIdMapper userIdMapper;
@Resource
private RedisUtils redisUtils;
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private final Object lock = new Object();
@Value(value = "${tencent.appId}")
private String tencentAppId;
......@@ -73,6 +79,10 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Value(value = "${permission.applicationId}")
private String permissionApplicationId;
@Value(value = "${permission.tenantId}")
private String permissionTenantId;
@Override
public void batchInsert(List<TencentMeetingUser> users) {
......@@ -82,26 +92,21 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
@Override
public void doUsers() {
/**
* 腾讯会议通过通讯录获取员工信息
*/
// 1. 构造client客户端
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId, tencentSecretKey)
.build();
// 2.获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches(client, 3);;
// 3. 检查重名并设置标志
// 获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches();;
// 检查重名并设置标志
markDuplicateNamesTecent(users);
// 批量插入数据库(分批次)
int batchSize = 10;
for (int i = 0; i < users.size(); i += batchSize) {
List<TencentMeetingUser> batch = users.subList(i, Math.min(i + batchSize, users.size()));
batchInsert(batch);
}
// 4. 批量插入数据库
batchInsert(users);
}
@Override
public List<TencentMeetingVO.RecordFile> getMeetingFiles() {
public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds) {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
......@@ -112,7 +117,7 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
// 查询近两天的会议录制列表
try {
ZonedDateTime now = ZonedDateTime.now();
String startTime = String.valueOf(now.minusDays(2).toEpochSecond());
String startTime = String.valueOf(now.minusDays(5).toEpochSecond());
String endTime = String.valueOf(now.toEpochSecond());
AtomicInteger currentPage = new AtomicInteger(1);
Long totalPage = 1L;
......@@ -129,7 +134,6 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
.pageSize("20")
.page(String.valueOf(currentPage.getAndIncrement()))
.mediaSetType("0")
.queryRecordType("0")
.build();
RecordsApi.ApiV1RecordsGetResponse response =
client.records().v1RecordsGet(request, new JWTAuthenticator.Builder()
......@@ -149,8 +153,11 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
return null;
}
for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) {
//会议没结束,跳过
if(meeting.getState() != 3) continue;
log.info("【会议检索】转录文件的meetingId->{},recordFileId->{}",meeting.getMeetingId(),meeting.getMeetingRecordId());
//查询会议详情
String meetingId = meeting.getMeetingId();
String subMeetingId = null;
......@@ -175,27 +182,85 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
//1:周期性会议
Long meetingType = meetingInfo.getMeetingType();
if(meetingType.intValue() == 1){
subMeetingId = meetingInfo.getCurrentSubMeetingId();
//如果是周期会议,获取子会议的ID,用于查询参会人员
List<V1MeetingsMeetingIdGet200ResponseMeetingInfoListInnerSubMeetingsInner> subMeetings = meetingInfo.getSubMeetings();
LocalDate meetingStartDate = Instant.ofEpochMilli(meeting.getMediaStartTime()).atZone(ZoneId.systemDefault()).toLocalDate();
Optional<V1MeetingsMeetingIdGet200ResponseMeetingInfoListInnerSubMeetingsInner> subMeeting = subMeetings.stream().filter(item ->
Instant.ofEpochSecond(Long.valueOf(item.getStartTime())).atZone(ZoneId.systemDefault()).toLocalDate().equals(meetingStartDate))
.findFirst();
if(!subMeeting.isPresent()){
System.out.println("周期会议"+meetingId+"未知子会议ID");
continue;
}
subMeetingId = subMeeting.get().getSubMeetingId();
}
//如果数据库中已有相同会议id的记录,跳过同步
if(!meetingIds.contains(meetingId)){
log.info("【会议检索】新的会议meetingId->{},开始持久化",meeting.getMeetingId());
List<V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner> recordFiles = meeting.getRecordFiles();
//按转录文件时间升序,便于后续的内容拼接
List<String> recordFileIdList = recordFiles.stream().sorted(Comparator.comparingLong(V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner::getRecordStartTime))
.map(V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner::getRecordFileId).collect(Collectors.toList());
TencentMeetingVO.RecordFile recordFileItem = TencentMeetingVO.RecordFile.builder()
.recordFileIdList(recordFileIdList).meetingId(meetingId).subMeetingId(subMeetingId).build();
// 获取参会成员明细
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest participantsRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest
.Builder(meetingId).subMeetingId(subMeetingId).operatorId(tencentAdminUserId).operatorIdType("1").build();
AuthenticatorBuilder<JWTAuthenticator> participantsAuthenticatorBuilder =
new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
//主持人角色,以下角色都可以表示主持人
//用户角色:
//0:普通成员角色
//1:创建者角色
//2:主持人
//3:创建者+主持人
//4:游客
//5:游客+主持人
//6:联席主持人
//7:创建者+联席主持人
List<Long> hostRoleList = Arrays.asList(2L,3L,5L,6L,7L);
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse =
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
V1MeetingsMeetingIdParticipantsGet200Response participantsData = participantsResponse.getData();
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = participantsData.getParticipants();
Optional<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> host = participants.stream().filter(item -> hostRoleList.contains(item.getUserRole())).findFirst();
String email;
if(host.isPresent()) {
String tid = host.get().getUserid();
//判断是否有权限生成纪要
boolean generateAccess = accessUserIds.stream().anyMatch(item -> item.getTid().equals(tid));
if(!generateAccess){
log.error("【权限校验】主持人{}没有生成纪要权限,跳过生成",tid);
continue;
}
log.info("【权限校验】主持人{}允许生成纪要",tid);
UserDTO userDTO = accessUserIds.stream().filter(item -> item.getTid().equals(tid)).findFirst().get();
email = userDTO.getEmail();
}else{
log.error("未找到主持人,默认没有生成纪要权限");
continue;
}
//会议基本信息保存
MeetingInfo meetingItem = MeetingInfo.builder().meetingId(meetingId).meetingCode(meetingInfo.getMeetingCode())
.subject(meetingInfo.getSubject())
.startTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getStartTime())), ZoneId.systemDefault()))
.endTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getEndTime())), ZoneId.systemDefault()))
// .hostId(meetingInfo.getHosts().stream().map(V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner::getUserid).collect(Collectors.joining(",")))
// .participantUserIds(meetingInfo.getParticipants().stream().map(V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner::getUserid).collect(Collectors.joining(",")))
.isGenerated(Boolean.FALSE).emailPushAccess(Boolean.TRUE).isPushed(Boolean.FALSE).syncTime(LocalDateTime.now())
.subMeetingId(subMeetingId)
.subMeetingId(subMeetingId).generateRetry(Boolean.FALSE).pushRetry(Boolean.FALSE)
.host(host.isPresent() ? new String(Base64.getDecoder().decode(host.get().getUserName())) : "未知主持人")
.hostUid(host.isPresent() ? host.get().getUserid() : null)
.participantUsers(participants.stream()
.map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、")))
.recordFileId(recordFileIdList.stream().collect(Collectors.joining(",")))
.email(email)
.build();
recordFileUrlList.add(recordFileItem);
meetingSaveList.add(meetingItem);
List<V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner> recordFiles = meeting.getRecordFiles();
for (V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner recordFile : recordFiles) {
TencentMeetingVO.RecordFile recordFileItem = TencentMeetingVO.RecordFile.builder()
.recordFileId(recordFile.getRecordFileId()).meetingId(meetingId).subMeetingId(subMeetingId).build();
recordFileUrlList.add(recordFileItem);
}
}
}
} catch (ClientException e) {
......@@ -207,7 +272,16 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
}
}
if(meetingSaveList.size() > 0){
meetingInfoMapper.batchInsert(meetingSaveList);
Map<String, List<MeetingInfo>> meetingSaveMap = meetingSaveList.stream().collect(Collectors.groupingBy(
item -> item.getMeetingId() + "_" +
(item.getSubMeetingId() != null ? item.getSubMeetingId() : "null")));
List<MeetingInfo> finalSaveList = new ArrayList<>();
for (Map.Entry<String, List<MeetingInfo>> entry : meetingSaveMap.entrySet()) {
MeetingInfo meetingInfo = entry.getValue().get(0);
meetingInfo.setRecordFileId(entry.getValue().stream().map(MeetingInfo::getRecordFileId).collect(Collectors.joining(",")));
finalSaveList.add(meetingInfo);
}
meetingInfoMapper.batchInsert(finalSaveList);
}
} catch (Exception e) {
log.error(e.getMessage());
......@@ -217,6 +291,71 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
}
/**
* 获取有权限的人员
* @return
*/
@Override
public List<UserDTO> getAccessUserIds() {
//权限控制
List<CoreModulePermissions> auths = authMapper.getAuthByTargetId(permissionApplicationId, permissionTenantId);
//授权的部门,type为1的为人员,为0表示部门
List<CoreModulePermissions> authDepts = auths.stream().filter(item -> item.getType().equals(0)).collect(Collectors.toList());
List<String> deptPath = new ArrayList<>();
for (CoreModulePermissions authDept : authDepts) {
String deptId = authDept.getRelId();
String tenantId = authDept.getTenantId();
getDeptPath(deptPath,deptId,tenantId);
}
//已被授权部门下的userid
List<String> accessUserIds = !CollectionUtils.isEmpty(deptPath) ? sysUserSyncMapper.getUsersByDept(deptPath, permissionTenantId) : new ArrayList<>();
//已被直接授权的人员追加进去
accessUserIds.addAll(auths.stream().filter(item -> item.getType().equals(1)).map(CoreModulePermissions::getRelId).collect(Collectors.toList()));
if(!CollectionUtils.isEmpty(accessUserIds)){
//查出人员邮箱
List<UserDTO> userEmailList = sysUserSyncMapper.getUserEmail(permissionTenantId);
Map<String, String> userEmailMap = CollectionUtils.isEmpty(userEmailList) ? new HashMap<>()
: userEmailList.stream().collect(Collectors.toMap(UserDTO::getWid, UserDTO::getEmail));
//查出企微id和腾会id的关联关系
Map<String,String> userIds = userIdMapper.selectList(null).stream().collect(Collectors.toMap(UserId::getWid,UserId::getTid));
List<UserDTO> accessUsers = new ArrayList<>();
for (String accessUserId : accessUserIds) {
if(userIds.containsKey(accessUserId)){
UserDTO accessUser = new UserDTO();
accessUser.setWid(accessUserId);
accessUser.setTid(userIds.get(accessUserId));
if(userEmailMap.containsKey(accessUserId))
accessUser.setEmail(userEmailMap.get(accessUserId));
accessUsers.add(accessUser);
}
}
return accessUsers;
}
return new ArrayList<>();
}
/**
* 获取部门的路径
* @param deptPath
* @param deptId
* @param tenantId
*/
private void getDeptPath(List<String> deptPath, String deptId, String tenantId) {
if(!deptPath.contains(deptId)) deptPath.add(deptId);
List<String> subDeptIds = sysUserSyncMapper.getSubDeptId(deptId, tenantId);
if(CollectionUtils.isEmpty(subDeptIds)) return;
for (String subDeptId : subDeptIds) {
//部门id去重
if(!deptPath.contains(subDeptId)){
deptPath.add(subDeptId);
getDeptPath(deptPath,subDeptId,tenantId);
}
}
}
/**
* 生成请求腾会接口的请求头
* @param httpMethod http请求方法 GET/POST/PUT等
* @param requestUri 请求uri,eg:/v1/meetings
......@@ -285,4 +424,174 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
}
return new String(buf);
}
/**
* 标记企业微信重名用户
*/
public static void markDuplicateNamesTecent(List<TencentMeetingUser> users) {
// 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(TencentMeetingUser::getUserName, Collectors.counting()));
// 设置是否重名标志
users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsrepeatName("1"); // 重名
} else {
user.setIsrepeatName("0"); // 不重名
}
});
}
/**
* 腾讯会议通过通讯录获取员工信息
* @return
*/
private List<TencentMeetingUser> fetchUsersInBatches() {
if(redisUtils.hasKey("TENCENT_USER_ARRAY")){
return (List<TencentMeetingUser>) redisUtils.get("TENCENT_USER_ARRAY");
}
//构造client客户端
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId, tencentSecretKey)
.build();
//先请求一条数据,获取到数据总量
UserManagerApi.ApiV1UsersListGetRequest firstRequest =
new UserManagerApi.ApiV1UsersListGetRequest.Builder()
.page(String.valueOf(1))
.pageSize(String.valueOf(1))
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
UserManagerApi.ApiV1UsersListGetResponse firstResponse;
try {
firstResponse = client.user_manager().v1UsersListGet(firstRequest, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
} catch (Exception e) {
e.printStackTrace();
log.error("获取腾会人员信息失败");
return new ArrayList<>();
}
Long totalUsers = firstResponse.getData().getTotalCount();
// 使用线程安全的集合存储结果
List<TencentMeetingUser> resultList = Collections.synchronizedList(new ArrayList<>());
Integer pageSize = 20;
int maxPage = (totalUsers.intValue() + pageSize - 1) / pageSize;
for (int currentPage = 1; currentPage <= maxPage; currentPage++) {
try {
// 1. 构造请求参数
UserManagerApi.ApiV1UsersListGetRequest request =
new UserManagerApi.ApiV1UsersListGetRequest.Builder()
.page(String.valueOf(currentPage))
.pageSize(String.valueOf(pageSize))
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
// 2. 构造JWT鉴权器
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
// 3. 发送请求
UserManagerApi.ApiV1UsersListGetResponse response =
client.user_manager().v1UsersListGet(request, authenticatorBuilder);
V1UsersListGet200Response responseData = response.getData();
List<V1UsersListGet200ResponseUsersInner> users = responseData.getUsers();
System.out.printf("第 %d 页,获取到 %d 个用户:\n", currentPage, responseData.getCurrentSize());
// 4. 处理响应并转换为TencentMeetingUser对象
for (V1UsersListGet200ResponseUsersInner user : users) {
// 创建TencentMeetingUser对象并设置属性
TencentMeetingUser meetingUser = new TencentMeetingUser();
meetingUser.setUserId(user.getUserid());
meetingUser.setUserName(user.getUsername());
meetingUser.setIsrepeatName("0"); // 默认设为不重名,可根据实际业务调整
resultList.add(meetingUser); // 添加到集合
System.out.printf("用户ID: %s, 用户名: %s\n", user.getUserid(), user.getUsername());
}
} catch (ClientException e) {
System.out.printf("客户端错误: %s\n", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
System.out.printf("服务端错误: %s\n", e);
System.out.printf("完整响应: %s\n", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
}
redisUtils.set("TENCENT_USER_ARRAY",resultList);
// 输出结果
System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", resultList.size());
return resultList;
}
/**
* 通过meetingId获取到会议主持人对应的腾讯会议userid
*
* @param meetingCode
* @return
*/
@Override
public String getMeetingHost(String meetingCode) {
/**
* 腾讯会议参数
*/
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId, tencentSecretKey)
.build();
MeetingsApi.ApiV1MeetingsGetRequest request =
new MeetingsApi.ApiV1MeetingsGetRequest.Builder()
.userid(tencentAdminUserId)
.instanceid("1")
.meetingCode(meetingCode)
.build();
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()))).timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
try {
MeetingsApi.ApiV1MeetingsGetResponse response =
client.meetings().v1MeetingsGet(request, authenticatorBuilder);
// response from `v1MeetingsGet`: V1MeetingsGet200Response
System.out.printf("Response from `MeetingsApi.v1MeetingsGet`: \nheader: %s\n%s\n",
response.getHeader(), response.getData());
// 提取主持人 userid
// 5. 处理响应数据
if (response == null || response.getData() == null) {
throw new RuntimeException("API响应数据为空");
}
// 6. 使用更安全的方式解析响应
V1MeetingsGet200Response responseData = response.getData();
if (responseData.getMeetingInfoList() == null || responseData.getMeetingInfoList().isEmpty()) {
throw new RuntimeException("未找到会议信息");
}
V1MeetingsGet200ResponseMeetingInfoListInner meetingInfo = responseData.getMeetingInfoList().get(0);
if (meetingInfo.getCurrentHosts() == null || meetingInfo.getCurrentHosts().isEmpty()) {
throw new RuntimeException("会议没有主持人");
}
String hostUserId = meetingInfo.getCurrentHosts().get(0).getUserid();
if (hostUserId == null || hostUserId.isEmpty()) {
throw new RuntimeException("主持人userid为空");
}
System.out.println("成功获取主持人userid: {}" + hostUserId);
return hostUserId;
} catch (ClientException e) {
System.out.printf("Error when calling `MeetingsApi.v1MeetingsGet`: %s\n", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
System.out.printf("Error when calling `MeetingsApi.v1MeetingsGet`: %s\n", e);
System.out.printf("Full HTTP response: %s\n", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
}
}
package com.cmeeting.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.WeComUserMapper;
import com.cmeeting.pojo.WeComUser;
import com.cmeeting.service.WeComService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import com.cmeeting.util.RedisUtils;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200ResponseMeetingInfoListInner;
import okhttp3.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.convertJsonToWeComUsers;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNames;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> implements WeComService {
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
private static final String ACCESS_TOKEN = "z8g0fkzlVBMKrwi67CRwE7gcnGqjOMrTK6LDih06vs1ixpV9BkrVWQJkoF3HFf5a2UrMvTKEBewOo7wPGvVxNjV_ZGOp_dUxTfnCG6kRUK9LKXPlhskGOI3mcI3ZZpGqpPcgjCsE877hh1BYTB_i3t5X_q1tZfv0K636o3a0HyAMf4GlzviCd1vQHfkGf3p15FjnATwDV8ddhMZo8U-_uA";
private static final String CORP_ID = "wx34544d057db97ffd";
private static final String CORP_SECRET = "7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0";
private static final String TOKEN_KEY = "ZHONGJI_WECOM_KEY";
@Resource
private WeComUserMapper weComUserMapper;
@Resource
private RedisUtils redisUtils;
@Override
public void batchInsert(List<WeComUser> users) {
......@@ -46,35 +48,42 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
@Override
public void doUsers() throws Exception {
// 示例:获取部门ID为6的用户列表
int departmentId = 6;
JsonObject departmentList = getDepartmentList();
JsonObject result = getUserListByDepartment(departmentId);
// 处理返回结果
if (result.get("errcode").getAsInt() == 0) {
System.out.println("企微获取用户列表成功:");
JsonArray userList = result.getAsJsonArray("userlist");
//获取所有可见范围下的部门
JSONArray departmentList = getDepartmentList();
Iterator<Object> iterator = departmentList.iterator();
List<WeComUser> users = new ArrayList<>();
while (iterator.hasNext()){
JSONObject department = (JSONObject) iterator.next();
String departmentId = String.valueOf(department.get("id"));
//获取部门下的所有人员
JSONArray userList = getUserListByDepartment(departmentId);
for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject();
System.out.println("姓名: " + user.get("name").getAsString() +
", UserID: " + user.get("userid").getAsString());
JSONObject userJson = (JSONObject)userList.get(i);
System.out.println("姓名: " + userJson.get("name") +
", UserID: " + userJson.get("userid"));
// 转换为WeComUser集合
WeComUser user = new WeComUser();
user.setUserId(userJson.getString("userid"));
user.setUserName(userJson.getString("name"));
users.add(user);
}
// 2. 转换为WeComUser集合
List<WeComUser> users = convertJsonToWeComUsers(result);
// 3. 检查重名并设置标志
markDuplicateNames(users);
// 4. 批量插入数据库
batchInsert(users);
}
// 检查重名并设置标志
markDuplicateNames(users);
// 批量插入数据库
batchInsert(users);
}
@Override
public String getToken() {
if(redisUtils.hasKey(TOKEN_KEY)){
return String.valueOf(redisUtils.get(TOKEN_KEY));
}
//获取token
// String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo";
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET;
String accessToken = "";
String accessToken;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
......@@ -84,8 +93,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
JSONObject jsonResponse = new JSONObject(responseBody);
JSONObject jsonResponse = JSONObject.parseObject(responseBody);
accessToken = jsonResponse.getString("access_token");
redisUtils.set(TOKEN_KEY,accessToken,Integer.valueOf(jsonResponse.getString("expires_in")) - 300);
} else {
throw new RuntimeException("Failed to fetch token. HTTP Code: " + response.code());
}
......@@ -95,13 +105,95 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
return accessToken;
}
@Override
public List<WeComUser> noBindUsers() {
List<WeComUser> noBindUsers = weComUserMapper.noBindUsers();
return noBindUsers;
}
/**
* 创建预约会议(传入 wid 作为主持人)
*/
@Override
public Map<String, String> createTempMeeting(String wid, String token) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/meeting/create?access_token=" + token;
org.json.JSONObject body = new org.json.JSONObject()
.put("admin_userid", wid) // 主持人 userid
.put("title", "自动创建会议")
.put("meeting_start", System.currentTimeMillis() / 1000 + 3600) // 1小时后开始
.put("meeting_duration", 3600)
.put("invitees", new org.json.JSONObject() // 添加 invitees 字段
.put("userid", new String[]{wid}) // 将 wid 作为参会人
);
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(MediaType.parse("application/json"), body.toString()))
.build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
org.json.JSONObject json = new org.json.JSONObject(response.body().string());
if (json.getInt("errcode") != 0) {
return null; // 或者抛出异常
}
Map<String, String> result = new HashMap<>();
result.put("meetingid", json.getString("meetingid"));
result.put("meeting_code", json.getString("meeting_code"));
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 通过企业微信接口取消预约会议
*
* @param meetingId
*/
@Override
public void cancelMeeting(String meetingId, String weComToken) {
// 1. 构造请求URL
String url = "https://qyapi.weixin.qq.com/cgi-bin/meeting/cancel?access_token=" + weComToken;
// 2. 构造请求体(JSON格式)
org.json.JSONObject requestBody = new org.json.JSONObject();
requestBody.put("meetingid", meetingId);
// 3. 发送POST请求
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(
MediaType.parse("application/json"),
requestBody.toString()
);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
// 4. 解析响应
String responseData = response.body().string();
org.json.JSONObject jsonResponse = new org.json.JSONObject(responseData);
if (jsonResponse.getInt("errcode") == 0) {
System.out.println("会议取消成功: " + meetingId);
} else {
System.err.println("会议取消失败: " + responseData);
}
} catch (IOException e) {
System.err.println("取消会议请求异常: " + e.getMessage());
}
}
/**
* 获取企微部门列表
*
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public JsonObject getDepartmentList() throws Exception {
public JSONArray getDepartmentList() throws Exception {
String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=" + getToken();
URL url = new URL(urlStr);
......@@ -119,9 +211,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
JSONObject jsonResult = JSONObject.parseObject(response.toString());
JSONArray departmentArray = (JSONArray)jsonResult.get("department_id");
return departmentArray;
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
......@@ -134,8 +226,8 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public JsonObject getUserListByDepartment ( int departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
public JSONArray getUserListByDepartment ( String departmentId) throws Exception {
String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=" + getToken() + "&department_id=" + departmentId;
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
......@@ -152,11 +244,29 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
JSONObject jsonResult = JSONObject.parseObject(response.toString());
JSONArray userList = (JSONArray)jsonResult.get("userlist");
return userList;
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
}
/**
* 标记企业微信重名用户
*/
public static void markDuplicateNames(List<WeComUser> users) {
// 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(WeComUser::getUserName, Collectors.counting()));
// 设置是否重名标志
users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsRepeatName("1"); // 重名
} else {
user.setIsRepeatName("0"); // 不重名
}
});
}
}
......@@ -33,6 +33,16 @@ public class MinioUtils {
}
}
public InputStream getFile(String fileName){
try{
getMinioClient();
InputStream inputStream = minioClient.getObject(bucketName, fileName);
return inputStream;
}catch (Exception e){
throw new RuntimeException(e);
}
}
public InputStream getFile(MinioClient minioClient, String fileName){
try{
InputStream inputStream = minioClient.getObject(bucketName, fileName);
......
package com.cmeeting.util;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author 刘振萌
* @date 2020/5/19 20:07
*/
@Component
public class RedisUtils {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
/**
* 通过key删除value
*
* @param keys
* @return
*/
public boolean removeKey(List<String> keys) {
return redisTemplate.delete(keys);
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 模糊查询获取key值
*
* @param pattern
* @return
*/
public Set keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 使用Redis的消息队列
*
* @param channel
* @param message 消息内容
*/
public void convertAndSend(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
//=========BoundListOperations 用法 start============
/**
* 根据起始结束序号遍历Redis中的list
*
* @param listKey
* @param start 起始序号
* @param end 结束序号
* @return
*/
public List<Object> rangeList(String listKey, long start, long end) {
//绑定操作
BoundListOperations<Object, Object> boundValueOperations = redisTemplate.boundListOps(listKey);
//查询数据
return boundValueOperations.range(start, end);
}
/**
* 弹出右边的值 --- 并且移除这个值
*
* @param listKey
*/
public Object rifhtPop(String listKey) {
//绑定操作
BoundListOperations<Object, Object> boundValueOperations = redisTemplate.boundListOps(listKey);
return boundValueOperations.rightPop();
}
public String getInitTokenKey(String sessionId, String robotId) {
return "INIT_ROBOT_TOKEN" + sessionId + "_" + robotId;
}
public String getAnswerTokenKey(String sessionId, String robotId) {
return "ANSWER_TOKEN_KEY" + sessionId + "_" + robotId;
}
}
\ No newline at end of file
......@@ -9,8 +9,6 @@ import com.google.gson.JsonElement;
import java.util.*;
import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.getUserListByDepartment;
public class WeComUserService {
// 假设这是你的MyBatis Mapper
......@@ -21,23 +19,6 @@ public class WeComUserService {
}
/**
* 处理部门用户数据并存入数据库
*/
public void processAndSaveDepartmentUsers(int departmentId) throws Exception {
// 1. 获取企业微信API数据
JsonObject result = getUserListByDepartment(departmentId);
// 2. 转换为WeComUser集合
List<WeComUser> users = convertJsonToWeComUsers(result);
// 3. 检查重名并设置标志
markDuplicateNames(users);
// 4. 批量插入数据库
weComUserMapper.batchInsert(users);
}
/**
* 将企业微信返回的JSON转换为WeComUser列表
*/
private List<WeComUser> convertJsonToWeComUsers(JsonObject json) {
......
......@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
public class TencentMeetingVO {
......@@ -30,6 +32,6 @@ public class TencentMeetingVO {
public static class RecordFile{
private String meetingId;//如果是周期会议,这个id表示主会议
private String subMeetingId;//如果是周期会议,这个id表示子会议
private String recordFileId;
private List<String> recordFileIdList;
}
}
\ No newline at end of file
server:
port: 8080
# ????????
# application.yml
# spring.datasource.url=jdbc:mysql://192.168.10.155:3306/cmeeting?useSSL=false&characterEncoding=utf8
# spring.datasource.username=root
# spring.datasource.password=qizhi123
# spring.datasource.driver-class-name=com.mysql.jdbc.Driver
############################################################## minio
MINIO_ADDRESS: http://192.168.10.154:9000
MINIO_BUCKET: zhongji
MINIO_USERNAME: minio
MINIO_PASSWORD: minio123
############################################################## redis
REDIS_ADDRESS: 192.168.10.154
REDIS_PORT: 6380
REDIS_PASS: standard123
REDIS_DATABASE: 8
############################################################## llm
# local
LLM_API_ADDR: https://bedrock.chatbot.cn
# prod
#LLM_API_ADDR: http://10.56.1.150:8000
############################################################## tencent meeting
# local
#TENCENT_APPID: 211153201
#TENCENT_SDKID: 28370276340
#TENCENT_SECRETID: BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6
#TENCENT_SECRETKEY: 3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO
#TENCENT_ADMIN_USERID: woaJARCQAAhkyWGuf8n9InhZsxQstjjA
#TENCENT_ADMIN_USERID: woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA
# prod
TENCENT_APPID: 210468336
TENCENT_SDKID: 28790143843
TENCENT_SECRETID: 0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL
TENCENT_SECRETKEY: gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy
TENCENT_ADMIN_USERID: woaJARCQAAftcvU6GGoOn66rdSZ4IrOA
############################################################## email
EMAIL_SENDER: cmeeting_assistant@cimc.com
EMAIL_SENDER_PWD: scyou@xih45g6@xih4
EMAIL_SMTP_HOST: smtp.office365.com
############################################################## permission
PERMISSION_APPLiCATION_ID: 1928343847335759872
PERMISSION_TENANT_ID: 1928343652791357440
# ?????primary?
#spring.datasource.primary.jdbc-url=jdbc:mysql://192.168.10.154:3307/aigc-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
##spring.datasource.primary.username=root
##spring.datasource.primary.password=123456
##spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver
##
### ?????secondary?
##spring.datasource.secondary.jdbc-url=jdbc:mysql://192.168.10.154:3307/useradmin-zhongji-test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC
##spring.datasource.secondary.username=root
##spring.datasource.secondary.password=123456
##spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
spring:
datasource:
......@@ -34,12 +56,23 @@ spring:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
redis:
database: ${REDIS_DATABASE}
host: ${REDIS_ADDRESS}
jedis:
pool:
max-active: 8
max-idle: 8
max-wait: 30000ms
min-idle: 1
port: ${REDIS_PORT}
timeout: 6000ms
password: ${REDIS_PASS}
# MyBatis ??
# mybatis.mapper-locations=classpath:mapper/primary/*.xml
mybatis.type-aliases-package: com.cmeeting.pojo\
# ??????
mybatis:
configuration:
map-underscore-to-camel-case: true
......@@ -52,19 +85,6 @@ global-config:
db-config:
column-underline: true
#logging:
# level:
# com:
# zaxxer:
# hikari: INFO
############################################################## minIO
MINIO_ADDRESS: http://192.168.10.154:9000
MINIO_BUCKET: zhongji
MINIO_USERNAME: minio
MINIO_PASSWORD: minio123
#Minio服务所在地址
minio.endpoint: ${MINIO_ADDRESS}
#存储桶名称
......@@ -73,33 +93,28 @@ minio.bucketName: ${MINIO_BUCKET}
minio.accessKey: ${MINIO_USERNAME}
#访问的秘钥
minio.secretKey: ${MINIO_PASSWORD}
############################################################## minIO
############################################################## tencent meeting
# local
#tencent.appId=211153201
#tencent.sdkId=28370276340
#tencent.secretId=BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6
#tencent.secretKey=3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO
#tencent.admin.userId=woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA
#tencent.admin.userId=woaJARCQAAhkyWGuf8n9InhZsxQstjjA
# prod
tencent:
appId: 210468336
sdkId: 28790143843
secretId: 0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL
secretKey: gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy
admin.userId: woaJARCQAAftcvU6GGoOn66rdSZ4IrOA
appId: ${TENCENT_APPID}
sdkId: ${TENCENT_SDKID}
secretId: ${TENCENT_SECRETID}
secretKey: ${TENCENT_SECRETKEY}
admin.userId: ${TENCENT_ADMIN_USERID}
meeting:
token: QQZNb7xWQB47MpZF4C2DFAkv8
aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
email:
sender: cmeeting_assistant@cimc.com
sender-pwd: scyou@xih45g6@xih4
smtp-host: smtp.office365.com
############################################################## tencent meeting
sender: ${EMAIL_SENDER}
sender-pwd: ${EMAIL_SENDER_PWD}
smtp-host: ${EMAIL_SMTP_HOST}
llm:
api-addr: ${LLM_API_ADDR}
permission:
applicationId: ${PERMISSION_APPLiCATION_ID}
tenantId: ${PERMISSION_TENANT_ID}
logging:
level:
......
......@@ -2,7 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmeeting.mapper.primary.AuthMapper">
<select id="getAuthByTargrtId" resultType="com.cmeeting.pojo.CoreModulePermissions">
<select id="getAuthByTargetId" resultType="com.cmeeting.pojo.CoreModulePermissions">
SELECT
id,
type,
......@@ -16,7 +16,8 @@
FROM
core_module_permissions
WHERE
target_id = #{targetId}
user_type = 2
and target_id = #{targetId}
<if test="tenantId != null and tenantId != ''">
AND tenant_id = #{tenantId}
</if>
......
......@@ -2,16 +2,17 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmeeting.mapper.primary.MeetingInfoMapper">
<insert id="batchInsert" parameterType="list">
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host_id, participant_user_ids, start_time,
end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id, record_content, record_xml)
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host, participant_users, start_time,
end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id, record_content, record_xml, generate_retry,
push_retry, record_file_id,email)
VALUES
<foreach collection="meetingSaveList" item="meeting" separator=",">
(
#{meeting.subject},
#{meeting.meetingId},
#{meeting.meetingCode},
#{meeting.hostId},
#{meeting.participantUserIds},
#{meeting.host},
#{meeting.participantUsers},
#{meeting.startTime},
#{meeting.endTime},
#{meeting.isGenerated},
......@@ -20,7 +21,11 @@ end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id,
#{meeting.syncTime},
#{meeting.subMeetingId},
#{meeting.recordContent},
#{meeting.recordXml}
#{meeting.recordXml},
#{meeting.generateRetry},
#{meeting.pushRetry},
#{meeting.recordFileId},
#{meeting.email}
)
</foreach>
</insert>
......
......@@ -4,9 +4,9 @@
<!-- 批量插入用户ID信息 -->
<insert id="insertUsers" parameterType="List">
INSERT INTO userid (
userName,
Wid,
Tid
user_name,
wid,
tid
) VALUES
<foreach collection="list" item="item" separator=",">
(
......@@ -19,14 +19,14 @@
<!-- 更新 Tid 字段 -->
<update id="updateUser" parameterType="com.cmeeting.pojo.UserId">
UPDATE userid
SET Tid = #{tid}
SET tid = #{tid}
WHERE wid = #{wid}
</update>
<select id="getUsers" resultType="com.cmeeting.pojo.UserId">
select * from userid
</select>
<select id="getWidByTid" resultType="java.lang.String">
select Wid from userid WHERE Tid = #{operatorUserId}
select wid from userid WHERE Tid = #{operatorUserId}
</select>
</mapper>
\ No newline at end of file
......@@ -30,8 +30,10 @@
<select id="getAlluser" resultType="com.cmeeting.pojo.WeComUser">
SELECT * FROM user_wecom
</select>
<select id="getSameName" resultType="com.cmeeting.pojo.WeComUser">
SELECT * FROM user_wecom
WHERE isrepeat_name = "1"
<select id="noBindUsers" resultType="com.cmeeting.pojo.WeComUser">
SELECT t1.*
FROM user_wecom t1
left join userid t2 on t1.user_id = t2.Wid
WHERE t2.id is null
</select>
</mapper>
\ No newline at end of file
......@@ -9,4 +9,31 @@
AND tenant_id = #{tenantId}
LIMIT 1
</select>
<select id="getParentDeptId" resultType="java.lang.String">
SELECT parent_id
FROM sys_user_sync_category
WHERE dept_id = #{deptId}
AND tenant_id = #{tenantId}
LIMIT 1
</select>
<select id="getSubDeptId" resultType="java.lang.String">
SELECT dept_id
FROM sys_user_sync_category
WHERE parent_id = #{deptId}
AND tenant_id = #{tenantId}
</select>
<select id="getUsersByDept" resultType="java.lang.String">
select t1.user_id as wId
from sys_user_sync t1
where t1.dept_id in
<foreach collection="deptIds" item="deptId" separator="," open="(" close=")">
#{deptId}
</foreach>
</select>
<select id="getUserEmail" resultType="com.cmeeting.dto.UserDTO">
select t1.user_id as wId,IFNULL(t1.email,t1.company_email) as email
from sys_user_sync t1
where t1.email is not null
or t1.company_email is not null
</select>
</mapper>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论