提交 e0b7d23d 作者: duanxincheng

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

父级 b87c127c
...@@ -330,11 +330,11 @@ ...@@ -330,11 +330,11 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>junit</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
<version>RELEASE</version> <version>2.7.0</version> <!-- 与你的Spring Boot版本一致 -->
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
......
...@@ -146,7 +146,7 @@ public class TencentMeetingCallbackController { ...@@ -146,7 +146,7 @@ public class TencentMeetingCallbackController {
//通过智能体id查询该id下的部门和用户 //通过智能体id查询该id下的部门和用户
String targetId = "1815393211829587968";//职能体id String targetId = "1815393211829587968";//职能体id
String tenantId = "1806976109082972160";//租户id String tenantId = "1806976109082972160";//租户id
List<CoreModulePermissions> auths = authMapper.getAuthByTargrtId(targetId, tenantId); List<CoreModulePermissions> auths = authMapper.getAuthByTargetId(targetId, tenantId);
// 创建两个集合分别存储type=0(部门)和type=1(员工)的数据 // 创建两个集合分别存储type=0(部门)和type=1(员工)的数据
List<CoreModulePermissions> type0List = new ArrayList<>(); List<CoreModulePermissions> type0List = new ArrayList<>();
List<CoreModulePermissions> type1List = new ArrayList<>(); List<CoreModulePermissions> type1List = new ArrayList<>();
......
package com.cmeeting; //package com.cmeeting;
//
import com.cmeeting.pojo.TencentMeetingUser; //import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.WeComUser; //import com.cmeeting.pojo.WeComUser;
import com.google.gson.Gson; //import com.google.gson.Gson;
import com.google.gson.JsonArray; //import com.google.gson.JsonArray;
import com.google.gson.JsonElement; //import com.google.gson.JsonElement;
import com.google.gson.JsonObject; //import com.google.gson.JsonObject;
//
import java.io.BufferedReader; //import java.io.BufferedReader;
import java.io.InputStreamReader; //import java.io.InputStreamReader;
import java.net.HttpURLConnection; //import java.net.HttpURLConnection;
import java.net.URL; //import java.net.URL;
//
import com.tencentcloudapi.wemeet.Client; //import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder; //import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator; //import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException; //import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException; //import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.user_manager.api.UserManagerApi; //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.V1UsersListGet200Response;
import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200ResponseUsersInner; //import com.tencentcloudapi.wemeet.service.user_manager.model.V1UsersListGet200ResponseUsersInner;
import org.springframework.stereotype.Service; //import org.springframework.stereotype.Service;
//
import java.math.BigInteger; //import java.math.BigInteger;
import java.security.SecureRandom; //import java.security.SecureRandom;
import java.util.ArrayList; //import java.util.ArrayList;
import java.util.List; //import java.util.List;
import java.util.Map; //import java.util.Map;
import java.util.stream.Collectors; //import java.util.stream.Collectors;
@Service //@Service
public class WeComAndTencentMeeting { //public class WeComAndTencentMeeting {
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist"; // 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"; // private static final String ACCESS_TOKEN = "wyYAmCbeVIPvj_Gpdd8ixp_td_hPv2OtTU8gpTGSpB7TaftbpNps5fc_JdCRzpgy_tUv-aGueJpRV6XJeYAG-rqkzCfDV-rcpJbqB0LypjvJUtxPeTmPqEtQuCMwH6euGGVaJZXoc33ATrS70U0T4_4WIrATqpsN2Ed2XsNGyN5N6aG8mq6AyLMG9TokVWx1qd2qF1zVTwve6aqRMWHwXg";
public static void main(String[] args) { // public static void main(String[] args) {
try { // try {
// 示例:获取部门ID为6的用户列表 // // 示例:获取部门ID为6的用户列表
int departmentId = 6; // int departmentId = 6;
JsonObject result = getUserListByDepartment(departmentId); // JsonObject result = getUserListByDepartment(departmentId);
// 处理返回结果 // // 处理返回结果
if (result.get("errcode").getAsInt() == 0) { // if (result.get("errcode").getAsInt() == 0) {
System.out.println("企微获取用户列表成功:"); // System.out.println("企微获取用户列表成功:");
JsonArray userList = result.getAsJsonArray("userlist"); // JsonArray userList = result.getAsJsonArray("userlist");
for (int i = 0; i < userList.size(); i++) { // for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject(); // JsonObject user = userList.get(i).getAsJsonObject();
System.out.println("姓名: " + user.get("name").getAsString() + // System.out.println("姓名: " + user.get("name").getAsString() +
", UserID: " + user.get("userid").getAsString()); // ", UserID: " + user.get("userid").getAsString());
} // }
} else { // } else {
System.out.println("企微获取用户列表失败: " + result.get("errmsg").getAsString()); // System.out.println("企微获取用户列表失败: " + result.get("errmsg").getAsString());
} // }
} catch (Exception e) { // } catch (Exception e) {
e.printStackTrace(); // e.printStackTrace();
} // }
/** // /**
* 腾讯会议通过通讯录获取员工信息 // * 腾讯会议通过通讯录获取员工信息
*/ // */
// 1. 构造client客户端 // // 1. 构造client客户端
Client client = new Client.Builder() // Client client = new Client.Builder()
.withAppId("211153201").withSdkId("28370276340") // .withAppId("211153201").withSdkId("28370276340")
.withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO") // .withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
.build(); // .build();
//
// 2. 开始循环获取用户列表 // // 2. 开始循环获取用户列表
fetchUsersInBatches(client, 3); // fetchUsersInBatches(client, 3);
} // }
/** // /**
* 将企业微信返回的JSON转换为WeComUser列表 // * 将企业微信返回的JSON转换为WeComUser列表
*/ // */
public static List<WeComUser> convertJsonToWeComUsers(JsonObject json) { // public static List<WeComUser> convertJsonToWeComUsers(JsonObject json) {
List<WeComUser> users = new ArrayList<>(); // List<WeComUser> users = new ArrayList<>();
//
if (json.has("userlist") && json.get("userlist").isJsonArray()) { // if (json.has("userlist") && json.get("userlist").isJsonArray()) {
JsonArray userList = json.getAsJsonArray("userlist"); // JsonArray userList = json.getAsJsonArray("userlist");
Gson gson = new Gson(); // Gson gson = new Gson();
//
for (JsonElement element : userList) { // for (JsonElement element : userList) {
JsonObject userJson = element.getAsJsonObject(); // JsonObject userJson = element.getAsJsonObject();
WeComUser user = new WeComUser(); // WeComUser user = new WeComUser();
//
// 根据企业微信API实际返回字段调整 // // 根据企业微信API实际返回字段调整
user.setUserId(userJson.get("userid").getAsString()); // user.setUserId(userJson.get("userid").getAsString());
user.setUserName(userJson.get("name").getAsString()); // user.setUserName(userJson.get("name").getAsString());
//
// 其他字段设置... // // 其他字段设置...
users.add(user); // users.add(user);
} // }
} // }
//
return users; // return users;
} // }
//
/** // /**
* 标记企业微信重名用户 // * 标记企业微信重名用户
*/ // */
public static void markDuplicateNames(List<WeComUser> users) { // public static void markDuplicateNames(List<WeComUser> users) {
// 按姓名分组,统计每个名字出现的次数 // // 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream() // Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(WeComUser::getUserName, Collectors.counting())); // .collect(Collectors.groupingBy(WeComUser::getUserName, Collectors.counting()));
//
// 设置是否重名标志 // // 设置是否重名标志
users.forEach(user -> { // users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) { // if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsRepeatName("1"); // 重名 // user.setIsRepeatName("1"); // 重名
} else { // } else {
user.setIsRepeatName("0"); // 不重名 // user.setIsRepeatName("0"); // 不重名
} // }
}); // });
} // }
//
/** // /**
* 标记企业微信重名用户 // * 标记企业微信重名用户
*/ // */
public static void markDuplicateNamesTecent(List<TencentMeetingUser> users) { // public static void markDuplicateNamesTecent(List<TencentMeetingUser> users) {
// 按姓名分组,统计每个名字出现的次数 // // 按姓名分组,统计每个名字出现的次数
Map<String, Long> nameCountMap = users.stream() // Map<String, Long> nameCountMap = users.stream()
.collect(Collectors.groupingBy(TencentMeetingUser::getUserName, Collectors.counting())); // .collect(Collectors.groupingBy(TencentMeetingUser::getUserName, Collectors.counting()));
//
// 设置是否重名标志 // // 设置是否重名标志
users.forEach(user -> { // users.forEach(user -> {
if (nameCountMap.get(user.getUserName()) > 1) { // if (nameCountMap.get(user.getUserName()) > 1) {
user.setIsrepeatName("1"); // 重名 // user.setIsrepeatName("1"); // 重名
} else { // } else {
user.setIsrepeatName("0"); // 不重名 // user.setIsrepeatName("0"); // 不重名
} // }
}); // });
} // }
//
/** // /**
* 根据企业微信部门ID获取用户列表 // * 根据企业微信部门ID获取用户列表
* // *
* @param departmentId 部门ID // * @param departmentId 部门ID
* @return 包含用户列表的JsonObject // * @return 包含用户列表的JsonObject
* @throws Exception // * @throws Exception
*/ // */
public static JsonObject getUserListByDepartment(int departmentId) throws Exception { // public static JsonObject getUserListByDepartment(int departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId; // String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
//
URL url = new URL(urlStr); // URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); // conn.setRequestMethod("GET");
//
int responseCode = conn.getResponseCode(); // int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); // BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine; // String inputLine;
StringBuilder response = new StringBuilder(); // StringBuilder response = new StringBuilder();
//
while ((inputLine = in.readLine()) != null) { // while ((inputLine = in.readLine()) != null) {
response.append(inputLine); // response.append(inputLine);
} // }
in.close(); // in.close();
//
// 使用Gson解析JSON响应 // // 使用Gson解析JSON响应
Gson gson = new Gson(); // Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class); // return gson.fromJson(response.toString(), JsonObject.class);
} else { // } else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode); // throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
} // }
} // }
//
/** // /**
* 腾讯会议获取员工信息 // * 腾讯会议获取员工信息
*/ // */
/** // /**
* 分批次获取用户列表 // * 分批次获取用户列表
* // *
* @param client 客户端实例 // * @param client 客户端实例
* @param pageSize 每页大小 // * @param pageSize 每页大小
* @return // * @return
*/ // */
public static List<TencentMeetingUser> fetchUsersInBatches(Client client, int pageSize) { // public static List<TencentMeetingUser> fetchUsersInBatches(Client client, int pageSize) {
int currentPage = 1; // int currentPage = 1;
boolean hasMore = true; // boolean hasMore = true;
int totalUsers = 0; // int totalUsers = 0;
List<TencentMeetingUser> userList = new ArrayList<>(); // 创建集合存储用户数据 // List<TencentMeetingUser> userList = new ArrayList<>(); // 创建集合存储用户数据
//
while (hasMore) { // while (hasMore) {
try { // try {
// 1. 构造请求参数 // // 1. 构造请求参数
UserManagerApi.ApiV1UsersListGetRequest request = // UserManagerApi.ApiV1UsersListGetRequest request =
new UserManagerApi.ApiV1UsersListGetRequest.Builder() // new UserManagerApi.ApiV1UsersListGetRequest.Builder()
.page(String.valueOf(currentPage)) // .page(String.valueOf(currentPage))
.pageSize(String.valueOf(pageSize)) // .pageSize(String.valueOf(pageSize))
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA") // .operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1") // .operatorIdType("1")
.build(); // .build();
//
// 2. 构造JWT鉴权器 // // 2. 构造JWT鉴权器
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())); // BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L); // String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder = // AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp); // new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
//
// 3. 发送请求 // // 3. 发送请求
UserManagerApi.ApiV1UsersListGetResponse response = // UserManagerApi.ApiV1UsersListGetResponse response =
client.user_manager().v1UsersListGet(request, authenticatorBuilder); // client.user_manager().v1UsersListGet(request, authenticatorBuilder);
//
// 4. 处理响应并转换为TencentMeetingUser对象 // // 4. 处理响应并转换为TencentMeetingUser对象
V1UsersListGet200Response responseData = response.getData(); // V1UsersListGet200Response responseData = response.getData();
List<V1UsersListGet200ResponseUsersInner> users = responseData.getUsers(); // List<V1UsersListGet200ResponseUsersInner> users = responseData.getUsers();
int currentSize = users.size(); // int currentSize = users.size();
totalUsers += currentSize; // totalUsers += currentSize;
//
System.out.printf("第 %d 页,获取到 %d 个用户:\n", currentPage, currentSize); // System.out.printf("第 %d 页,获取到 %d 个用户:\n", currentPage, currentSize);
//
for (V1UsersListGet200ResponseUsersInner user : users) { // for (V1UsersListGet200ResponseUsersInner user : users) {
// 创建TencentMeetingUser对象并设置属性 // // 创建TencentMeetingUser对象并设置属性
TencentMeetingUser meetingUser = new TencentMeetingUser(); // TencentMeetingUser meetingUser = new TencentMeetingUser();
meetingUser.setUserId(user.getUserid()); // meetingUser.setUserId(user.getUserid());
meetingUser.setUserName(user.getUsername()); // meetingUser.setUserName(user.getUsername());
meetingUser.setIsrepeatName("0"); // 默认设为不重名,可根据实际业务调整 // meetingUser.setIsrepeatName("0"); // 默认设为不重名,可根据实际业务调整
//
userList.add(meetingUser); // 添加到集合 // userList.add(meetingUser); // 添加到集合
System.out.printf("用户ID: %s, 用户名: %s\n", user.getUserid(), user.getUsername()); // System.out.printf("用户ID: %s, 用户名: %s\n", user.getUserid(), user.getUsername());
} // }
//
// 5. 检查是否还有更多数据 // // 5. 检查是否还有更多数据
if (currentSize == 0 || currentSize < pageSize) { // if (currentSize == 0 || currentSize < pageSize) {
hasMore = false; // hasMore = false;
System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", totalUsers); // System.out.printf("\n所有用户获取完成,共获取 %d 个用户\n", totalUsers);
} else { // } else {
currentPage++; // currentPage++;
} // }
//
} catch (ClientException e) { // } catch (ClientException e) {
System.out.printf("客户端错误: %s\n", e); // System.out.printf("客户端错误: %s\n", e);
throw new RuntimeException(e); // throw new RuntimeException(e);
} catch (ServiceException e) { // } catch (ServiceException e) {
System.out.printf("服务端错误: %s\n", e); // System.out.printf("服务端错误: %s\n", e);
System.out.printf("完整响应: %s\n", new String(e.getApiResp().getRawBody())); // System.out.printf("完整响应: %s\n", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e); // throw new RuntimeException(e);
} // }
} // }
//
return userList; // 返回用户集合 // return userList; // 返回用户集合
//
} // }
} //}
...@@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController @RestController
@RequestMapping("/tencent") @RequestMapping("/tencent")
...@@ -24,8 +23,8 @@ public class TencentMeetingController { ...@@ -24,8 +23,8 @@ public class TencentMeetingController {
tecentMeetingService.doUsers(); tecentMeetingService.doUsers();
} }
@GetMapping("/getMeetingFiles") // @GetMapping("/getMeetingFiles")
public void getMeetingFiles(){ // public void getMeetingFiles(){
tecentMeetingService.getMeetingFiles(); // tecentMeetingService.getMeetingFiles();
} // }
} }
...@@ -127,32 +127,32 @@ public class UnifiedController { ...@@ -127,32 +127,32 @@ public class UnifiedController {
//取消预约会议 //取消预约会议
} }
//todo 待测试 // //todo 待测试
@GetMapping("/sameNameInsertTid") // @GetMapping("/sameNameInsertTid")
public void sameNameInsertTid(String corpid, String corpsecret) throws IOException { // public void sameNameInsertTid(String corpid, String corpsecret) throws IOException {
String weComToken = getWeComToken(corpid,corpsecret); // String weComToken = getWeComToken(corpid,corpsecret);
List<WeComUser> sameNameUsers = weComUserMapper.getSameName(); // List<WeComUser> sameNameUsers = weComUserMapper.getSameName();
List<UserId> userIds = new ArrayList<>(); // List<UserId> userIds = new ArrayList<>();
for (WeComUser user : sameNameUsers) { // for (WeComUser user : sameNameUsers) {
Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getUserId(), weComToken); // Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getUserId(), weComToken);
String meetingId = meetingCodeAndMeetingid.get("meetingid"); // String meetingId = meetingCodeAndMeetingid.get("meetingid");
String meetingCode = meetingCodeAndMeetingid.get("meeting_code"); // String meetingCode = meetingCodeAndMeetingid.get("meeting_code");
if (meetingCode == null || meetingCode.isEmpty()) { // if (meetingCode == null || meetingCode.isEmpty()) {
System.err.println("会议创建失败,跳过用户: " + user.getUserId()); // System.err.println("会议创建失败,跳过用户: " + user.getUserId());
continue; // continue;
} // }
// 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid) // // 3.2 查询会议详情,获取主持人腾讯会议 userid(假设是 host_userid)
String hostUserId = getMeetingHost(meetingCode); // String hostUserId = getMeetingHost(meetingCode);
if (hostUserId == null || hostUserId.isEmpty()) { // if (hostUserId == null || hostUserId.isEmpty()) {
System.err.println("获取主持人失败,跳过会议: " + meetingCode); // System.err.println("获取主持人失败,跳过会议: " + meetingCode);
cancelMeeting(meetingId, weComToken); // 尝试取消无效会议 // cancelMeeting(meetingId, weComToken); // 尝试取消无效会议
continue; // continue;
} // }
UserId userId = new UserId(user.getUserName(), user.getUserId(), hostUserId); // UserId userId = new UserId(user.getUserName(), user.getUserId(), hostUserId);
userIds.add(userId); // userIds.add(userId);
} // }
userIdMapper.insertUsers(userIds); // userIdMapper.insertUsers(userIds);
} // }
/** /**
* 通过企业微信接口取消预约会议 * 通过企业微信接口取消预约会议
......
...@@ -18,7 +18,6 @@ import java.net.URL; ...@@ -18,7 +18,6 @@ import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController @RestController
@RequestMapping("/wecom") @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; package com.cmeeting.email;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -29,10 +30,10 @@ public class EmailSender { ...@@ -29,10 +30,10 @@ public class EmailSender {
* @param toEmail 收件人 * @param toEmail 收件人
* @param subject 邮件主题 * @param subject 邮件主题
* @param attachmentPath 附件路径 * @param attachmentPath 附件路径
* @param recordFileId 转录文件ID * @param meetingId 会议id
* @return * @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(); Properties props = new Properties();
props.put("mail.smtp.host", SMTP_HOST); props.put("mail.smtp.host", SMTP_HOST);
...@@ -61,60 +62,63 @@ public class EmailSender { ...@@ -61,60 +62,63 @@ public class EmailSender {
" 附件为您本次会议的会议纪要,烦请下载查看"; " 附件为您本次会议的会议纪要,烦请下载查看";
AtomicInteger retryCount = new AtomicInteger(0); AtomicInteger retryCount = new AtomicInteger(0);
boolean isSent = false; boolean isSent = false;
while (retryCount.intValue() < MAX_RETRY && !isSent){ if(StringUtils.isEmpty(toEmail)){
try { log.error("收件邮箱为空,推送失败");
// 创建邮件消息 return false;
Message message = new MimeMessage(session); }
message.setFrom(new InternetAddress(SENDER)); while (retryCount.intValue() < MAX_RETRY && !isSent){
message.setRecipients(Message.RecipientType.TO, try {
InternetAddress.parse(toEmail)); // 创建邮件消息
message.setSubject(subject); Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER));
// 创建消息体部分(正文) message.setRecipients(Message.RecipientType.TO,
MimeBodyPart messageBodyPart = new MimeBodyPart(); InternetAddress.parse(toEmail));
messageBodyPart.setText(body); message.setSubject(subject);
// 创建多部分消息 // 创建消息体部分(正文)
Multipart multipart = new MimeMultipart(); MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText(body);
// 添加文本部分
multipart.addBodyPart(messageBodyPart); // 创建多部分消息
Multipart multipart = new MimeMultipart();
// 添加附件部分
if (attachmentPath != null && !attachmentPath.isEmpty()) { // 添加文本部分
MimeBodyPart attachmentPart = new MimeBodyPart(); multipart.addBodyPart(messageBodyPart);
DataSource source = new FileDataSource(attachmentPath);
attachmentPart.setDataHandler(new DataHandler(source)); // 添加附件部分
attachmentPart.setFileName(new File(attachmentPath).getName()); if (attachmentPath != null && !attachmentPath.isEmpty()) {
multipart.addBodyPart(attachmentPart); 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); message.setContent(multipart);
// 发送邮件 // 发送邮件
Transport.send(message); Transport.send(message);
log.error("邮件已成功发送: recordFileId->{}", recordFileId); log.error("邮件已成功发送: meetingId->{}", meetingId);
isSent = true; isSent = true;
} catch (MessagingException e) { } catch (MessagingException e) {
//todo 邮件失败记录 //todo 邮件失败记录
// 异常处理 // 异常处理
retryCount.getAndIncrement(); retryCount.getAndIncrement();
if (retryCount.intValue() > MAX_RETRY) { if (retryCount.intValue() > MAX_RETRY) {
log.error("邮件发送达到最大重试次数: recordFileId->{}", recordFileId); log.error("邮件发送达到最大重试次数: meetingId->{}", meetingId);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
// 指数退避 // 指数退避
try { try {
Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000); Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000);
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new RuntimeException("邮件发送重试失败", ie); throw new RuntimeException("邮件发送重试失败", ie);
}
return false;
} }
} }
return true; }
return isSent;
} }
} }
\ No newline at end of file
package com.cmeeting.job; 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.FileProcessProducer;
import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.service.TecentMeetingService; import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.service.WeComService; import com.cmeeting.service.WeComService;
import com.cmeeting.vo.TencentMeetingVO; import com.cmeeting.vo.TencentMeetingVO;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component @Component
@Slf4j
public class CmeetingJob { 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 @Autowired
private WeComService weComService; private WeComService weComService;
@Autowired @Autowired
private TecentMeetingService tecentMeetingService; private TecentMeetingService tecentMeetingService;
@Value(value = "${tencent.appId}") @Autowired
private String tencentAppId; private MeetingInfoService meetingInfoService;
@Value(value = "${tencent.sdkId}") @Autowired
private String tencentSdkId; private UserIdMapper userIdMapper;
@Value(value = "${tencent.secretId}")
private String tencentSecretId;
@Value(value = "${tencent.secretKey}")
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Autowired @Autowired
private FileProcessProducer producer; private FileProcessProducer producer;
/** /**
* 企微人员定时同步 * 企微人员定时同步
*/ */
// @Scheduled(fixedRate = 5 * 60 * 1000) @Scheduled(cron = "0 30 6 * * ?")
public void weComUserSync() { public void weComUserSync() {
try { try {
logger.info("-------企微人员定时同步任务开始-------"); log.info("-------企微人员定时同步任务开始-------");
logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
weComService.doUsers(); weComService.doUsers();
logger.info("-------企微人员定时同步任务结束--------"); log.info("-------企微人员定时同步任务结束--------");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
/** /**
* 企微人员定时同步 * 腾讯会议人员定时同步
*/ */
@Scheduled(cron = "0 0 7 * * ?")
// @Scheduled(fixedRate = 5 * 60 * 1000) // @Scheduled(fixedRate = 5 * 60 * 1000)
public void TencentUserSync() { public void TencentUserSync() {
try { try {
logger.info("-------腾讯会议人员定时同步任务开始-------"); log.info("-------腾讯会议人员定时同步任务开始-------");
logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); log.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
tecentMeetingService.doUsers(); tecentMeetingService.doUsers();
logger.info("-------腾讯会议人员定时同步任务结束--------"); log.info("-------腾讯会议人员定时同步任务结束--------");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); 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() { public void execute() {
// 定义时间格式化器 // 定义时间格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
...@@ -86,19 +137,101 @@ public class CmeetingJob { ...@@ -86,19 +137,101 @@ public class CmeetingJob {
Long nowTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond(); Long nowTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond();
Long beforeDayTimestamp = beforeDay.atZone(ZoneId.systemDefault()).toEpochSecond(); Long beforeDayTimestamp = beforeDay.atZone(ZoneId.systemDefault()).toEpochSecond();
//日志记录 //日志记录
logger.info("起始时间: " + beforeDay.format(formatter) + " | Unix 时间戳: " + beforeDayTimestamp); log.info("起始时间: " + beforeDay.format(formatter) + " | Unix 时间戳: " + beforeDayTimestamp);
logger.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp); log.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp);
logger.info("----------------------------------"); log.info("----------------------------------");
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles();
List<UserDTO> accessUserIds = tecentMeetingService.getAccessUserIds();
if (CollectionUtils.isEmpty(accessUserIds)) {
log.info("无生成纪要权限的人员");
return;
}
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles(accessUserIds);
if (meetingFiles == null || meetingFiles.isEmpty()) { if (meetingFiles == null || meetingFiles.isEmpty()) {
logger.info("没有录制文件需要处理"); log.info("没有录制文件需要处理");
return; 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; ...@@ -10,9 +10,13 @@ import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.email.EmailSender; import com.cmeeting.email.EmailSender;
import com.cmeeting.mapper.primary.MeetingInfoMapper; import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.MeetingInfo; import com.cmeeting.pojo.MeetingInfo;
import com.cmeeting.pojo.MeetingRecordTemplate;
import com.cmeeting.pojo.UserId;
import com.cmeeting.service.MeetingInfoService; import com.cmeeting.service.MeetingInfoService;
import com.cmeeting.util.MinioUtils; import com.cmeeting.util.MinioUtils;
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.XWPFTemplate;
...@@ -37,11 +41,13 @@ import okhttp3.Response; ...@@ -37,11 +41,13 @@ import okhttp3.Response;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor; import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.*; import java.io.*;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.SecureRandom; import java.security.SecureRandom;
...@@ -59,7 +65,7 @@ import java.util.stream.Collectors; ...@@ -59,7 +65,7 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class FileProcessTask { public class FileProcessTask {
private String recordFileId; private List<String> recordFileIdList;
private String meetingId; private String meetingId;
private String subMeetingId; private String subMeetingId;
private String savePath; private String savePath;
...@@ -73,136 +79,113 @@ public class FileProcessTask { ...@@ -73,136 +79,113 @@ public class FileProcessTask {
private String tencentSecretId; private String tencentSecretId;
private String tencentSecretKey; private String tencentSecretKey;
private String tencentAdminUserId; private String tencentAdminUserId;
private String llmApiAddr;
private Boolean finalRetry; //表示是兜底重试机制
private MeetingInfoMapper meetingInfoMapper; private MeetingInfoMapper meetingInfoMapper;
private MinioUtils minioUtils; private MinioUtils minioUtils;
private EmailSender emailSender; private EmailSender emailSender;
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
// 实际处理逻辑 // 实际处理逻辑
public void process() { public void process() {
boolean isSuccess = false; boolean isSuccess = false;
while (retryCount <= MAX_RETRY && !isSuccess) { while (retryCount <= MAX_RETRY && !isSuccess) {
try {
Client client = new Client.Builder() Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId) .withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey) .withSecret(tencentSecretId,tencentSecretKey)
.build(); .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 { try {
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse = MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>()
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder); .eq(MeetingInfo::getMeetingId,meetingId)
V1MeetingsMeetingIdParticipantsGet200Response data = participantsResponse.getData(); .eq(subMeetingId != null, MeetingInfo::getSubMeetingId, subMeetingId));
String scheduleStartTime = data.getScheduleStartTime();
String scheduleEndTime = data.getScheduleEndTime(); //每场会议可能会分段录制,查出每个文件的转录记录后拼接
ZoneId zoneId = ZoneId.of("Asia/Shanghai"); StringBuffer recordTextBuffer = new StringBuffer();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd"); for (String recordFileId : recordFileIdList) {
String zonedDateTimeStart = Instant.ofEpochSecond(Long.valueOf(scheduleStartTime)).atZone(zoneId).format(df); //查询录制转写详情
// String zonedDateTimeEnd = Instant.ofEpochSecond(Long.valueOf(scheduleEndTime)).atZone(zoneId).format(df); RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
participantsMap.put("meeting_date",zonedDateTimeStart); .operatorId(tencentAdminUserId)
participantsMap.put("meeting_location","线上腾讯会议"); .operatorIdType("1")
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = data.getParticipants(); .build();
participantsMap.put("meeting_participants", RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse =
participants.stream() client.records().v1AddressesRecordFileIdGet(addressRequest,
.map(item->new String(Base64.getDecoder().decode(item.getUserName()))).distinct().collect(Collectors.joining("、"))); new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
Optional<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> host = participants.stream().filter(item -> hostRoleList.contains(item.getUserRole())).findFirst(); .timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
participantsMap.put("meeting_host",host.isPresent() ? new String(Base64.getDecoder().decode(host.get().getUserName())) : "未知主持人"); // 处理响应
} catch (Exception e) { if (addressResponse != null && addressResponse.getData() != null) {
e.printStackTrace(); log.info("Successfully got address for record file {}", recordFileId);
log.error(e.getMessage()); V1AddressesRecordFileIdGet200Response addressData = addressResponse.getData();
throw new RuntimeException(e);
} // 获取AI会议转录文件
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
//查询录制转写详情 addressData.getAiMeetingTranscripts();
RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId) if (transcripts != null && !transcripts.isEmpty()) {
.operatorId(tencentAdminUserId) log.info("Found {} AI meeting transcripts for record file {}",
.operatorIdType("1") transcripts.size(), recordFileId);
.build();
RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse = // 处理每个转录文件
client.records().v1AddressesRecordFileIdGet(addressRequest, for (V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner transcript : transcripts) {
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()))) String fileType = transcript.getFileType();
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L))); String downloadUrl = transcript.getDownloadAddress();
// 处理响应
if (addressResponse != null && addressResponse.getData() != null) { if ("txt".equalsIgnoreCase(fileType)) {
log.info("Successfully got address for record file {}", recordFileId); log.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
V1AddressesRecordFileIdGet200Response addressData = addressResponse.getData();
// 1. 下载文件
// 获取AI会议转录文件 byte[] fileData = downloadFile(downloadUrl);
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
addressData.getAiMeetingTranscripts(); // 2. 将二进制文件转换为文本
String recordTextContent = new String(fileData);
if (transcripts != null && !transcripts.isEmpty()) { // byte[] fileData = Files.readAllBytes(Paths.get("/20250514132555-中集AIGC项目例会-转写智能优化版.txt"));
log.info("Found {} AI meeting transcripts for record file {}", // String recordTextContent = new String(fileData);
transcripts.size(), recordFileId); if(StringUtils.isNotEmpty(recordTextContent.replaceAll("\\n","").trim())){
recordTextBuffer.append("\n\n");
// 处理每个转录文件 recordTextBuffer.append(recordTextContent);
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;
} }
} else {
// 3. 处理文件 (调用Claude API等) log.info("No AI meeting transcripts found for record file {}", recordFileId);
String processedResult = processWithClaude(recordTextContent);
// 4. 保存结果
saveResult(savePath, processedResult, participantsMap, fileData);
} }
} 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 { // 4. 保存结果
log.warn("Empty response for record file: {}", recordFileId); saveResult(savePath, processedResult, recordTextBuffer.toString().getBytes(StandardCharsets.UTF_8),meetingInfo);
}
isSuccess = true; isSuccess = true;
} catch (Exception e) { } catch (Exception e) {
// 异常处理 // 异常处理
retryCount++; retryCount++;
if (retryCount > MAX_RETRY) { if (retryCount > MAX_RETRY) {
log.error("达到最大重试次数: {}", recordFileId); log.error("达到最大重试次数:meetingId {}", meetingId);
throw new RuntimeException(e); //如果是兜底重试,最终还是失败了,设置会议的重试状态为已重试
} if(finalRetry){
// 指数退避 meetingInfoMapper.update(null,
try { new LambdaUpdateWrapper<MeetingInfo>()
Thread.sleep((long) Math.pow(2, retryCount) * 1000); .eq(MeetingInfo::getMeetingId,meetingId)
} catch (InterruptedException ie) { .eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
Thread.currentThread().interrupt(); .set(MeetingInfo::getGenerateRetry,Boolean.TRUE));
throw new RuntimeException("重试失败", ie); }
}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 { ...@@ -248,12 +231,13 @@ public class FileProcessTask {
private String processWithClaude(String textContent) { private String processWithClaude(String textContent) {
//将文件传送给大模型处理 //将文件传送给大模型处理
String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO"; 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"; String model = "anthropic.claude-3-5-sonnet-20240620-v1:0";
int maxTokens = 5000; int maxTokens = 5000;
//获取系统模板
MeetingRecordTemplate meetingRecordTemplate = meetingRecordTemplateMapper.selectById(1);
List<Message> messages = new ArrayList<>(); 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); messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议记录"); chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议记录");
messages.add(chatMessage); messages.add(chatMessage);
...@@ -266,7 +250,7 @@ public class FileProcessTask { ...@@ -266,7 +250,7 @@ public class FileProcessTask {
return ret; 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 nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss"));
String targetPath = path + nowTime + ".json"; // 改为.json扩展名; String targetPath = path + nowTime + ".json"; // 改为.json扩展名;
String targetFileName; String targetFileName;
...@@ -284,36 +268,51 @@ public class FileProcessTask { ...@@ -284,36 +268,51 @@ public class FileProcessTask {
log.info("json文件已保存到: {}", targetPath); log.info("json文件已保存到: {}", targetPath);
//将xml格式的内容转换为map,用于填充模板 //将xml格式的内容转换为map,用于填充模板
Map<String, Object> dataModel = convertXmlToMap(xml); 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); 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")) : "腾讯会议纪要"; meetingName = dataModel.get("meeting_name") != null ? String.valueOf(dataModel.get("meeting_name")) : "腾讯会议纪要";
targetFileName = meetingName + "_" + nowTime; targetFileName = meetingName + "_" + nowTime;
template.writeAndClose(new FileOutputStream(path + targetFileName + ".docx")); try (InputStream inputStream = resource.getInputStream()) {
byte[] recordXmlData = Files.readAllBytes(Paths.get(path + targetFileName + ".docx")); XWPFTemplate template = XWPFTemplate.compile(inputStream).render(dataModel);
minioUtils.upload(recordXmlPath,recordXmlData); 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) { } catch (Exception e) {
log.error("填充会议纪要失败: {}", e.getMessage(), e); log.error("填充会议纪要失败: {}", e.getMessage(), e);
throw new RuntimeException("填充会议纪要失败"); throw new RuntimeException("填充会议纪要失败");
} }
MeetingInfo meetingInfo = meetingInfoMapper.selectOne(new LambdaQueryWrapper<MeetingInfo>().eq(MeetingInfo::getMeetingId,meetingId));
Boolean isPushed; Boolean isPushed;
try { log.info("开始邮件推送------");
if(meetingInfo.getEmailPushAccess()){
log.info("用户允许邮件推送,准备推送邮件至{}------",meetingInfo.getEmail());
//邮件推送 //邮件推送
emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId); isPushed = emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",meetingId);
// emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId); // emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",meetingId);
// emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",recordFileId); // emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",meetingId);
isPushed = Boolean.TRUE; }else{
} catch (Exception e) { log.info("用户关闭了邮件推送,推送终止------");
isPushed = Boolean.FALSE; isPushed = Boolean.FALSE;
e.printStackTrace();
log.error(e.getMessage());
} }
meetingInfoMapper.update(meetingInfo, meetingInfoMapper.update(meetingInfo,
new LambdaUpdateWrapper<MeetingInfo>() new LambdaUpdateWrapper<MeetingInfo>()
.eq(MeetingInfo::getMeetingId,meetingId) .eq(MeetingInfo::getMeetingId,meetingId)
.eq(subMeetingId != null,MeetingInfo::getSubMeetingId,subMeetingId)
.set(MeetingInfo::getRecordContent,recordContentPath) .set(MeetingInfo::getRecordContent,recordContentPath)
.set(MeetingInfo::getRecordXml,recordXmlPath) .set(MeetingInfo::getRecordXml,recordXmlPath)
.set(MeetingInfo::getIsGenerated,Boolean.TRUE) .set(MeetingInfo::getIsGenerated,Boolean.TRUE)
...@@ -420,10 +419,11 @@ public class FileProcessTask { ...@@ -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, String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId,
MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender) { MeetingInfoMapper meetingInfoMapper, MinioUtils minioUtils, EmailSender emailSender, MeetingRecordTemplateMapper meetingRecordTemplateMapper,
this.recordFileId = recordFileId; String llmApiAddr, Boolean finalRetry) {
this.recordFileIdList = recordFileIdList;
this.savePath = savePath; this.savePath = savePath;
this.metadata = metadata; this.metadata = metadata;
this.tencentAppId = tencentAppId; this.tencentAppId = tencentAppId;
...@@ -436,5 +436,8 @@ public class FileProcessTask { ...@@ -436,5 +436,8 @@ public class FileProcessTask {
this.meetingInfoMapper = meetingInfoMapper; this.meetingInfoMapper = meetingInfoMapper;
this.minioUtils = minioUtils; this.minioUtils = minioUtils;
this.emailSender = emailSender; 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; ...@@ -8,5 +8,6 @@ import java.util.List;
@Mapper @Mapper
public interface AuthMapper { 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; package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.UserId; import com.cmeeting.pojo.UserId;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@Mapper @Mapper
public interface UserIdMapper { public interface UserIdMapper extends BaseMapper<UserId> {
void insertUsers (List<UserId> userIds); void insertUsers (List<UserId> userIds);
List<UserId> getUsers(); List<UserId> getUsers();
......
...@@ -7,7 +7,7 @@ import java.util.List; ...@@ -7,7 +7,7 @@ import java.util.List;
public interface WeComUserMapper extends BaseMapper<WeComUser> { public interface WeComUserMapper extends BaseMapper<WeComUser> {
List<WeComUser> getSameName() ; List<WeComUser> noBindUsers() ;
WeComUser selectById(Integer id); WeComUser selectById(Integer id);
int insert(WeComUser user); int insert(WeComUser user);
......
package com.cmeeting.mapper.secondary; package com.cmeeting.mapper.secondary;
import com.cmeeting.dto.UserDTO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper @Mapper
public interface SysUserSysMapper { public interface SysUserSysMapper {
String getCompanyEmail(String wid, String tenantId); 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; ...@@ -20,6 +20,8 @@ import java.time.LocalDateTime;
@Accessors(chain = true) @Accessors(chain = true)
@TableName("cmt_meeting_info") @TableName("cmt_meeting_info")
public class MeetingInfo implements Serializable { public class MeetingInfo implements Serializable {
private static final long serialVersionUID = -26238487532381000L;
/** /**
* 主键id * 主键id
*/ */
...@@ -48,12 +50,17 @@ public class MeetingInfo implements Serializable { ...@@ -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 { ...@@ -78,6 +85,14 @@ public class MeetingInfo implements Serializable {
*/ */
private Boolean isPushed; private Boolean isPushed;
/** /**
* 会议纪要重新生成标识
*/
private Boolean generateRetry;
/**
* 邮件推送重试标识
*/
private Boolean pushRetry;
/**
* 同步时间 * 同步时间
*/ */
private LocalDateTime syncTime; private LocalDateTime syncTime;
...@@ -91,4 +106,12 @@ public class MeetingInfo implements Serializable { ...@@ -91,4 +106,12 @@ public class MeetingInfo implements Serializable {
* 纪要xml * 纪要xml
*/ */
private String recordXml; 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; 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 * 主键ID
*/ */
@TableId(type = IdType.AUTO)
private Integer id; private Integer id;
/** /**
...@@ -25,26 +37,4 @@ public class TencentMeetingUser { ...@@ -25,26 +37,4 @@ public class TencentMeetingUser {
* 是否是重名用户(1:重名, 0:不重名) * 是否是重名用户(1:重名, 0:不重名)
*/ */
private String isrepeatName; 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; package com.cmeeting.pojo;
import lombok.Data; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import lombok.experimental.Accessors;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("userid")
public class UserId { public class UserId {
/** /**
* 主键ID * 主键ID
...@@ -24,23 +32,4 @@ public class UserId { ...@@ -24,23 +32,4 @@ public class UserId {
*/ */
private String tid; 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; 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 com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data; import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable; import java.io.Serializable;
@Data @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 Integer id;
private String userName; private String userName;
private String userId; private String userId;
private String isRepeatName; private String isRepeatName;
private String email; 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 { ...@@ -11,7 +11,7 @@ public class FileProcessCallbackHandler {
// 单个任务完成回调 // 单个任务完成回调
public void onComplete(FileProcessTask task) { public void onComplete(FileProcessTask task) {
// 可以记录任务状态、发送通知等 // 可以记录任务状态、发送通知等
log.info("任务处理完成: {}", task.getRecordFileId()); log.info("任务处理完成: meetingId {}", task.getMeetingId());
// 更新数据库状态等 // 更新数据库状态等
// taskRepository.updateStatus(task.getId(), "COMPLETED"); // taskRepository.updateStatus(task.getId(), "COMPLETED");
......
package com.cmeeting.service; package com.cmeeting.service;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.email.EmailSender; import com.cmeeting.email.EmailSender;
import com.cmeeting.job.EmailPushTask;
import com.cmeeting.job.FileProcessTask; import com.cmeeting.job.FileProcessTask;
import com.cmeeting.mapper.primary.MeetingInfoMapper; import com.cmeeting.mapper.primary.MeetingInfoMapper;
import com.cmeeting.mapper.primary.MeetingRecordTemplateMapper;
import com.cmeeting.pojo.UserId;
import com.cmeeting.util.MinioUtils; import com.cmeeting.util.MinioUtils;
import com.cmeeting.vo.TencentMeetingVO; import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
...@@ -37,24 +41,33 @@ public class FileProcessProducer { ...@@ -37,24 +41,33 @@ public class FileProcessProducer {
private String tencentSecretKey; private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}") @Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId; private String tencentAdminUserId;
@Value(value = "${llm.api-addr}")
private String llmApiAddr;
@Resource @Resource
private MeetingInfoMapper meetingInfoMapper; private MeetingInfoMapper meetingInfoMapper;
@Resource @Resource
private MeetingRecordTemplateMapper meetingRecordTemplateMapper;
@Resource
private MinioUtils minioUtils; private MinioUtils minioUtils;
@Resource @Resource
private EmailSender emailSender; 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<>(); List<Future<?>> futures = new ArrayList<>();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) { for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务 // 为每个URL创建任务
FileProcessTask task = new FileProcessTask( FileProcessTask task = new FileProcessTask(
recordFile.getRecordFileId(), recordFile.getRecordFileIdList(),
recordFile.getMeetingId(), recordFile.getMeetingId(),
recordFile.getSubMeetingId(), recordFile.getSubMeetingId(),
"/save/", baseSavePath,
Collections.emptyMap(), Collections.emptyMap(),
tencentAppId, tencentAppId,
tencentSdkId, tencentSdkId,
...@@ -63,7 +76,10 @@ public class FileProcessProducer { ...@@ -63,7 +76,10 @@ public class FileProcessProducer {
tencentAdminUserId, tencentAdminUserId,
meetingInfoMapper, meetingInfoMapper,
minioUtils, minioUtils,
emailSender emailSender,
meetingRecordTemplateMapper,
llmApiAddr,
finalRetry
); );
// 提交任务到线程池 // 提交任务到线程池
...@@ -78,6 +94,35 @@ public class FileProcessProducer { ...@@ -78,6 +94,35 @@ public class FileProcessProducer {
// 可以添加一个监控线程来检查所有任务完成情况 // 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures); 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) { private void monitorTaskCompletion(List<Future<?>> futures) {
new Thread(() -> { 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; package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.dto.UserDTO;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.UserId;
import com.cmeeting.vo.TencentMeetingVO; import com.cmeeting.vo.TencentMeetingVO;
import java.util.List; import java.util.List;
...@@ -11,6 +13,10 @@ public interface TecentMeetingService extends IService<TencentMeetingUser> { ...@@ -11,6 +13,10 @@ public interface TecentMeetingService extends IService<TencentMeetingUser> {
void doUsers(); 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; ...@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import java.util.List; import java.util.List;
import java.util.Map;
public interface WeComService extends IService<WeComUser> { public interface WeComService extends IService<WeComUser> {
...@@ -12,4 +13,10 @@ public interface WeComService extends IService<WeComUser> { ...@@ -12,4 +13,10 @@ public interface WeComService extends IService<WeComUser> {
void doUsers() throws Exception; void doUsers() throws Exception;
String getToken(); 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; package com.cmeeting.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.MeetingInfoMapper;
import com.cmeeting.mapper.primary.TecentMeetingMapper; 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.MeetingInfo;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.pojo.UserId;
import com.cmeeting.service.TecentMeetingService; import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.util.RedisUtils;
import com.cmeeting.vo.TencentMeetingVO; import com.cmeeting.vo.TencentMeetingVO;
import com.tencentcloudapi.wemeet.Client; import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder; import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
...@@ -14,44 +20,35 @@ import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator; ...@@ -14,44 +20,35 @@ import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException; import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException; import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi; import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200ResponseMeetingInfoListInnerCurrentCoHostsInner; import com.tencentcloudapi.wemeet.service.meetings.model.*;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi; import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.*; 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 lombok.extern.slf4j.Slf4j;
import okhttp3.Headers; 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.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.Instant; import java.time.*;
import java.time.LocalDateTime; import java.util.*;
import java.time.ZoneId; import java.util.concurrent.ExecutorService;
import java.time.ZonedDateTime; import java.util.concurrent.Executors;
import java.util.ArrayList; import java.util.concurrent.TimeUnit;
import java.util.Base64; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.fetchUsersInBatches;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNamesTecent;
@Service @Service
@Slf4j @Slf4j
public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TecentMeetingService { public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TecentMeetingService {
...@@ -59,9 +56,18 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -59,9 +56,18 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
private TecentMeetingMapper tecentMeetingMapper; private TecentMeetingMapper tecentMeetingMapper;
@Resource @Resource
private MeetingInfoMapper meetingInfoMapper; 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 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 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}") @Value(value = "${tencent.appId}")
private String tencentAppId; private String tencentAppId;
...@@ -73,6 +79,10 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -73,6 +79,10 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
private String tencentSecretKey; private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}") @Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId; private String tencentAdminUserId;
@Value(value = "${permission.applicationId}")
private String permissionApplicationId;
@Value(value = "${permission.tenantId}")
private String permissionTenantId;
@Override @Override
public void batchInsert(List<TencentMeetingUser> users) { public void batchInsert(List<TencentMeetingUser> users) {
...@@ -82,26 +92,21 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -82,26 +92,21 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
@Override @Override
public void doUsers() { public void doUsers() {
/** // 获取到全部用户
* 腾讯会议通过通讯录获取员工信息 List<TencentMeetingUser> users = fetchUsersInBatches();;
*/ // 检查重名并设置标志
// 1. 构造client客户端
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId, tencentSecretKey)
.build();
// 2.获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches(client, 3);;
// 3. 检查重名并设置标志
markDuplicateNamesTecent(users); 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 @Override
public List<TencentMeetingVO.RecordFile> getMeetingFiles() { public List<TencentMeetingVO.RecordFile> getMeetingFiles(List<UserDTO> accessUserIds) {
Client client = new Client.Builder() Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId) .withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey) .withSecret(tencentSecretId,tencentSecretKey)
...@@ -112,7 +117,7 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -112,7 +117,7 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
// 查询近两天的会议录制列表 // 查询近两天的会议录制列表
try { try {
ZonedDateTime now = ZonedDateTime.now(); 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()); String endTime = String.valueOf(now.toEpochSecond());
AtomicInteger currentPage = new AtomicInteger(1); AtomicInteger currentPage = new AtomicInteger(1);
Long totalPage = 1L; Long totalPage = 1L;
...@@ -129,7 +134,6 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -129,7 +134,6 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
.pageSize("20") .pageSize("20")
.page(String.valueOf(currentPage.getAndIncrement())) .page(String.valueOf(currentPage.getAndIncrement()))
.mediaSetType("0") .mediaSetType("0")
.queryRecordType("0")
.build(); .build();
RecordsApi.ApiV1RecordsGetResponse response = RecordsApi.ApiV1RecordsGetResponse response =
client.records().v1RecordsGet(request, new JWTAuthenticator.Builder() client.records().v1RecordsGet(request, new JWTAuthenticator.Builder()
...@@ -149,8 +153,11 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -149,8 +153,11 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
return null; return null;
} }
for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) { for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) {
//会议没结束,跳过
if(meeting.getState() != 3) continue; if(meeting.getState() != 3) continue;
log.info("【会议检索】转录文件的meetingId->{},recordFileId->{}",meeting.getMeetingId(),meeting.getMeetingRecordId());
//查询会议详情 //查询会议详情
String meetingId = meeting.getMeetingId(); String meetingId = meeting.getMeetingId();
String subMeetingId = null; String subMeetingId = null;
...@@ -175,27 +182,85 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -175,27 +182,85 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
//1:周期性会议 //1:周期性会议
Long meetingType = meetingInfo.getMeetingType(); Long meetingType = meetingInfo.getMeetingType();
if(meetingType.intValue() == 1){ 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的记录,跳过同步 //如果数据库中已有相同会议id的记录,跳过同步
if(!meetingIds.contains(meetingId)){ 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()) MeetingInfo meetingItem = MeetingInfo.builder().meetingId(meetingId).meetingCode(meetingInfo.getMeetingCode())
.subject(meetingInfo.getSubject()) .subject(meetingInfo.getSubject())
.startTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getStartTime())), ZoneId.systemDefault())) .startTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getStartTime())), ZoneId.systemDefault()))
.endTime(LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.valueOf(meetingInfo.getEndTime())), 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()) .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(); .build();
recordFileUrlList.add(recordFileItem);
meetingSaveList.add(meetingItem); 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) { } catch (ClientException e) {
...@@ -207,7 +272,16 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -207,7 +272,16 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
} }
} }
if(meetingSaveList.size() > 0){ 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) { } catch (Exception e) {
log.error(e.getMessage()); log.error(e.getMessage());
...@@ -217,6 +291,71 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -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 httpMethod http请求方法 GET/POST/PUT等
* @param requestUri 请求uri,eg:/v1/meetings * @param requestUri 请求uri,eg:/v1/meetings
...@@ -285,4 +424,174 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te ...@@ -285,4 +424,174 @@ public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,Te
} }
return new String(buf); 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; 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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.WeComUserMapper; import com.cmeeting.mapper.primary.WeComUserMapper;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import com.cmeeting.service.WeComService; import com.cmeeting.service.WeComService;
import com.google.gson.Gson; import com.cmeeting.util.RedisUtils;
import com.google.gson.JsonArray; import com.tencentcloudapi.wemeet.Client;
import com.google.gson.JsonObject; import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import okhttp3.OkHttpClient; import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import okhttp3.Request; import com.tencentcloudapi.wemeet.core.exception.ClientException;
import okhttp3.Response; import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import org.json.JSONObject; import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import org.springframework.beans.factory.annotation.Autowired; import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsGet200ResponseMeetingInfoListInner;
import okhttp3.*;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Collection; import java.security.SecureRandom;
import java.util.List; import java.util.*;
import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.convertJsonToWeComUsers;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNames;
@Service @Service
public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> implements WeComService { 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_ID = "wx34544d057db97ffd";
private static final String CORP_SECRET = "7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0"; private static final String CORP_SECRET = "7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0";
private static final String TOKEN_KEY = "ZHONGJI_WECOM_KEY";
@Resource @Resource
private WeComUserMapper weComUserMapper; private WeComUserMapper weComUserMapper;
@Resource
private RedisUtils redisUtils;
@Override @Override
public void batchInsert(List<WeComUser> users) { public void batchInsert(List<WeComUser> users) {
...@@ -46,35 +48,42 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -46,35 +48,42 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
@Override @Override
public void doUsers() throws Exception { public void doUsers() throws Exception {
// 示例:获取部门ID为6的用户列表 //获取所有可见范围下的部门
int departmentId = 6; JSONArray departmentList = getDepartmentList();
JsonObject departmentList = getDepartmentList(); Iterator<Object> iterator = departmentList.iterator();
JsonObject result = getUserListByDepartment(departmentId); List<WeComUser> users = new ArrayList<>();
// 处理返回结果 while (iterator.hasNext()){
if (result.get("errcode").getAsInt() == 0) { JSONObject department = (JSONObject) iterator.next();
System.out.println("企微获取用户列表成功:"); String departmentId = String.valueOf(department.get("id"));
JsonArray userList = result.getAsJsonArray("userlist"); //获取部门下的所有人员
JSONArray userList = getUserListByDepartment(departmentId);
for (int i = 0; i < userList.size(); i++) { for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject(); JSONObject userJson = (JSONObject)userList.get(i);
System.out.println("姓名: " + user.get("name").getAsString() + System.out.println("姓名: " + userJson.get("name") +
", UserID: " + user.get("userid").getAsString()); ", 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 @Override
public String getToken() { public String getToken() {
if(redisUtils.hasKey(TOKEN_KEY)){
return String.valueOf(redisUtils.get(TOKEN_KEY));
}
//获取token //获取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=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo";
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET; String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET;
String accessToken = ""; String accessToken;
OkHttpClient client = new OkHttpClient(); OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
...@@ -84,8 +93,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -84,8 +93,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String responseBody = response.body().string(); String responseBody = response.body().string();
JSONObject jsonResponse = new JSONObject(responseBody); JSONObject jsonResponse = JSONObject.parseObject(responseBody);
accessToken = jsonResponse.getString("access_token"); accessToken = jsonResponse.getString("access_token");
redisUtils.set(TOKEN_KEY,accessToken,Integer.valueOf(jsonResponse.getString("expires_in")) - 300);
} else { } else {
throw new RuntimeException("Failed to fetch token. HTTP Code: " + response.code()); throw new RuntimeException("Failed to fetch token. HTTP Code: " + response.code());
} }
...@@ -95,13 +105,95 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -95,13 +105,95 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
return accessToken; 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 * @return 包含用户列表的JsonObject
* @throws Exception * @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(); String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=" + getToken();
URL url = new URL(urlStr); URL url = new URL(urlStr);
...@@ -119,9 +211,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -119,9 +211,9 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
} }
in.close(); in.close();
// 使用Gson解析JSON响应 JSONObject jsonResult = JSONObject.parseObject(response.toString());
Gson gson = new Gson(); JSONArray departmentArray = (JSONArray)jsonResult.get("department_id");
return gson.fromJson(response.toString(), JsonObject.class); return departmentArray;
} else { } else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode); throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
} }
...@@ -134,8 +226,8 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -134,8 +226,8 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
* @return 包含用户列表的JsonObject * @return 包含用户列表的JsonObject
* @throws Exception * @throws Exception
*/ */
public JsonObject getUserListByDepartment ( int departmentId) throws Exception { public JSONArray getUserListByDepartment ( String departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId; String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=" + getToken() + "&department_id=" + departmentId;
URL url = new URL(urlStr); URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
...@@ -152,11 +244,29 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i ...@@ -152,11 +244,29 @@ public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> i
} }
in.close(); in.close();
// 使用Gson解析JSON响应 JSONObject jsonResult = JSONObject.parseObject(response.toString());
Gson gson = new Gson(); JSONArray userList = (JSONArray)jsonResult.get("userlist");
return gson.fromJson(response.toString(), JsonObject.class); return userList;
} else { } else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode); 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 { ...@@ -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){ public InputStream getFile(MinioClient minioClient, String fileName){
try{ try{
InputStream inputStream = minioClient.getObject(bucketName, fileName); 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; ...@@ -9,8 +9,6 @@ import com.google.gson.JsonElement;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.cmeeting.WeComAndTencentMeeting.getUserListByDepartment;
public class WeComUserService { public class WeComUserService {
// 假设这是你的MyBatis Mapper // 假设这是你的MyBatis Mapper
...@@ -21,23 +19,6 @@ public class WeComUserService { ...@@ -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列表 * 将企业微信返回的JSON转换为WeComUser列表
*/ */
private List<WeComUser> convertJsonToWeComUsers(JsonObject json) { private List<WeComUser> convertJsonToWeComUsers(JsonObject json) {
......
...@@ -6,6 +6,8 @@ import lombok.Builder; ...@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.List;
@Data @Data
@Builder @Builder
public class TencentMeetingVO { public class TencentMeetingVO {
...@@ -30,6 +32,6 @@ public class TencentMeetingVO { ...@@ -30,6 +32,6 @@ public class TencentMeetingVO {
public static class RecordFile{ public static class RecordFile{
private String meetingId;//如果是周期会议,这个id表示主会议 private String meetingId;//如果是周期会议,这个id表示主会议
private String subMeetingId;//如果是周期会议,这个id表示子会议 private String subMeetingId;//如果是周期会议,这个id表示子会议
private String recordFileId; private List<String> recordFileIdList;
} }
} }
\ No newline at end of file
server: server:
port: 8080 port: 8080
# ???????? ############################################################## minio
# application.yml MINIO_ADDRESS: http://192.168.10.154:9000
# spring.datasource.url=jdbc:mysql://192.168.10.155:3306/cmeeting?useSSL=false&characterEncoding=utf8 MINIO_BUCKET: zhongji
# spring.datasource.username=root MINIO_USERNAME: minio
# spring.datasource.password=qizhi123 MINIO_PASSWORD: minio123
# spring.datasource.driver-class-name=com.mysql.jdbc.Driver ############################################################## 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: spring:
datasource: datasource:
...@@ -34,12 +56,23 @@ spring: ...@@ -34,12 +56,23 @@ spring:
username: root username: root
password: 123456 password: 123456
driver-class-name: com.mysql.jdbc.Driver 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 ??
# mybatis.mapper-locations=classpath:mapper/primary/*.xml # mybatis.mapper-locations=classpath:mapper/primary/*.xml
mybatis.type-aliases-package: com.cmeeting.pojo\ mybatis.type-aliases-package: com.cmeeting.pojo\
# ??????
mybatis: mybatis:
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
...@@ -52,19 +85,6 @@ global-config: ...@@ -52,19 +85,6 @@ global-config:
db-config: db-config:
column-underline: true 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服务所在地址
minio.endpoint: ${MINIO_ADDRESS} minio.endpoint: ${MINIO_ADDRESS}
#存储桶名称 #存储桶名称
...@@ -73,33 +93,28 @@ minio.bucketName: ${MINIO_BUCKET} ...@@ -73,33 +93,28 @@ minio.bucketName: ${MINIO_BUCKET}
minio.accessKey: ${MINIO_USERNAME} minio.accessKey: ${MINIO_USERNAME}
#访问的秘钥 #访问的秘钥
minio.secretKey: ${MINIO_PASSWORD} 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: tencent:
appId: 210468336 appId: ${TENCENT_APPID}
sdkId: 28790143843 sdkId: ${TENCENT_SDKID}
secretId: 0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL secretId: ${TENCENT_SECRETID}
secretKey: gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy secretKey: ${TENCENT_SECRETKEY}
admin.userId: woaJARCQAAftcvU6GGoOn66rdSZ4IrOA admin.userId: ${TENCENT_ADMIN_USERID}
meeting: meeting:
token: QQZNb7xWQB47MpZF4C2DFAkv8 token: QQZNb7xWQB47MpZF4C2DFAkv8
aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm aesKey: agy6ALUePp34lljWz1uIQWa7yQq3dgxxQNmfaN9GROm
email: email:
sender: cmeeting_assistant@cimc.com sender: ${EMAIL_SENDER}
sender-pwd: scyou@xih45g6@xih4 sender-pwd: ${EMAIL_SENDER_PWD}
smtp-host: smtp.office365.com smtp-host: ${EMAIL_SMTP_HOST}
############################################################## tencent meeting
llm:
api-addr: ${LLM_API_ADDR}
permission:
applicationId: ${PERMISSION_APPLiCATION_ID}
tenantId: ${PERMISSION_TENANT_ID}
logging: logging:
level: level:
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!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"> <mapper namespace="com.cmeeting.mapper.primary.AuthMapper">
<select id="getAuthByTargrtId" resultType="com.cmeeting.pojo.CoreModulePermissions"> <select id="getAuthByTargetId" resultType="com.cmeeting.pojo.CoreModulePermissions">
SELECT SELECT
id, id,
type, type,
...@@ -16,7 +16,8 @@ ...@@ -16,7 +16,8 @@
FROM FROM
core_module_permissions core_module_permissions
WHERE WHERE
target_id = #{targetId} user_type = 2
and target_id = #{targetId}
<if test="tenantId != null and tenantId != ''"> <if test="tenantId != null and tenantId != ''">
AND tenant_id = #{tenantId} AND tenant_id = #{tenantId}
</if> </if>
......
...@@ -2,16 +2,17 @@ ...@@ -2,16 +2,17 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!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"> <mapper namespace="com.cmeeting.mapper.primary.MeetingInfoMapper">
<insert id="batchInsert" parameterType="list"> <insert id="batchInsert" parameterType="list">
INSERT IGNORE INTO cmt_meeting_info (subject, meeting_id, meeting_code, host_id, participant_user_ids, start_time, 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) 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 VALUES
<foreach collection="meetingSaveList" item="meeting" separator=","> <foreach collection="meetingSaveList" item="meeting" separator=",">
( (
#{meeting.subject}, #{meeting.subject},
#{meeting.meetingId}, #{meeting.meetingId},
#{meeting.meetingCode}, #{meeting.meetingCode},
#{meeting.hostId}, #{meeting.host},
#{meeting.participantUserIds}, #{meeting.participantUsers},
#{meeting.startTime}, #{meeting.startTime},
#{meeting.endTime}, #{meeting.endTime},
#{meeting.isGenerated}, #{meeting.isGenerated},
...@@ -20,7 +21,11 @@ end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id, ...@@ -20,7 +21,11 @@ end_time, is_generated, email_push_access, is_pushed, sync_time, sub_meeting_id,
#{meeting.syncTime}, #{meeting.syncTime},
#{meeting.subMeetingId}, #{meeting.subMeetingId},
#{meeting.recordContent}, #{meeting.recordContent},
#{meeting.recordXml} #{meeting.recordXml},
#{meeting.generateRetry},
#{meeting.pushRetry},
#{meeting.recordFileId},
#{meeting.email}
) )
</foreach> </foreach>
</insert> </insert>
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
<!-- 批量插入用户ID信息 --> <!-- 批量插入用户ID信息 -->
<insert id="insertUsers" parameterType="List"> <insert id="insertUsers" parameterType="List">
INSERT INTO userid ( INSERT INTO userid (
userName, user_name,
Wid, wid,
Tid tid
) VALUES ) VALUES
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
( (
...@@ -19,14 +19,14 @@ ...@@ -19,14 +19,14 @@
<!-- 更新 Tid 字段 --> <!-- 更新 Tid 字段 -->
<update id="updateUser" parameterType="com.cmeeting.pojo.UserId"> <update id="updateUser" parameterType="com.cmeeting.pojo.UserId">
UPDATE userid UPDATE userid
SET Tid = #{tid} SET tid = #{tid}
WHERE wid = #{wid} WHERE wid = #{wid}
</update> </update>
<select id="getUsers" resultType="com.cmeeting.pojo.UserId"> <select id="getUsers" resultType="com.cmeeting.pojo.UserId">
select * from userid select * from userid
</select> </select>
<select id="getWidByTid" resultType="java.lang.String"> <select id="getWidByTid" resultType="java.lang.String">
select Wid from userid WHERE Tid = #{operatorUserId} select wid from userid WHERE Tid = #{operatorUserId}
</select> </select>
</mapper> </mapper>
\ No newline at end of file
...@@ -30,8 +30,10 @@ ...@@ -30,8 +30,10 @@
<select id="getAlluser" resultType="com.cmeeting.pojo.WeComUser"> <select id="getAlluser" resultType="com.cmeeting.pojo.WeComUser">
SELECT * FROM user_wecom SELECT * FROM user_wecom
</select> </select>
<select id="getSameName" resultType="com.cmeeting.pojo.WeComUser"> <select id="noBindUsers" resultType="com.cmeeting.pojo.WeComUser">
SELECT * FROM user_wecom SELECT t1.*
WHERE isrepeat_name = "1" FROM user_wecom t1
left join userid t2 on t1.user_id = t2.Wid
WHERE t2.id is null
</select> </select>
</mapper> </mapper>
\ No newline at end of file
...@@ -9,4 +9,31 @@ ...@@ -9,4 +9,31 @@
AND tenant_id = #{tenantId} AND tenant_id = #{tenantId}
LIMIT 1 LIMIT 1
</select> </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> </mapper>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论