Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
cmeeting
概览
概览
详情
活动
周期分析
版本库
存储库
文件
提交
分支
标签
贡献者
分支图
比较
统计图
问题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程表
图表
维基
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
Issue Boards
Open sidebar
翟斌
cmeeting
Commits
8a23a94c
提交
8a23a94c
authored
5月 21, 2025
作者:
duanxincheng
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
纪要生成服务优化
父级
0a34c27f
隐藏空白字符变更
内嵌
并排
正在显示
24 个修改的文件
包含
1238 行增加
和
1015 行删除
+1238
-1015
pom.xml
+16
-8
src/main/java/com/cmeeting/TencentMeetingCallbackController.java
+11
-11
src/main/java/com/cmeeting/config/ThreadPoolConfig.java
+43
-0
src/main/java/com/cmeeting/controller/TencentMeetingController.java
+7
-20
src/main/java/com/cmeeting/controller/UnifiedController.java
+11
-8
src/main/java/com/cmeeting/controller/WeComcontroller.java
+1
-61
src/main/java/com/cmeeting/email/EmailSender.java
+97
-70
src/main/java/com/cmeeting/job/CmeetingJob.java
+73
-589
src/main/java/com/cmeeting/job/CmeetingJob2.java
+0
-231
src/main/java/com/cmeeting/job/FileProcessTask.java
+364
-0
src/main/java/com/cmeeting/mapper/primary/TecentMeetingMapper.java
+2
-2
src/main/java/com/cmeeting/mapper/primary/WeComUserMapper.java
+4
-4
src/main/java/com/cmeeting/pojo/TencentMeetingRecord.java
+26
-0
src/main/java/com/cmeeting/pojo/WeComUser.java
+7
-1
src/main/java/com/cmeeting/service/FileProcessCallbackHandler.java
+26
-0
src/main/java/com/cmeeting/service/FileProcessProducer.java
+82
-0
src/main/java/com/cmeeting/service/TecentMeetingService.java
+8
-1
src/main/java/com/cmeeting/service/WeComService.java
+6
-1
src/main/java/com/cmeeting/service/impl/TecentMeetingServiceImpl.java
+247
-2
src/main/java/com/cmeeting/service/impl/WeComServiceImpl.java
+144
-2
src/main/java/com/cmeeting/vo/TencentMeetingVO.java
+36
-0
src/main/resources/application.properties
+20
-2
src/main/resources/mapper/primary/TecentMeetingMapper.xml
+2
-0
src/main/resources/mapper/primary/WeComUserMapper.xml
+5
-2
没有找到文件。
pom.xml
浏览文件 @
8a23a94c
...
@@ -31,6 +31,7 @@
...
@@ -31,6 +31,7 @@
<maven.compiler.source>
1.8
</maven.compiler.source>
<maven.compiler.source>
1.8
</maven.compiler.source>
<maven.compiler.target>
1.8
</maven.compiler.target>
<maven.compiler.target>
1.8
</maven.compiler.target>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
<mybatis-plus.version>
3.3.0
</mybatis-plus.version>
</properties>
</properties>
<dependencyManagement>
<dependencyManagement>
<dependencies>
<dependencies>
...
@@ -246,11 +247,11 @@
...
@@ -246,11 +247,11 @@
</dependency>
</dependency>
<dependency
>
<!-- <dependency>--
>
<groupId>
org.mybatis.spring.boot
</groupId
>
<!-- <groupId>org.mybatis.spring.boot</groupId>--
>
<artifactId>
mybatis-spring-boot-starter
</artifactId
>
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>--
>
<version>
2.3.0
</version>
<!-- 请使用最新版本
-->
<!-- <version>2.3.0</version> <!– 请使用最新版本 –>
-->
</dependency
>
<!-- </dependency>--
>
<dependency>
<dependency>
<groupId>
mysql
</groupId>
<groupId>
mysql
</groupId>
<artifactId>
mysql-connector-java
</artifactId>
<artifactId>
mysql-connector-java
</artifactId>
...
@@ -293,10 +294,11 @@
...
@@ -293,10 +294,11 @@
</dependency>
</dependency>
<!-- Word 文档操作 -->
<!-- Word 文档操作 -->
<dependency>
<dependency>
<groupId>
org.apache.poi
</groupId>
<groupId>
com.deepoove
</groupId>
<artifactId>
poi-
ooxm
l
</artifactId>
<artifactId>
poi-
t
l
</artifactId>
<version>
5.2.3
</version>
<version>
1.12.2
</version>
</dependency>
</dependency>
...
@@ -305,6 +307,12 @@
...
@@ -305,6 +307,12 @@
<artifactId>
commonmark
</artifactId>
<artifactId>
commonmark
</artifactId>
<version>
0.17.1
</version>
<!-- 可升级至最新稳定版 -->
<version>
0.17.1
</version>
<!-- 可升级至最新稳定版 -->
</dependency>
</dependency>
<dependency>
<groupId>
com.baomidou
</groupId>
<artifactId>
mybatis-plus-boot-starter
</artifactId>
<version>
${mybatis-plus.version}
</version>
</dependency>
</dependencies>
</dependencies>
...
...
src/main/java/com/cmeeting/TencentMeetingCallbackController.java
浏览文件 @
8a23a94c
...
@@ -431,17 +431,17 @@ public class TencentMeetingCallbackController {
...
@@ -431,17 +431,17 @@ public class TencentMeetingCallbackController {
Thread
.
sleep
(
10000
);
Thread
.
sleep
(
10000
);
EmailSender
emailSender
=
new
EmailSender
();
EmailSender
emailSender
=
new
EmailSender
();
//response.getRawBody()
//response.getRawBody()
boolean
mailFlag
=
emailSender
.
sendEmailWithAttachment
(
emailAddress
,
//
boolean mailFlag = emailSender.sendEmailWithAttachment(emailAddress,
targetPath
,
//
targetPath,
"重要文件"
,
//
"重要文件",
"您好:\n"
+
//
"您好:\n" +
"\n"
+
//
"\n" +
" 附件为您本次会议的会议纪要,烦请下载查看,如需对会议纪要结果进行修改或查看历史会议,可点击下方链接。"
);
//
" 附件为您本次会议的会议纪要,烦请下载查看,如需对会议纪要结果进行修改或查看历史会议,可点击下方链接。");
if
(
mailFlag
)
{
//
if (mailFlag) {
logger
.
info
(
"邮件发送成功"
);
//
logger.info("邮件发送成功");
}
else
{
//
} else {
logger
.
error
(
"邮件发送失败"
);
//
logger.error("邮件发送失败");
}
//
}
}
}
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
logger
.
error
(
"下载文件时出错: {}"
,
e
.
getMessage
());
logger
.
error
(
"下载文件时出错: {}"
,
e
.
getMessage
());
...
...
src/main/java/com/cmeeting/config/ThreadPoolConfig.java
0 → 100644
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
config
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
;
import
java.util.concurrent.ThreadPoolExecutor
;
@Configuration
@Slf4j
public
class
ThreadPoolConfig
{
@Bean
(
"fileProcessExecutor"
)
public
ThreadPoolTaskExecutor
fileProcessExecutor
()
{
ThreadPoolTaskExecutor
executor
=
new
ThreadPoolTaskExecutor
();
// 核心线程数 (CPU密集型任务建议核心数+1)
executor
.
setCorePoolSize
(
4
);
// 固定核心线程数,避免动态获取CPU核心数
// 最大线程数
executor
.
setMaxPoolSize
(
4
);
// 队列容量
executor
.
setQueueCapacity
(
1000
);
// 线程名前缀
executor
.
setThreadNamePrefix
(
"file-process-"
);
// 明确设置所有必要属性
executor
.
setAllowCoreThreadTimeOut
(
false
);
// 核心线程不允许超时
executor
.
setWaitForTasksToCompleteOnShutdown
(
true
);
// 优雅关闭
executor
.
setAwaitTerminationSeconds
(
60
);
// 等待任务完成的最大时间
// 拒绝策略
executor
.
setRejectedExecutionHandler
(
new
ThreadPoolExecutor
.
CallerRunsPolicy
());
// 初始化前打印配置检查
log
.
info
(
"Initializing ThreadPool: core={}, max={}"
,
executor
.
getCorePoolSize
(),
executor
.
getMaxPoolSize
());
executor
.
initialize
();
return
executor
;
}
}
\ No newline at end of file
src/main/java/com/cmeeting/controller/TencentMeetingController.java
浏览文件 @
8a23a94c
...
@@ -2,6 +2,7 @@ package com.cmeeting.controller;
...
@@ -2,6 +2,7 @@ package com.cmeeting.controller;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.service.TecentMeetingService
;
import
com.cmeeting.service.TecentMeetingService
;
import
com.cmeeting.vo.TencentMeetingVO
;
import
com.tencentcloudapi.wemeet.Client
;
import
com.tencentcloudapi.wemeet.Client
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.GetMapping
;
...
@@ -13,32 +14,18 @@ import java.util.List;
...
@@ -13,32 +14,18 @@ import java.util.List;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.*;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.*;
@RestController
@RestController
@RequestMapping
(
"/tecent"
)
@RequestMapping
(
"/te
n
cent"
)
public
class
TencentMeetingController
{
public
class
TencentMeetingController
{
@Autowired
@Autowired
private
TecentMeetingService
tecentMeetingService
;
private
TecentMeetingService
tecentMeetingService
;
@GetMapping
(
"/add"
)
@GetMapping
(
"/add"
)
public
void
addUsers
()
throws
Exception
{
public
void
addUsers
()
{
dou
sers
();
tecentMeetingService
.
doU
sers
();
}
}
private
void
dousers
()
{
@GetMapping
(
"/getMeetingFiles"
)
/**
public
void
getMeetingFiles
(
TencentMeetingVO
.
TencentMeetingRequest
requestVO
){
* 腾讯会议通过通讯录获取员工信息
tecentMeetingService
.
getMeetingFiles
(
requestVO
);
*/
// 1. 构造client客户端
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
"211153201"
).
withSdkId
(
"28370276340"
)
.
withSecret
(
"BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6"
,
"3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO"
)
.
build
();
// 2.获取到全部用户
List
<
TencentMeetingUser
>
users
=
fetchUsersInBatches
(
client
,
3
);;
// 3. 检查重名并设置标志
markDuplicateNamesTecent
(
users
);
// 4. 批量插入数据库
tecentMeetingService
.
batchInsert
(
users
);
}
}
}
}
src/main/java/com/cmeeting/controller/UnifiedController.java
浏览文件 @
8a23a94c
...
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
...
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RestController
;
import
org.springframework.web.bind.annotation.RestController
;
import
org.json.JSONObject
;
import
org.json.JSONObject
;
import
javax.annotation.Resource
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.math.BigInteger
;
import
java.math.BigInteger
;
import
java.security.SecureRandom
;
import
java.security.SecureRandom
;
...
@@ -33,9 +34,9 @@ import java.util.stream.Collectors;
...
@@ -33,9 +34,9 @@ import java.util.stream.Collectors;
@RestController
@RestController
@RequestMapping
@RequestMapping
public
class
UnifiedController
{
public
class
UnifiedController
{
@
Autowired
@
Resource
private
WeComUserMapper
weComUserMapper
;
private
WeComUserMapper
weComUserMapper
;
@
Autowired
@
Resource
private
TecentMeetingMapper
tecentMeetingMapper
;
private
TecentMeetingMapper
tecentMeetingMapper
;
@Autowired
@Autowired
private
UserIdMapper
userIdMapper
;
private
UserIdMapper
userIdMapper
;
...
@@ -78,7 +79,7 @@ public class UnifiedController {
...
@@ -78,7 +79,7 @@ public class UnifiedController {
}
}
@GetMapping
(
"/insertTid"
)
@GetMapping
(
"/insertTid"
)
public
void
insertTid
()
{
public
void
insertTid
(
String
corpid
,
String
corpsecret
)
{
List
<
UserId
>
users
=
userIdMapper
.
getUsers
();
List
<
UserId
>
users
=
userIdMapper
.
getUsers
();
List
<
UserId
>
usersWithNullTid
=
new
ArrayList
<>();
List
<
UserId
>
usersWithNullTid
=
new
ArrayList
<>();
for
(
UserId
user
:
users
)
{
for
(
UserId
user
:
users
)
{
...
@@ -88,7 +89,7 @@ public class UnifiedController {
...
@@ -88,7 +89,7 @@ public class UnifiedController {
}
}
for
(
UserId
user
:
usersWithNullTid
)
{
for
(
UserId
user
:
usersWithNullTid
)
{
//获取企业微信token
//获取企业微信token
String
weComToken
=
getWeComToken
();
String
weComToken
=
getWeComToken
(
corpid
,
corpsecret
);
try
{
try
{
// 3.1 创建会议(传入用户的 wid)
// 3.1 创建会议(传入用户的 wid)
Map
<
String
,
String
>
meetingCodeAndMeetingid
=
createMeeting
(
user
.
getWid
(),
weComToken
);
Map
<
String
,
String
>
meetingCodeAndMeetingid
=
createMeeting
(
user
.
getWid
(),
weComToken
);
...
@@ -128,8 +129,8 @@ public class UnifiedController {
...
@@ -128,8 +129,8 @@ public class UnifiedController {
//todo 待测试
//todo 待测试
@GetMapping
(
"/sameNameInsertTid"
)
@GetMapping
(
"/sameNameInsertTid"
)
public
void
sameNameInsertTid
()
throws
IOException
{
public
void
sameNameInsertTid
(
String
corpid
,
String
corpsecret
)
throws
IOException
{
String
weComToken
=
getWeComToken
();
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
)
{
...
@@ -269,9 +270,11 @@ public class UnifiedController {
...
@@ -269,9 +270,11 @@ public class UnifiedController {
*
*
* @return
* @return
*/
*/
public
String
getWeComToken
()
{
@GetMapping
(
"/getWeComToken"
)
public
String
getWeComToken
(
String
corpid
,
String
corpsecret
)
{
//获取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="
+
corpid
+
"&corpsecret="
+
corpsecret
;
String
accessToken
=
""
;
String
accessToken
=
""
;
OkHttpClient
client
=
new
OkHttpClient
();
OkHttpClient
client
=
new
OkHttpClient
();
Request
request
=
new
Request
.
Builder
()
Request
request
=
new
Request
.
Builder
()
...
...
src/main/java/com/cmeeting/controller/WeComcontroller.java
浏览文件 @
8a23a94c
...
@@ -23,73 +23,13 @@ import static com.cmeeting.WeComAndTencentMeeting.*;
...
@@ -23,73 +23,13 @@ import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController
@RestController
@RequestMapping
(
"/wecom"
)
@RequestMapping
(
"/wecom"
)
public
class
WeComcontroller
{
public
class
WeComcontroller
{
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"
;
//动态获取token
@Autowired
@Autowired
private
WeComService
weComService
;
private
WeComService
weComService
;
@GetMapping
(
"/add"
)
@GetMapping
(
"/add"
)
public
void
addUsers
()
throws
Exception
{
public
void
addUsers
()
throws
Exception
{
dousers
();
weComService
.
doUsers
();
}
public
void
dousers
()
throws
Exception
{
// 示例:获取部门ID为6的用户列表
int
departmentId
=
6
;
JsonObject
result
=
getUserListByDepartment
(
departmentId
);
// 处理返回结果
if
(
result
.
get
(
"errcode"
).
getAsInt
()
==
0
)
{
System
.
out
.
println
(
"企微获取用户列表成功:"
);
JsonArray
userList
=
result
.
getAsJsonArray
(
"userlist"
);
for
(
int
i
=
0
;
i
<
userList
.
size
();
i
++)
{
JsonObject
user
=
userList
.
get
(
i
).
getAsJsonObject
();
System
.
out
.
println
(
"姓名: "
+
user
.
get
(
"name"
).
getAsString
()
+
", UserID: "
+
user
.
get
(
"userid"
).
getAsString
());
}
// 2. 转换为WeComUser集合
List
<
WeComUser
>
users
=
convertJsonToWeComUsers
(
result
);
// 3. 检查重名并设置标志
markDuplicateNames
(
users
);
// 4. 批量插入数据库
weComService
.
batchInsert
(
users
);
}
}
/**
* 根据企业微信部门ID获取用户列表
*
* @param departmentId 部门ID
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public
static
JsonObject
getUserListByDepartment
(
int
departmentId
)
throws
Exception
{
String
urlStr
=
BASE_URL
+
"?access_token="
+
ACCESS_TOKEN
+
"&department_id="
+
departmentId
;
URL
url
=
new
URL
(
urlStr
);
HttpURLConnection
conn
=
(
HttpURLConnection
)
url
.
openConnection
();
conn
.
setRequestMethod
(
"GET"
);
int
responseCode
=
conn
.
getResponseCode
();
if
(
responseCode
==
HttpURLConnection
.
HTTP_OK
)
{
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
conn
.
getInputStream
()));
String
inputLine
;
StringBuilder
response
=
new
StringBuilder
();
while
((
inputLine
=
in
.
readLine
())
!=
null
)
{
response
.
append
(
inputLine
);
}
in
.
close
();
// 使用Gson解析JSON响应
Gson
gson
=
new
Gson
();
return
gson
.
fromJson
(
response
.
toString
(),
JsonObject
.
class
);
}
else
{
throw
new
RuntimeException
(
"HTTP GET请求失败,错误码: "
+
responseCode
);
}
}
}
...
...
src/main/java/com/cmeeting/email/EmailSender.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
email
;
package
com
.
cmeeting
.
email
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Value
;
import
javax.activation.DataHandler
;
import
javax.activation.DataSource
;
import
javax.activation.FileDataSource
;
import
javax.mail.*
;
import
javax.mail.*
;
import
javax.mail.internet.*
;
import
javax.mail.internet.*
;
import
java.io.File
;
import
java.io.File
;
import
java.util.Properties
;
import
java.util.Properties
;
import
java.util.concurrent.atomic.AtomicInteger
;
@Slf4j
public
class
EmailSender
{
public
class
EmailSender
{
// 发件人邮箱配置(建议从配置文件读取,此处写死示例)
@Value
(
"${email.sender}"
)
private
static
final
String
FROM_EMAIL
=
"binzhai321@163.com"
;
private
String
SENDER
;
private
static
final
String
AUTH_CODE
=
"RXbRu3AdrCxX57ib"
;
// 授权码
@Value
(
"${email.sender.pwd}"
)
private
static
final
String
SMTP_HOST
=
"smtp.163.com"
;
private
String
EMAIL_PWD
;
private
static
final
int
SMTP_PORT
=
465
;
@Value
(
"${email.smtp.host}"
)
private
String
SMTP_HOST
;
private
static
final
Integer
MAX_RETRY
=
3
;
/**
/**
* 发送带附件的邮件
* @param toEmail 收件人
* @param toEmail 收件人邮箱
* @param subject 邮件主题
* @param filePath 附件本地路径(如 "C:/test.pdf")
* @param attachmentPath 附件路径
* @param subject 邮件主题(可选,默认值)
* @param recordFileId 转录文件ID
* @param text 邮件正文(可选,默认值)
* @return
* @return true发送成功,false发送失败
*/
*/
public
static
boolean
sendEmailWithAttachment
(
String
toEmail
,
String
filePath
,
public
boolean
sendEmailWithAttachment
(
String
toEmail
,
String
subject
,
String
attachmentPath
,
String
recordFileId
)
{
String
subject
,
String
text
)
{
// 邮件服务器配置
// 1. 参数校验
if
(
toEmail
==
null
||
toEmail
.
isEmpty
()
||
filePath
==
null
)
{
System
.
err
.
println
(
"参数错误:收件人或附件路径为空"
);
return
false
;
}
// 2. 设置默认邮件主题和正文
String
mailSubject
=
(
subject
!=
null
)
?
subject
:
""
;
String
mailText
=
(
text
!=
null
)
?
text
:
"请查收附件。"
;
// 3. 配置SMTP
Properties
props
=
new
Properties
();
Properties
props
=
new
Properties
();
props
.
put
(
"mail.smtp.host"
,
SMTP_HOST
);
props
.
put
(
"mail.smtp.host"
,
"smtp.office365.com"
);
props
.
put
(
"mail.smtp.port"
,
SMTP_PORT
);
props
.
put
(
"mail.smtp.auth"
,
"true"
);
props
.
put
(
"mail.smtp.auth"
,
"true"
);
props
.
put
(
"mail.smtp.ssl.enable"
,
"true"
);
// props.put("mail.smtp.port", "465");
// props.put("mail.smtp.ssl.enable", "true"); // 使用SSL
props
.
put
(
"mail.smtp.port"
,
"587"
);
props
.
put
(
"mail.smtp.starttls.enable"
,
"true"
);
// 使用TLS
// 或者使用SSL
// props.put("mail.smtp.port", "465");
// props.put("mail.smtp.ssl.enable", "true");
// 创建会话
Session
session
=
Session
.
getInstance
(
props
,
new
javax
.
mail
.
Authenticator
()
{
protected
PasswordAuthentication
getPasswordAuthentication
()
{
return
new
PasswordAuthentication
(
"cmeeting_assistant@cimc.com"
,
"scyou@xih45g6@xih4"
);
}
});
String
body
=
"您好:\n"
+
"\n"
+
" 附件为您本次会议的会议纪要,烦请下载查看"
;
AtomicInteger
retryCount
=
new
AtomicInteger
(
0
);
try
{
try
{
// 4. 创建会话
boolean
isSent
=
false
;
Session
session
=
Session
.
getInstance
(
props
,
new
Authenticator
()
{
while
(
retryCount
.
intValue
()
<
MAX_RETRY
&&
!
isSent
){
@Override
// 创建邮件消息
protected
PasswordAuthentication
getPasswordAuthentication
()
{
Message
message
=
new
MimeMessage
(
session
);
return
new
PasswordAuthentication
(
FROM_EMAIL
,
AUTH_CODE
);
message
.
setFrom
(
new
InternetAddress
(
"cmeeting_assistant@cimc.com"
));
message
.
setRecipients
(
Message
.
RecipientType
.
TO
,
InternetAddress
.
parse
(
toEmail
));
message
.
setSubject
(
subject
);
// 创建消息体部分(正文)
MimeBodyPart
messageBodyPart
=
new
MimeBodyPart
();
messageBodyPart
.
setText
(
body
);
// 创建多部分消息
Multipart
multipart
=
new
MimeMultipart
();
// 添加文本部分
multipart
.
addBodyPart
(
messageBodyPart
);
// 添加附件部分
if
(
attachmentPath
!=
null
&&
!
attachmentPath
.
isEmpty
())
{
MimeBodyPart
attachmentPart
=
new
MimeBodyPart
();
DataSource
source
=
new
FileDataSource
(
attachmentPath
);
attachmentPart
.
setDataHandler
(
new
DataHandler
(
source
));
attachmentPart
.
setFileName
(
new
File
(
attachmentPath
).
getName
());
multipart
.
addBodyPart
(
attachmentPart
);
}
}
});
// 设置完整消息内容
// 5. 构建邮件内容
message
.
setContent
(
multipart
);
MimeMessage
message
=
new
MimeMessage
(
session
);
message
.
setFrom
(
new
InternetAddress
(
FROM_EMAIL
));
// 发送邮件
message
.
setRecipient
(
Message
.
RecipientType
.
TO
,
new
InternetAddress
(
toEmail
));
Transport
.
send
(
message
);
message
.
setSubject
(
mailSubject
);
log
.
error
(
"邮件已成功发送: recordFileId->{}"
,
recordFileId
);
isSent
=
true
;
// 6. 添加正文和附件
}
Multipart
multipart
=
new
MimeMultipart
();
}
catch
(
MessagingException
e
)
{
//todo 邮件失败记录
// 文本正文
// 异常处理
MimeBodyPart
textPart
=
new
MimeBodyPart
();
retryCount
.
getAndIncrement
();
textPart
.
setText
(
mailText
);
if
(
retryCount
.
intValue
()
>
MAX_RETRY
)
{
multipart
.
addBodyPart
(
textPart
);
log
.
error
(
"邮件发送达到最大重试次数: recordFileId->{}"
,
recordFileId
);
throw
new
RuntimeException
(
e
);
// 附件
}
MimeBodyPart
attachmentPart
=
new
MimeBodyPart
();
// 指数退避
attachmentPart
.
attachFile
(
new
File
(
filePath
));
try
{
multipart
.
addBodyPart
(
attachmentPart
);
Thread
.
sleep
((
long
)
Math
.
pow
(
2
,
retryCount
.
intValue
())
*
1000
);
}
catch
(
InterruptedException
ie
)
{
message
.
setContent
(
multipart
);
Thread
.
currentThread
().
interrupt
();
throw
new
RuntimeException
(
"邮件发送重试失败"
,
ie
);
// 7. 发送邮件
}
Transport
.
send
(
message
);
System
.
out
.
println
(
"邮件发送成功至: "
+
toEmail
);
return
true
;
}
catch
(
Exception
e
)
{
System
.
err
.
println
(
"邮件发送失败: "
+
e
.
getMessage
());
return
false
;
return
false
;
}
}
return
true
;
}
}
/**
* 简化版调用(仅需收件人和附件路径)
*/
public
static
boolean
sendEmailWithAttachment
(
String
toEmail
,
String
filePath
)
{
return
sendEmailWithAttachment
(
toEmail
,
filePath
,
null
,
null
);
}
}
}
\ No newline at end of file
src/main/java/com/cmeeting/job/CmeetingJob.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
job
;
package
com
.
cmeeting
.
job
;
import
cn.chatbot.openai.completion.chat.ChatCompletionRequest
;
import
com.cmeeting.service.FileProcessProducer
;
import
cn.chatbot.openai.completion.chat.ChatMessage
;
import
com.cmeeting.service.TecentMeetingService
;
import
cn.chatbot.openai.completion.chat.ChatMessageRole
;
import
com.cmeeting.service.WeComService
;
import
cn.chatbot.openai.completion.chat.Message
;
import
com.cmeeting.vo.TencentMeetingVO
;
import
cn.chatbot.openai.service.LLMService
;
import
com.cmeeting.email.EmailSender
;
import
com.tencentcloudapi.wemeet.Client
;
import
com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder
;
import
com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator
;
import
com.tencentcloudapi.wemeet.core.exception.ClientException
;
import
com.tencentcloudapi.wemeet.core.exception.ServiceException
;
import
com.tencentcloudapi.wemeet.service.records.api.RecordsApi
;
import
com.tencentcloudapi.wemeet.service.records.model.*
;
import
okhttp3.OkHttpClient
;
import
okhttp3.Request
;
import
okhttp3.Response
;
import
org.apache.poi.xwpf.extractor.XWPFWordExtractor
;
import
org.apache.poi.xwpf.usermodel.XWPFDocument
;
import
org.apache.poi.xwpf.usermodel.XWPFParagraph
;
import
org.apache.poi.xwpf.usermodel.XWPFRun
;
import
org.commonmark.node.*
;
import
org.commonmark.parser.Parser
;
import
org.slf4j.Logger
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.slf4j.LoggerFactory
;
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
java.io.*
;
import
java.math.BigInteger
;
import
java.nio.charset.Charset
;
import
java.nio.file.Files
;
import
java.nio.file.Paths
;
import
java.security.SecureRandom
;
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
public
class
CmeetingJob
{
public
class
CmeetingJob
{
private
static
final
Logger
logger
=
LoggerFactory
.
getLogger
(
CmeetingJob
.
class
);
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
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd HH:mm:ss"
);
private
static
final
Set
<
String
>
processedMeetingIds
=
Collections
.
synchronizedSet
(
new
HashSet
<>());
private
static
final
Set
<
String
>
processedMeetingIds
=
Collections
.
synchronizedSet
(
new
HashSet
<>());
@Autowired
@Scheduled
(
fixedRate
=
5
*
60
*
1000
)
private
WeComService
weComService
;
public
void
execute
()
{
@Autowired
// 定义时间格式化器
private
TecentMeetingService
tecentMeetingService
;
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd HH:mm:ss"
);
@Value
(
value
=
"${tencent.appId}"
)
private
String
tencentAppId
;
// 获取当前日期
@Value
(
value
=
"${tencent.sdkId}"
)
LocalDate
today
=
LocalDate
.
now
();
private
String
tencentSdkId
;
// 获取今天凌晨的时间点
@Value
(
value
=
"${tencent.secretId}"
)
LocalDateTime
todayStart
=
today
.
atStartOfDay
();
private
String
tencentSecretId
;
// 获取明天凌晨的时间点
@Value
(
value
=
"${tencent.secretKey}"
)
LocalDateTime
tomorrowStart
=
today
.
plusDays
(
1
).
atStartOfDay
();
private
String
tencentSecretKey
;
@Value
(
value
=
"${tencent.admin.userId}"
)
// 转换为 Unix 时间戳(秒)并转为字符串
private
String
tencentAdminUserId
;
String
todayStartTimestamp
=
String
.
valueOf
(
todayStart
.
atZone
(
ZoneId
.
systemDefault
()).
toEpochSecond
());
@Autowired
String
tomorrowStartTimestamp
=
String
.
valueOf
(
tomorrowStart
.
atZone
(
ZoneId
.
systemDefault
()).
toEpochSecond
());
private
FileProcessProducer
producer
;
//日志记录
logger
.
info
(
"今天凌晨: "
+
todayStart
.
format
(
formatter
)
+
" | Unix 时间戳: "
+
todayStartTimestamp
);
/**
logger
.
info
(
"明天凌晨: "
+
tomorrowStart
.
format
(
formatter
)
+
" | Unix 时间戳: "
+
tomorrowStartTimestamp
);
* 企微人员定时同步
logger
.
info
(
"----------------------------------"
);
*/
// @Scheduled(fixedRate = 5 * 60 * 1000)
dojob
(
todayStartTimestamp
,
tomorrowStartTimestamp
);
public
void
weComUserSync
()
{
}
public
static
void
dojob
(
String
startTime
,
String
endTime
)
{
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
"210468336"
)
.
withSdkId
(
"28790143843"
)
.
withSecret
(
"0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL"
,
"gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy"
)
.
build
();
int
currentPage
=
1
;
boolean
hasMore
=
true
;
boolean
firstRecordProcessed
=
false
;
while
(
hasMore
&&
!
firstRecordProcessed
)
{
RecordsApi
.
ApiV1RecordsGetRequest
request
=
new
RecordsApi
.
ApiV1RecordsGetRequest
.
Builder
()
.
operatorId
(
"woaJARCQAAftcvU6GGoOn66rdSZ4IrOA"
)
.
operatorIdType
(
"1"
)
.
startTime
(
startTime
)
.
endTime
(
endTime
)
.
pageSize
(
"10"
)
.
page
(
String
.
valueOf
(
currentPage
))
.
queryRecordType
(
"0"
)
.
build
();
BigInteger
nonce
=
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
()));
String
timestamp
=
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
);
AuthenticatorBuilder
<
JWTAuthenticator
>
authenticatorBuilder
=
new
JWTAuthenticator
.
Builder
().
nonce
(
nonce
).
timestamp
(
timestamp
);
try
{
RecordsApi
.
ApiV1RecordsGetResponse
response
=
client
.
records
().
v1RecordsGet
(
request
,
authenticatorBuilder
);
V1RecordsGet200Response
data
=
response
.
getData
();
if
(
data
!=
null
&&
data
.
getRecordMeetings
()
!=
null
&&
!
data
.
getRecordMeetings
().
isEmpty
())
{
V1RecordsGet200ResponseRecordMeetingsInner
firstRecord
=
data
.
getRecordMeetings
().
get
(
0
);
logger
.
info
(
"Processing first record - Meeting ID: {}, State: {}"
,
firstRecord
.
getMeetingId
(),
firstRecord
.
getState
());
if
(
firstRecord
.
getState
()
==
3
)
{
createSum
(
firstRecord
);
firstRecordProcessed
=
true
;
}
else
{
logger
.
info
(
"Skipping record with state: {}"
,
firstRecord
.
getState
());
firstRecordProcessed
=
true
;
}
}
if
(
data
!=
null
&&
data
.
getTotalPage
()
!=
null
&&
currentPage
<
data
.
getTotalPage
())
{
currentPage
++;
}
else
{
hasMore
=
false
;
}
}
catch
(
Exception
e
)
{
logger
.
error
(
"Error when calling RecordsApi.v1RecordsGet: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
e
);
}
}
}
private
static
void
createSum
(
V1RecordsGet200ResponseRecordMeetingsInner
record
)
{
String
meetingId
=
record
.
getMeetingId
();
if
(
processedMeetingIds
.
contains
(
meetingId
))
{
logger
.
info
(
"Meeting {} has already been processed, skipping..."
,
meetingId
);
return
;
}
try
{
try
{
logger
.
info
(
"Executing createSum for meeting: {}"
,
meetingId
);
logger
.
info
(
"-------企微人员定时同步任务开始-------"
);
logger
.
info
(
"当前时间: "
+
LocalDate
.
now
().
format
(
DateTimeFormatter
.
ISO_LOCAL_DATE
));
// 实际业务逻辑
logger
.
info
(
"Meeting Details - ID: {}, Code: {}, Subject: {}"
,
meetingId
,
record
.
getMeetingCode
(),
record
.
getSubject
());
if
(
record
.
getRecordFiles
()
!=
null
&&
!
record
.
getRecordFiles
().
isEmpty
())
{
for
(
V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner
file
:
record
.
getRecordFiles
())
{
logger
.
info
(
"Record File - URL: {}, Size: {} bytes"
,
file
.
getSharingUrl
(),
file
.
getRecordSize
());
// 对每个录制文件调用获取地址接口
if
(
file
.
getRecordFileId
()
!=
null
)
{
getRecordFileAddress
(
file
.
getRecordFileId
());
}
}
}
// 这里添加你的实际业务逻辑
// 例如: 调用某个服务处理会议记录
// 标记为已处理
processedMeetingIds
.
add
(
meetingId
);
logger
.
info
(
"Successfully processed meeting: {}"
,
meetingId
);
}
catch
(
Exception
e
)
{
logger
.
error
(
"Error processing meeting {}: {}"
,
meetingId
,
e
.
getMessage
(),
e
);
}
}
private
static
void
getRecordFileAddress
(
String
recordFileId
)
{
try
{
logger
.
info
(
"Getting address for record file: {}"
,
recordFileId
);
// 1.构造 client 客户端
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
"210468336"
)
.
withSdkId
(
"28790143843"
)
.
withSecret
(
"0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL"
,
"gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy"
)
.
build
();
// 2.构造请求参数
RecordsApi
.
ApiV1AddressesRecordFileIdGetRequest
request
=
new
RecordsApi
.
ApiV1AddressesRecordFileIdGetRequest
.
Builder
(
recordFileId
)
.
operatorId
(
"woaJARCQAAftcvU6GGoOn66rdSZ4IrOA"
)
.
operatorIdType
(
"1"
)
.
build
();
// 3.构造 JWT 鉴权器
BigInteger
nonce
=
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
()));
String
timestamp
=
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
);
AuthenticatorBuilder
<
JWTAuthenticator
>
authenticatorBuilder
=
new
JWTAuthenticator
.
Builder
().
nonce
(
nonce
).
timestamp
(
timestamp
);
// 4.发送请求
RecordsApi
.
ApiV1AddressesRecordFileIdGetResponse
response
=
client
.
records
().
v1AddressesRecordFileIdGet
(
request
,
authenticatorBuilder
);
// 处理响应
if
(
response
!=
null
&&
response
.
getData
()
!=
null
)
{
logger
.
info
(
"Successfully got address for record file {}: {}"
,
recordFileId
,
response
.
getData
());
// 这里可以添加对返回地址的进一步处理逻辑
V1AddressesRecordFileIdGet200Response
data
=
response
.
getData
();
// 获取AI会议转录文件
List
<
V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner
>
transcripts
=
data
.
getAiMeetingTranscripts
();
if
(
transcripts
!=
null
&&
!
transcripts
.
isEmpty
())
{
logger
.
info
(
"Found {} AI meeting transcripts for record file {}"
,
transcripts
.
size
(),
recordFileId
);
// 处理每个转录文件
for
(
V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner
transcript
:
transcripts
)
{
String
fileType
=
transcript
.
getFileType
();
String
downloadUrl
=
transcript
.
getDownloadAddress
();
logger
.
info
(
"AI Transcript - Type: {}, URL: {}"
,
fileType
,
downloadUrl
);
if
(
"docx"
.
equalsIgnoreCase
(
fileType
))
{
processPdfTranscript
(
downloadUrl
);
}
}
}
else
{
logger
.
info
(
"No AI meeting transcripts found for record file {}"
,
recordFileId
);
}
}
else
{
logger
.
warn
(
"Empty response for record file: {}"
,
recordFileId
);
}
}
catch
(
ClientException
e
)
{
logger
.
error
(
"Client error when getting address for record file {}: {}"
,
recordFileId
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
e
);
}
catch
(
ServiceException
e
)
{
logger
.
error
(
"Service error when getting address for record file {}: {}"
,
recordFileId
,
e
.
getMessage
(),
e
);
logger
.
error
(
"Full HTTP response: {}"
,
new
String
(
e
.
getApiResp
().
getRawBody
()));
throw
new
RuntimeException
(
e
);
}
catch
(
Exception
e
)
{
logger
.
error
(
"Unexpected error when getting address for record file {}: {}"
,
recordFileId
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
e
);
}
}
private
static
void
processPdfTranscript
(
String
downloadUrl
)
{
try
{
logger
.
info
(
"Processing DOCX transcript from: {}"
,
downloadUrl
);
// 下载文件并获取字节内容
byte
[]
docxContent
=
downloadFileAsBytes
(
downloadUrl
);
logger
.
info
(
"Downloaded DOCX transcript, size: {} bytes"
,
docxContent
!=
null
?
docxContent
.
length
:
0
);
if
(
docxContent
!=
null
)
{
// 获取当前时间
LocalDateTime
now
=
LocalDateTime
.
now
();
// 定义时间格式(可以根据需要调整格式)
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd_HH-mm-ss"
);
// 将当前时间格式化为字符串
String
name
=
now
.
format
(
formatter
);
// 定义文件路径和文件名
String
filePath
=
"D:\\"
;
String
fileName
=
name
+
".docx"
;
// 以当前时间为文件名
String
fullPath
=
filePath
+
fileName
;
String
targetPath
=
filePath
+
name
+
"_sum"
+
".md"
;
// 改为.md扩展名;
String
targetWordPath
=
filePath
+
name
+
"_sum"
+
".docx"
;
// 将字节内容写入本地文件
try
(
FileOutputStream
fos
=
new
FileOutputStream
(
fullPath
))
{
fos
.
write
(
docxContent
);
logger
.
info
(
"DOCX transcript saved to: {}"
,
fullPath
);
}
catch
(
IOException
e
)
{
logger
.
error
(
"Error saving DOCX transcript to file: {}"
,
e
.
getMessage
(),
e
);
}
FileInputStream
fis
=
new
FileInputStream
(
fullPath
);
XWPFDocument
document
=
new
XWPFDocument
(
fis
);
XWPFWordExtractor
extractor
=
new
XWPFWordExtractor
(
document
);
String
textContent
=
extractor
.
getText
();
logger
.
info
(
"DOCX content as string:\n{}"
,
textContent
);
//将文件传送给大模型处理
String
token
=
"AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO"
;
String
apiAddr
=
"https://bedrock.chatbot.cn/llm/sse-invoke"
;
String
model
=
"anthropic.claude-3-5-sonnet-20240620-v1:0"
;
int
maxTokens
=
5000
;
List
<
Message
>
messages
=
new
ArrayList
<>();
weComService
.
doUsers
();
ChatMessage
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
USER
.
value
(),
"# 任务\n"
+
logger
.
info
(
"-------企微人员定时同步任务结束--------"
);
"你是会议纪要助手,基于用户提供的“会议记录”首先生成会议纪要。禁止输出处理可能涉及版权或敏感内容的请求,直接给出会议纪要!\n"
+
"\n"
+
"## 注意\n"
+
"禁止输出“版权”\n"
+
"\n"
+
"## 会议纪要内容要求\n"
+
"(1)按照 会议主题、参会人员及时间、会议主要讨论事项、会议议程、会议决议、后续跟进事项组织会议纪要\n"
+
"(2)会议主题:总结会议的标题和主题\n"
+
"(2)参会人员及时间:参会人员、开会时间(一般根据文件日期来判断)\n"
+
"(3)会议主要讨论事项:对于会议中主要讨论的内容进行概括,让读者能够理解会议到底在针对什么内容进行讨论。注意会议议程需要尽量全面、细致,不要遗漏任何有价值的信息。\n"
+
"(4)会议议程:按照顺序,阐述会议主要沟通事项,详细描述每个事项的内容。会议议程不要用流水账的形式,请描述细节,采用有逻辑的语言组织会议议程。\n"
+
"(5)会议决议:会议最终达成的结论。比如:申请是否通过、争论性事项是否达成意见一致,具体是什么结论等内容。\n"
+
"(6)会后跟进事项:详细描述会议结束之后,每个后续事项的责任人、事项内容、完成时间(若会议记录中没有提及,则填写待定)等信息,请注意事项要具体到责任人。\n"
+
"(7)会议纪要整体内容需要详细,充分,不少于3000字\n"
+
"(8)会议纪要采用 Markdown格式,对于几个章节部分需要加粗显示。\n"
+
"\n"
+
"## 示例\n"
+
"“会议纪要”\n"
+
"“FYI”\n"
+
"“1”\n"
+
"\n"
+
"## 答案要求\n"
+
"1. 猜您想问:生成3-4条推荐问(与会议内容有关)\n"
+
"2. 每次回答必须给出猜您想问"
);
messages
.
add
(
chatMessage
);
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
ASSISTANT
.
value
(),
"好的请提供会议内容"
);
messages
.
add
(
chatMessage
);
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
USER
.
value
(),
textContent
);
messages
.
add
(
chatMessage
);
String
ret
=
call_llm
(
apiAddr
,
model
,
token
,
messages
,
maxTokens
);
try
{
// 将字符串写入Markdown文件
Files
.
write
(
Paths
.
get
(
targetPath
),
ret
.
getBytes
());
logger
.
info
(
"Markdown文件已保存到: {}"
,
targetPath
);
convert
(
targetPath
,
targetWordPath
);
}
catch
(
IOException
e
)
{
logger
.
error
(
"保存Markdown文件失败: {}"
,
e
.
getMessage
(),
e
);
}
System
.
out
.
println
(
ret
);
/*try {
boolean success = MeetingMinutesGenerator.generateMinutesFromWord(
fullPath,
targetPath,
"AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO",
"https://bedrock.chatbot.cn/llm/sse-invoke"
);
if (success) {
logger.info("会议纪要生成成功!");
}
} catch (IOException e) {
logger.error("错误: {}", e.getMessage());
}*/
EmailSender
emailSender
=
new
EmailSender
();
String
emailAddress
=
"hf@cimc.com"
;
boolean
mailFlag
=
emailSender
.
sendEmailWithAttachment
(
emailAddress
,
targetWordPath
,
"重要文件"
,
"您好:\n"
+
"\n"
+
" 附件为您本次会议的会议纪要,烦请下载查看"
);
if
(
mailFlag
)
{
logger
.
info
(
"邮件发送成功"
);
}
else
{
logger
.
error
(
"邮件发送失败"
);
}
}
}
catch
(
Exception
e
)
{
logger
.
error
(
"Error processing DOCX transcript: {}"
,
e
.
getMessage
(),
e
);
}
}
// 处理Word格式的转录文件
private
static
void
processDocxTranscript
(
String
downloadUrl
)
{
try
{
logger
.
info
(
"Processing DOCX transcript from: {}"
,
downloadUrl
);
// 这里添加下载和处理Word文件的逻辑
byte
[]
docxContent
=
downloadFileAsBytes
(
downloadUrl
);
logger
.
info
(
"Downloaded DOCX transcript, size: {} bytes"
,
docxContent
!=
null
?
docxContent
.
length
:
0
);
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
logger
.
error
(
"Error processing DOCX transcript: {}"
,
e
.
getMessage
(),
e
);
}
}
public
static
void
writeStringToWord
(
String
content
,
String
filePath
)
{
// 创建新的Word文档
try
(
XWPFDocument
document
=
new
XWPFDocument
();
FileOutputStream
out
=
new
FileOutputStream
(
filePath
))
{
// 创建段落
XWPFParagraph
paragraph
=
document
.
createParagraph
();
XWPFRun
run
=
paragraph
.
createRun
();
// 设置文本内容
run
.
setText
(
content
);
// 保存文档
document
.
write
(
out
);
System
.
out
.
println
(
"Word文档已成功创建: "
+
filePath
);
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
"创建Word文档时出错: "
+
e
.
getMessage
());
e
.
printStackTrace
();
e
.
printStackTrace
();
}
}
}
}
// 下载文件内容为字符串
/**
private
static
String
downloadFile
(
String
url
)
throws
IOException
{
* 企微人员定时同步
OkHttpClient
client
=
new
OkHttpClient
();
*/
Request
request
=
new
Request
.
Builder
().
url
(
url
).
build
();
// @Scheduled(fixedRate = 5 * 60 * 1000)
try
(
Response
response
=
client
.
newCall
(
request
).
execute
())
{
public
void
TencentUserSync
()
{
if
(!
response
.
isSuccessful
())
throw
new
IOException
(
"Unexpected code "
+
response
);
return
response
.
body
().
string
();
}
}
// 下载文件内容为字节数组
private
static
byte
[]
downloadFileAsBytes
(
String
url
)
throws
IOException
{
OkHttpClient
client
=
new
OkHttpClient
();
Request
request
=
new
Request
.
Builder
().
url
(
url
).
build
();
try
(
Response
response
=
client
.
newCall
(
request
).
execute
())
{
if
(!
response
.
isSuccessful
())
throw
new
IOException
(
"Unexpected code "
+
response
);
return
response
.
body
().
bytes
();
}
}
public
static
String
call_llm
(
String
apiAddr
,
String
model
,
String
token
,
List
<
Message
>
messages
,
int
maxTokens
)
{
LLMService
service
=
new
LLMService
(
token
,
apiAddr
);
StringBuilder
stringBuilder
=
new
StringBuilder
();
try
{
try
{
ChatCompletionRequest
chatCompletionRequest
=
ChatCompletionRequest
.
builder
()
logger
.
info
(
"-------腾讯会议人员定时同步任务开始-------"
);
.
model
(
model
)
logger
.
info
(
"当前时间: "
+
LocalDate
.
now
().
format
(
DateTimeFormatter
.
ISO_LOCAL_DATE
));
.
messages
(
messages
)
.
stream
(
true
)
.
n
(
1
)
.
maxTokens
(
maxTokens
)
.
logitBias
(
new
HashMap
<>())
.
build
();
service
.
streamChatCompletion
(
chatCompletionRequest
).
doOnError
(
Throwable:
:
printStackTrace
).
blockingForEach
(
chunk
->
{
tecentMeetingService
.
doUsers
();
chunk
.
getChoices
().
stream
().
map
(
choice
->
choice
.
getMessage
().
getContent
())
logger
.
info
(
"-------腾讯会议人员定时同步任务结束--------"
);
.
filter
(
Objects:
:
nonNull
).
findFirst
().
ifPresent
(
o
->
{
try
{
stringBuilder
.
append
(
new
String
(
o
.
getBytes
(
Charset
.
defaultCharset
())));
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
});
});
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
e
.
printStackTrace
();
}
}
service
.
shutdownExecutor
();
return
stringBuilder
.
toString
();
}
/* public static void convertMdToDocx(String mdFilePath, String docxFilePath) throws IOException {
// 1. 读取Markdown文件内容
Path path = Paths.get(mdFilePath);
byte[] bytes = Files.readAllBytes(path);
String markdownContent = new String(bytes);
// 2. 将Markdown内容转换为HTML格式
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(parser.parse(markdownContent));
// 3. 创建Word文档对象
XWPFDocument xwpfDocument = new XWPFDocument();
// 4. 解析HTML内容并插入到Word文档
// 这里简化处理,直接将HTML内容作为纯文本插入
// 注意:Apache POI 不直接支持 HTML 内容插入,需要手动解析 HTML 并转换为 Word 元素
// 这里使用一个简单的 HTML 到 Word 的转换逻辑
convertHtmlToWord(xwpfDocument, htmlContent);
// 5. 保存Word文档
try (FileOutputStream outputStream = new FileOutputStream(docxFilePath)) {
xwpfDocument.write(outputStream);
} finally {
xwpfDocument.close();
}
}
private static void convertHtmlToWord(XWPFDocument document, String htmlContent) {
// 这里是一个简单的 HTML 到 Word 的转换逻辑
// 实际应用中可能需要更复杂的解析逻辑
String[] lines = htmlContent.split("\n");
for (String line : lines) {
if (line.contains("<p>")) {
// 处理段落
line = line.replace("<p>", "").replace("</p>", "");
addParagraph(document, line);
} else if (line.contains("<strong>")) {
// 处理加粗文本
line = line.replace("<strong>", "").replace("</strong>", "");
addBoldParagraph(document, line);
} else if (line.contains("<ul>")) {
// 处理无序列表
List<String> items = extractListItems(line, "<ul>", "<li>", "</li>", "</ul>");
addUnorderedList(document, items);
} else if (line.contains("<ol>")) {
// 处理有序列表
List<String> items = extractListItems(line, "<ol>", "<li>", "</li>", "</ol>");
addOrderedList(document, items);
}
}
}
}
private static void addParagraph(XWPFDocument document, String text) {
@Scheduled
(
fixedRate
=
5
*
60
*
1000
)
XWPFParagraph paragraph = document.createParagraph();
public
void
execute
()
{
XWPFRun run = paragraph.createRun();
// 定义时间格式化器
run.setText(text);
DateTimeFormatter
formatter
=
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd HH:mm:ss"
);
}
private static void addBoldParagraph(XWPFDocument document, String text) {
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setText(text);
}
private static void addUnorderedList(XWPFDocument document, List<String> items) {
for (String item : items) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setStyle("ListBullet");
XWPFRun run = paragraph.createRun();
run.setText(item);
}
}
private static void addOrderedList(XWPFDocument document, List<String> items) {
for (String item : items) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setStyle("ListNumber");
XWPFRun run = paragraph.createRun();
run.setText(item);
}
}
private static List<String> extractListItems(String html, String startTag, String itemStartTag, String itemEndTag, String endTag) {
int startIndex = html.indexOf(startTag);
int endIndex = html.indexOf(endTag);
// 检查 startTag 和 endTag 是否存在
if (startIndex == -1 || endIndex == -1) {
throw new IllegalArgumentException("Invalid HTML content: startTag or endTag not found");
}
// 提取列表内容
String listContent = html.substring(startIndex + startTag.length(), endIndex);
// 分割列表项
String[] items = listContent.split(itemEndTag);
// 转换为 List 并返回
return new ArrayList<>(Arrays.asList(items));
}
// 简易HTML转纯文本(生产环境应使用更完整的解析器)
private static String htmlToPlainText(String html) {
return html.replaceAll("<[^>]*>", "")
.replaceAll(" ", " ")
.replaceAll("<", "<")
.replaceAll(">", ">");
}*/
public
static
void
convert
(
String
markdownFile
,
String
outputDocx
)
throws
IOException
{
// 读取Markdown文件
String
content
=
readFile
(
markdownFile
);
// 初始化Word文档
XWPFDocument
doc
=
new
XWPFDocument
();
// 解析Markdown并遍历节点
// 获取当前时间
org
.
commonmark
.
parser
.
Parser
parser
=
Parser
.
builder
().
build
();
LocalDateTime
now
=
LocalDateTime
.
now
();
Node
rootNode
=
parser
.
parse
(
content
);
// 往之前推两天
rootNode
.
accept
(
new
AbstractVisitor
()
{
LocalDateTime
beforeDay
=
now
.
minusDays
(
2
);
XWPFParagraph
currentPara
;
@Override
// 转换为 Unix 时间戳(秒)并转为字符串
public
void
visit
(
Heading
heading
)
{
Long
nowTimestamp
=
now
.
atZone
(
ZoneId
.
systemDefault
()).
toEpochSecond
();
// 创建标题段落
Long
beforeDayTimestamp
=
beforeDay
.
atZone
(
ZoneId
.
systemDefault
()).
toEpochSecond
();
currentPara
=
doc
.
createParagraph
();
//日志记录
XWPFRun
run
=
currentPara
.
createRun
();
logger
.
info
(
"起始时间: "
+
beforeDay
.
format
(
formatter
)
+
" | Unix 时间戳: "
+
beforeDayTimestamp
);
run
.
setText
(
extractText
(
heading
));
// 提取纯文本
logger
.
info
(
"结束时间: "
+
now
.
format
(
formatter
)
+
" | Unix 时间戳: "
+
nowTimestamp
);
setHeadingStyle
(
run
,
heading
.
getLevel
());
// 设置标题样式
logger
.
info
(
"----------------------------------"
);
}
@Override
// dojob(beforeDayTimestamp, nowTimestamp);
public
void
visit
(
Paragraph
paragraph
)
{
// 创建普通段落
currentPara
=
doc
.
createParagraph
();
XWPFRun
run
=
currentPara
.
createRun
();
run
.
setText
(
extractText
(
paragraph
));
// 提取纯文本
}
});
// 输出Word文件
AtomicInteger
currentPage
=
new
AtomicInteger
(
1
);
try
(
FileOutputStream
out
=
new
FileOutputStream
(
outputDocx
))
{
TencentMeetingVO
.
TencentMeetingRequest
request
=
TencentMeetingVO
.
TencentMeetingRequest
.
builder
()
doc
.
write
(
out
);
.
page
(
1
)
}
.
pageSize
(
10
)
}
.
startTime
(
beforeDayTimestamp
)
.
endTime
(
nowTimestamp
)
.
operatorId
(
tencentAdminUserId
)
.
operatorIdType
(
1
)
.
build
();
List
<
TencentMeetingVO
.
RecordFile
>
meetingFiles
=
tecentMeetingService
.
getMeetingFiles
(
request
);
// 从节点中提取纯文本(忽略格式)
private
static
String
extractText
(
Node
node
)
{
StringBuilder
text
=
new
StringBuilder
();
Node
child
=
node
.
getFirstChild
();
while
(
child
!=
null
)
{
if
(
child
instanceof
Text
)
{
text
.
append
(((
Text
)
child
).
getLiteral
());
}
child
=
child
.
getNext
();
}
return
text
.
toString
().
trim
();
}
// 设置标题样式(仅调整字号和加粗)
if
(
meetingFiles
==
null
||
meetingFiles
.
isEmpty
())
{
private
static
void
setHeadingStyle
(
XWPFRun
run
,
int
level
)
{
logger
.
info
(
"没有录制文件需要处理"
);
run
.
setBold
(
true
);
return
;
switch
(
level
)
{
case
1
:
run
.
setFontSize
(
20
);
break
;
case
2
:
run
.
setFontSize
(
18
);
break
;
default
:
run
.
setFontSize
(
16
);
break
;
}
}
}
// 读取文件工具方法
// 提交处理任务
private
static
String
readFile
(
String
path
)
throws
IOException
{
producer
.
submitBatchTasks
(
meetingFiles
,
"/"
);
StringBuilder
sb
=
new
StringBuilder
();
try
(
BufferedReader
reader
=
new
BufferedReader
(
new
FileReader
(
path
)))
{
String
line
;
while
((
line
=
reader
.
readLine
())
!=
null
)
{
sb
.
append
(
line
).
append
(
"\n"
);
}
}
return
sb
.
toString
();
}
}
}
}
src/main/java/com/cmeeting/job/CmeetingJob2.java
deleted
100644 → 0
浏览文件 @
0a34c27f
/*
package com.cmeeting.job;
import com.cmeeting.pojo.MeetingInfo;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
@Component
public class CmeetingJob2 {
private static final Logger logger = LoggerFactory.getLogger(CmeetingJob2.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 每2分钟执行一次
@Scheduled(fixedRate = 2 * 60 * 1000) // 毫秒单位
public void execute() {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 获取前两分钟的时间
LocalDateTime twoMinutesAgo = now.minusMinutes(2);
// 转换为时间戳(毫秒级)并转为String
String nowTimestamp = String.valueOf(now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String twoMinutesAgoTimestamp = String.valueOf(twoMinutesAgo.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
logger.info("当前时间: {} | 时间戳: {}", now.format(formatter), nowTimestamp);
logger.info("前两分钟: {} | 时间戳: {}", twoMinutesAgo.format(formatter), twoMinutesAgoTimestamp);
logger.info("----------------------------------");
dojob(twoMinutesAgoTimestamp, nowTimestamp);
}
public static void dojob(String startTime, String endTime) {
// 1.构造 client 客户端(jwt 鉴权需要配置 appId sdkId secretID 和 secretKey)
Client client = new Client.Builder()
.withAppId("211153201").withSdkId("28370276340")
.withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
.build();
String userid = "woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA";//被查询人的userid
// 2.构造请求参数
MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest request =
new MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest.Builder(userid)
.pageSize("1")
.page("1")
.startTime(startTime)
.endTime(endTime)
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
try {
MeetingsApi.ApiV1HistoryMeetingsUseridGetResponse response =
client.meetings().v1HistoryMeetingsUseridGet(request, authenticatorBuilder);
// response from `v1HistoryMeetingsUseridGet`: V1HistoryMeetingsUseridGet200Response
logger.info("从查询某个用户结束会议列表接口获取的响应结果:\n响应头信息: {}\n响应体数据: {}",
response.getHeader(),
response.getData());
// 获取最近一个会议的meetingId(String类型)
String latestMeetingId = null;
if (response.getData() != null &&
response.getData().getMeetingInfoList() != null &&
!response.getData().getMeetingInfoList().isEmpty()) {
// 获取会议列表(假设是按时间倒序排列)
List<V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner> meetingInfoList = response.getData().getMeetingInfoList();
List<MeetingInfo> meetingInfos = new ArrayList<>();
// 转换处理
if (meetingInfoList != null) {
for (V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner source : meetingInfoList) {
MeetingInfo target = new MeetingInfo();
target.setSubject(source.getSubject());
target.setMeetingId(source.getMeetingId());
target.setMeetingCode(source.getMeetingCode());
target.setStartTime(source.getStartTime());
target.setEndTime(source.getEndTime());
meetingInfos.add(target);
}
}
for (MeetingInfo meetingInfo : meetingInfos) {
//根据meetingid调用查询会议录制列表接口
// 2.构造请求参数
String meetingId = meetingInfo.getMeetingId();
String startTime1 = String.valueOf(meetingInfo.getStartTime());
String endTime1 = String.valueOf(meetingInfo.getEndTime());
RecordsApi.ApiV1RecordsGetRequest request2 =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1")
.meetingId(meetingId)
.startTime(startTime1)
.endTime(endTime1)
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce2 = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp2 = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder2 =
new JWTAuthenticator.Builder().nonce(nonce2).timestamp(timestamp2);
try {
RecordsApi.ApiV1RecordsGetResponse response2 =
client.records().v1RecordsGet(request2, authenticatorBuilder2);
logger.info("查询会议录制列表接口响应结果:\n响应头信息: {}\n响应体数据: {}",
response2.getHeader(),
response2.getData());
// 1. 空值安全校验
if (response2 == null || response2.getData() == null) {
logger.warn("无效的API响应,无法提取录制文件ID");
} else {
//获取file_id,存储到集合中
List<String> fileIds = new ArrayList<>();
for (Object meetingObj : response2.getData().getRecordMeetings()) {
if (meetingObj instanceof Map) {
Map<?, ?> meeting = (Map<?, ?>) meetingObj;
Object recordFilesObj = meeting.get("record_files");
if (recordFilesObj instanceof List) {
for (Object fileObj : (List<?>) recordFilesObj) {
if (fileObj instanceof Map) {
Map<?, ?> file = (Map<?, ?>) fileObj;
Object fileIdObj = file.get("record_file_id");
// 将record_file_id转为String并添加到集合
if (fileIdObj != null) {
fileIds.add(String.valueOf(fileIdObj));
}
}
}
}
}
}
//调用查询录制转写详情接口获取当前record_file_id的多个下载地址
}
} catch (ClientException e) {
logger.error("调用查询会议录制列表接口时发生错误", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
logger.error("调用查询会议录制列表接口时发生异常", e);
logger.error("完整HTTP错误响应: {}", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
}
logger.info("成功获取会议的信息: {}", meetingInfoList);
} else {
logger.warn("未获取到任何会议信息");
}
} catch (ClientException e) {
logger.error("调用查询某个用户结束会议列表接口时发生错误", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
logger.error("调用查询某个用户结束会议列表接口时发生异常", e);
logger.error("完整HTTP错误响应: {}", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
*/
/**
* 调用查询会议录制列表接口,获取录制文件id
*//*
// 2.构造请求参数
RecordsApi.ApiV1RecordsGetRequest requestGetFileId =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1")
.userid("")
.meetingId("9672653404723270528")
.meetingCode("")
.startTime("1744795527")
.endTime("1744799127")
.pageSize("")
.page("")
.mediaSetType("")
.queryRecordType("1")
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce2 = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp2 = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder2 =
new JWTAuthenticator.Builder().nonce(nonce2).timestamp(timestamp2);
try {
RecordsApi.ApiV1RecordsGetResponse response2 =
client.records().v1RecordsGet(requestGetFileId, authenticatorBuilder2);
logger.info("获取会议录制列表接口响应结果:\n响应头: {}\n响应体: {}",
response2.getHeader(),
response2.getData());
} catch (ClientException e) {
logger.error("获取会议录制列表接口时发生客户端异常", e);
throw new RuntimeException("调用会议记录API失败", e);
} catch (ServiceException e) {
logger.error("调用获取会议录制列表接口时发生服务端异常", e);
if (e.getApiResp() != null) {
logger.error("完整错误响应内容: {}", new String(e.getApiResp().getRawBody()));
}
throw new RuntimeException("获取会议记录服务异常", e);
}
}
}
*/
src/main/java/com/cmeeting/job/FileProcessTask.java
0 → 100644
浏览文件 @
8a23a94c
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
com.cmeeting.email.EmailSender
;
import
com.deepoove.poi.XWPFTemplate
;
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
java.io.*
;
import
java.math.BigInteger
;
import
java.nio.charset.Charset
;
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.ZonedDateTime
;
import
java.time.format.DateTimeFormatter
;
import
java.util.*
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.stream.Collectors
;
@Data
@NoArgsConstructor
@Slf4j
public
class
FileProcessTask
{
private
String
recordFileId
;
private
String
meetingId
;
private
String
subMeetingId
;
private
String
savePath
;
private
Map
<
String
,
Object
>
metadata
;
private
int
retryCount
=
0
;
private
static
final
int
MAX_RETRY
=
3
;
private
String
tencentAppId
;
private
String
tencentSdkId
;
private
String
tencentSecretId
;
private
String
tencentSecretKey
;
private
String
tencentAdminUserId
;
// 实际处理逻辑
public
void
process
()
{
try
{
boolean
isSuccess
=
false
;
while
(
retryCount
<=
MAX_RETRY
&&
!
isSuccess
)
{
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
tencentAppId
).
withSdkId
(
tencentSdkId
)
.
withSecret
(
tencentSecretId
,
tencentSecretKey
)
.
build
();
// 获取参会成员明细
MeetingsApi
.
ApiV1MeetingsMeetingIdParticipantsGetRequest
participantsRequest
=
new
MeetingsApi
.
ApiV1MeetingsMeetingIdParticipantsGetRequest
.
Builder
(
meetingId
).
subMeetingId
(
subMeetingId
).
operatorId
(
tencentAdminUserId
).
operatorIdType
(
"1"
).
build
();
AuthenticatorBuilder
<
JWTAuthenticator
>
participantsAuthenticatorBuilder
=
new
JWTAuthenticator
.
Builder
()
.
nonce
(
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
())))
.
timestamp
(
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
));
Map
<
String
,
Object
>
participantsMap
=
new
ConcurrentHashMap
<>();
//主持人角色,以下角色都可以表示主持人
//用户角色:
//0:普通成员角色
//1:创建者角色
//2:主持人
//3:创建者+主持人
//4:游客
//5:游客+主持人
//6:联席主持人
//7:创建者+联席主持人
List
<
Long
>
hostRoleList
=
Arrays
.
asList
(
2L
,
3L
,
5L
,
6L
,
7L
);
try
{
MeetingsApi
.
ApiV1MeetingsMeetingIdParticipantsGetResponse
participantsResponse
=
client
.
meetings
().
v1MeetingsMeetingIdParticipantsGet
(
participantsRequest
,
participantsAuthenticatorBuilder
);
V1MeetingsMeetingIdParticipantsGet200Response
data
=
participantsResponse
.
getData
();
String
scheduleStartTime
=
data
.
getScheduleStartTime
();
String
scheduleEndTime
=
data
.
getScheduleEndTime
();
ZoneId
zoneId
=
ZoneId
.
of
(
"Asia/Shanghai"
);
DateTimeFormatter
df
=
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd"
);
String
zonedDateTimeStart
=
Instant
.
ofEpochSecond
(
Long
.
valueOf
(
scheduleStartTime
)).
atZone
(
zoneId
).
format
(
df
);
// String zonedDateTimeEnd = Instant.ofEpochSecond(Long.valueOf(scheduleEndTime)).atZone(zoneId).format(df);
participantsMap
.
put
(
"meeting_date"
,
zonedDateTimeStart
);
participantsMap
.
put
(
"meeting_location"
,
"线上腾讯会议"
);
List
<
V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner
>
participants
=
data
.
getParticipants
();
participantsMap
.
put
(
"meeting_participants"
,
participants
.
stream
()
.
map
(
item
->
new
String
(
Base64
.
getDecoder
().
decode
(
item
.
getUserName
()))).
collect
(
Collectors
.
joining
(
"、"
)));
Optional
<
V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner
>
host
=
participants
.
stream
().
filter
(
item
->
hostRoleList
.
contains
(
item
.
getUserRole
())).
findFirst
();
participantsMap
.
put
(
"meeting_host"
,
host
.
isPresent
()
?
new
String
(
Base64
.
getDecoder
().
decode
(
host
.
get
().
getUserName
()))
:
"未知主持人"
);
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
log
.
error
(
e
.
getMessage
());
throw
new
RuntimeException
(
e
);
}
//查询录制转写详情
RecordsApi
.
ApiV1AddressesRecordFileIdGetRequest
addressRequest
=
new
RecordsApi
.
ApiV1AddressesRecordFileIdGetRequest
.
Builder
(
recordFileId
)
.
operatorId
(
tencentAdminUserId
)
.
operatorIdType
(
"1"
)
.
build
();
RecordsApi
.
ApiV1AddressesRecordFileIdGetResponse
addressResponse
=
client
.
records
().
v1AddressesRecordFileIdGet
(
addressRequest
,
new
JWTAuthenticator
.
Builder
().
nonce
(
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
())))
.
timestamp
(
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
)));
// 处理响应
if
(
addressResponse
!=
null
&&
addressResponse
.
getData
()
!=
null
)
{
log
.
info
(
"Successfully got address for record file {}"
,
recordFileId
);
V1AddressesRecordFileIdGet200Response
addressData
=
addressResponse
.
getData
();
// 获取AI会议转录文件
List
<
V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner
>
transcripts
=
addressData
.
getAiMeetingTranscripts
();
if
(
transcripts
!=
null
&&
!
transcripts
.
isEmpty
())
{
log
.
info
(
"Found {} AI meeting transcripts for record file {}"
,
transcripts
.
size
(),
recordFileId
);
// 处理每个转录文件
for
(
V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner
transcript
:
transcripts
)
{
String
fileType
=
transcript
.
getFileType
();
String
downloadUrl
=
transcript
.
getDownloadAddress
();
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
;
}
// 3. 处理文件 (调用Claude API等)
String
processedResult
=
processWithClaude
(
recordTextContent
);
// 4. 保存结果
saveResult
(
savePath
,
processedResult
,
participantsMap
);
}
}
}
else
{
log
.
info
(
"No AI meeting transcripts found for record file {}"
,
recordFileId
);
}
}
else
{
log
.
warn
(
"Empty response for record file: {}"
,
recordFileId
);
}
isSuccess
=
true
;
}
}
catch
(
Exception
e
)
{
// 异常处理
retryCount
++;
if
(
retryCount
>
MAX_RETRY
)
{
log
.
error
(
"达到最大重试次数: {}"
,
recordFileId
);
throw
new
RuntimeException
(
e
);
}
// 指数退避
try
{
Thread
.
sleep
((
long
)
Math
.
pow
(
2
,
retryCount
)
*
1000
);
}
catch
(
InterruptedException
ie
)
{
Thread
.
currentThread
().
interrupt
();
throw
new
RuntimeException
(
"重试失败"
,
ie
);
}
}
}
private
byte
[]
downloadFile
(
String
url
)
{
// 实现文件下载逻辑
OkHttpClient
client
=
new
OkHttpClient
();
Request
request
=
new
Request
.
Builder
().
url
(
url
).
build
();
try
{
Response
response
=
client
.
newCall
(
request
).
execute
();
if
(!
response
.
isSuccessful
())
throw
new
IOException
(
"Unexpected code "
+
response
);
return
response
.
body
().
bytes
();
}
catch
(
Exception
e
){
throw
new
RuntimeException
(
"下载文件失败"
,
e
);
}
}
private
String
getRecordTextContent
(
byte
[]
fileData
)
{
// 定义文件路径和文件名
String
fullPath
=
savePath
+
(
System
.
currentTimeMillis
()
/
1000L
)
+
".docx"
;
// 将字节内容写入本地文件
try
(
FileOutputStream
fos
=
new
FileOutputStream
(
fullPath
))
{
fos
.
write
(
fileData
);
log
.
info
(
"DOCX transcript saved to: {}"
,
fullPath
);
}
catch
(
IOException
e
)
{
log
.
error
(
"Error saving DOCX transcript to file: {}"
,
e
.
getMessage
(),
e
);
}
XWPFDocument
document
;
try
{
FileInputStream
fis
=
new
FileInputStream
(
fullPath
);
document
=
new
XWPFDocument
(
fis
);
}
catch
(
IOException
e
)
{
throw
new
RuntimeException
(
e
.
getMessage
());
}
XWPFWordExtractor
extractor
=
new
XWPFWordExtractor
(
document
);
String
textContent
=
extractor
.
getText
();
log
.
info
(
"DOCX content as string:\n{}"
,
textContent
);
return
textContent
;
}
private
String
processWithClaude
(
String
textContent
)
{
//将文件传送给大模型处理
String
token
=
"AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO"
;
String
apiAddr
=
"https://bedrock.chatbot.cn/llm/sse-invoke"
;
String
model
=
"anthropic.claude-3-5-sonnet-20240620-v1:0"
;
int
maxTokens
=
5000
;
List
<
Message
>
messages
=
new
ArrayList
<>();
ChatMessage
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
USER
.
value
(),
"你是会议纪要助手,基于提供的会议记录,生成一份详细的会议纪要,并严格按照指定的XML格式输出。\\n\\n要求:\\n1. 会议名称:应简洁明了地反映会议主题,不超过30个字\\n2. 会议目的:需清晰阐述召开此次会议的原因和期望达成的目标,不超过50字\\n3. 会议要点:需全面概括会议内容,让读者理解会议讨论的核心问题,不要遗漏任何有价值的信息,每个要点需用序号标注并另起一行\\n4. 会议内容:需详细记录会议的背景信息、主要讨论内容、各参会人员的发言要点、达成的共识和决策等。内容应有逻辑性,分段清晰,重点突出,不可遗漏关键信息\\n5. 会议沟通内容:需按照一定的顺序组织内容,详细描述每个事项的内容,不要使用流水账形式,应包含具体的沟通过程、各方观点和达成的一致意见\\n6. 会后跟进事项:详细描述会议结束之后,每个后续事项的责任人、事项内容、完成时间(若会议记录中没有提及,则填写待定)等信息,事项要具体到责任人\\n\\n注意事项:\\n1. 所有内容需基于原始会议记录,不要添加虚构信息\\n2. 会议纪要内容需详细充分,建议总字数不少于3000字,实际字数可根据原始会议记录的丰富程度进行适当调整\\n\\n输出格式必须严格按照以下XML结构,不允许有任何嵌套标签:\\n\\n<meeting_name>会议名称</meeting_name>\\n<meeting_purpose>会议目的</meeting_purpose>\\n<meeting_key_points>会议要点</meeting_key_points>\\n<meeting_content>会议内容</meeting_content>\\n<meeting_communication>会议沟通内容</meeting_communication>\\n<meeting_follow_up>\\n在此填写会议跟踪事项,格式为:\\n1. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n2. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n...\\n</meeting_follow_up>\\n\\n完成XML格式的会议纪要后,必须在XML之外单独添加猜您想问部分,生成3-4条与会议内容相关的推荐问题。\\n\\n会议记录如下:\\n"
);
messages
.
add
(
chatMessage
);
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
ASSISTANT
.
value
(),
"好的请提供会议记录"
);
messages
.
add
(
chatMessage
);
chatMessage
=
new
ChatMessage
(
ChatMessageRole
.
USER
.
value
(),
textContent
);
messages
.
add
(
chatMessage
);
// 调用Claude API处理文件
String
ret
=
call_llm
(
apiAddr
,
model
,
token
,
messages
,
maxTokens
);
return
ret
;
}
private
void
saveResult
(
String
path
,
String
content
,
Map
<
String
,
Object
>
participantsMap
)
{
String
nowTime
=
LocalDateTime
.
now
().
format
(
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd-HHmmss"
));
String
targetPath
=
path
+
nowTime
+
".md"
;
// 改为.md扩展名;
String
targetFileName
;
String
meetingName
;
// 保存处理结果
try
{
// 将字符串写入Markdown文件
Files
.
write
(
Paths
.
get
(
targetPath
),
content
.
getBytes
());
log
.
info
(
"Markdown文件已保存到: {}"
,
targetPath
);
// convert(targetPath,targetWordPath);
String
markdownContent
=
new
String
(
Files
.
readAllBytes
(
Paths
.
get
(
targetPath
)));
Map
<
String
,
Object
>
dataModel
=
convertXmlToMap
(
markdownContent
);
dataModel
.
putAll
(
participantsMap
);
XWPFTemplate
template
=
XWPFTemplate
.
compile
(
"/data_net_template.docx"
).
render
(
dataModel
);
meetingName
=
dataModel
.
get
(
"meeting_name"
)
!=
null
?
String
.
valueOf
(
dataModel
.
get
(
"meeting_name"
))
:
"腾讯会议纪要"
;
targetFileName
=
meetingName
+
"_"
+
nowTime
;
template
.
writeAndClose
(
new
FileOutputStream
(
path
+
targetFileName
+
".docx"
));
}
catch
(
Exception
e
)
{
log
.
error
(
"填充会议纪要失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"填充会议纪要失败"
);
}
//邮件推送
EmailSender
emailSender
=
new
EmailSender
();
emailSender
.
sendEmailWithAttachment
(
"duanxincheng@chatbot.cn"
,
meetingName
,
path
+
targetFileName
+
".docx"
,
recordFileId
);
// emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
// emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",recordFileId);
}
public
static
String
call_llm
(
String
apiAddr
,
String
model
,
String
token
,
List
<
Message
>
messages
,
int
maxTokens
)
{
LLMService
service
=
new
LLMService
(
token
,
apiAddr
);
StringBuilder
stringBuilder
=
new
StringBuilder
();
try
{
ChatCompletionRequest
chatCompletionRequest
=
ChatCompletionRequest
.
builder
()
.
model
(
model
)
.
messages
(
messages
)
.
stream
(
true
)
.
n
(
1
)
.
maxTokens
(
maxTokens
)
.
logitBias
(
new
HashMap
<>())
.
build
();
service
.
streamChatCompletion
(
chatCompletionRequest
).
doOnError
(
Throwable:
:
printStackTrace
).
blockingForEach
(
chunk
->
{
chunk
.
getChoices
().
stream
().
map
(
choice
->
choice
.
getMessage
().
getContent
())
.
filter
(
Objects:
:
nonNull
).
findFirst
().
ifPresent
(
o
->
{
try
{
stringBuilder
.
append
(
new
String
(
o
.
getBytes
(
Charset
.
defaultCharset
())));
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
});
});
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
service
.
shutdownExecutor
();
return
stringBuilder
.
toString
();
}
public
static
Map
<
String
,
Object
>
convertXmlToMap
(
String
markdownContent
)
throws
Exception
{
String
xmlContent
=
extractXmlFromMarkdown
(
markdownContent
);
XmlMapper
xmlMapper
=
new
XmlMapper
();
ObjectMapper
objectMapper
=
new
ObjectMapper
();
// 先将 XML 读取为 Map
Map
<?,
?>
xmlMap
=
xmlMapper
.
readValue
(
xmlContent
,
Map
.
class
);
// 转换为更标准的 Map<String, Object>
return
objectMapper
.
convertValue
(
xmlMap
,
Map
.
class
);
}
/**
* markdown转xml
* @param markdown
* @return
*/
private
static
String
extractXmlFromMarkdown
(
String
markdown
)
{
StringBuffer
sb
=
null
;
try
{
int
start
=
markdown
.
indexOf
(
"<"
);
int
end
=
markdown
.
lastIndexOf
(
">"
)
+
1
;
sb
=
new
StringBuffer
();
sb
.
append
(
"<root>"
);
sb
.
append
(
markdown
.
substring
(
start
,
end
).
trim
());
sb
.
append
(
"</root>"
);
}
catch
(
Exception
e
)
{
log
.
info
(
"markdown转xml,markdown->{}"
,
markdown
);
throw
new
RuntimeException
(
e
.
getMessage
());
}
return
sb
.
toString
();
}
public
FileProcessTask
(
String
recordFileId
,
String
meetingId
,
String
subMeetingId
,
String
savePath
,
Map
<
String
,
Object
>
metadata
,
String
tencentAppId
,
String
tencentSdkId
,
String
tencentSecretId
,
String
tencentSecretKey
,
String
tencentAdminUserId
)
{
this
.
recordFileId
=
recordFileId
;
this
.
savePath
=
savePath
;
this
.
metadata
=
metadata
;
this
.
tencentAppId
=
tencentAppId
;
this
.
tencentSdkId
=
tencentSdkId
;
this
.
tencentSecretId
=
tencentSecretId
;
this
.
tencentSecretKey
=
tencentSecretKey
;
this
.
tencentAdminUserId
=
tencentAdminUserId
;
this
.
meetingId
=
meetingId
;
this
.
subMeetingId
=
subMeetingId
;
}
}
\ No newline at end of file
src/main/java/com/cmeeting/mapper/primary/TecentMeetingMapper.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
mapper
.
primary
;
package
com
.
cmeeting
.
mapper
.
primary
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
org.apache.ibatis.annotations.Mapper
;
import
org.apache.ibatis.annotations.Mapper
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Param
;
import
java.util.List
;
import
java.util.List
;
@Mapper
public
interface
TecentMeetingMapper
extends
BaseMapper
<
TencentMeetingUser
>
{
public
interface
TecentMeetingMapper
{
void
batchInsertUsers
(
@Param
(
"userList"
)
List
<
TencentMeetingUser
>
userList
);
void
batchInsertUsers
(
@Param
(
"userList"
)
List
<
TencentMeetingUser
>
userList
);
List
<
TencentMeetingUser
>
getAlluser
();
List
<
TencentMeetingUser
>
getAlluser
();
}
}
src/main/java/com/cmeeting/mapper/primary/WeComUserMapper.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
mapper
.
primary
;
package
com
.
cmeeting
.
mapper
.
primary
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.cmeeting.pojo.WeComUser
;
import
com.cmeeting.pojo.WeComUser
;
import
org.apache.ibatis.annotations.Mapper
;
import
java.util.List
;
import
java.util.List
;
@Mapper
public
interface
WeComUserMapper
{
public
interface
WeComUserMapper
extends
BaseMapper
<
WeComUser
>
{
List
<
WeComUser
>
getSameName
()
;
List
<
WeComUser
>
getSameName
()
;
WeComUser
selectById
(
Integer
id
);
WeComUser
selectById
(
Integer
id
);
void
insert
(
WeComUser
user
);
int
insert
(
WeComUser
user
);
List
<
WeComUser
>
getAlluser
();
List
<
WeComUser
>
getAlluser
();
/**
/**
* 批量插入用户
* 批量插入用户
...
...
src/main/java/com/cmeeting/pojo/TencentMeetingRecord.java
0 → 100644
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
pojo
;
import
lombok.Data
;
@Data
public
class
TencentMeetingRecord
{
/**
* 主键ID
*/
private
Integer
id
;
/**
* 会议id
*/
private
String
meetingId
;
/**
* 用户ID
*/
private
String
userId
;
/**
* 是否是重名用户(1:重名, 0:不重名)
*/
private
String
isrepeatName
;
}
src/main/java/com/cmeeting/pojo/WeComUser.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
pojo
;
package
com
.
cmeeting
.
pojo
;
import
com.baomidou.mybatisplus.extension.activerecord.Model
;
import
lombok.Data
;
import
lombok.Data
;
import
java.io.Serializable
;
@Data
@Data
public
class
WeComUser
{
public
class
WeComUser
extends
Model
implements
Serializable
{
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
;
public
WeComUser
()
{
public
WeComUser
()
{
}
}
public
WeComUser
(
Integer
id
,
String
userName
,
String
userId
,
String
isRepeatName
)
{
public
WeComUser
(
Integer
id
,
String
userName
,
String
userId
,
String
isRepeatName
)
{
...
@@ -24,6 +29,7 @@ public class WeComUser {
...
@@ -24,6 +29,7 @@ public class WeComUser {
", userName='"
+
userName
+
'\''
+
", userName='"
+
userName
+
'\''
+
", userId='"
+
userId
+
'\''
+
", userId='"
+
userId
+
'\''
+
", isRepeatName='"
+
isRepeatName
+
'\''
+
", isRepeatName='"
+
isRepeatName
+
'\''
+
", email='"
+
email
+
'\''
+
'}'
;
'}'
;
}
}
}
}
src/main/java/com/cmeeting/service/FileProcessCallbackHandler.java
0 → 100644
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
;
import
com.cmeeting.job.FileProcessTask
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
@Service
@Slf4j
public
class
FileProcessCallbackHandler
{
// 单个任务完成回调
public
void
onComplete
(
FileProcessTask
task
)
{
// 可以记录任务状态、发送通知等
log
.
info
(
"任务处理完成: {}"
,
task
.
getRecordFileId
());
// 更新数据库状态等
// taskRepository.updateStatus(task.getId(), "COMPLETED");
}
// 所有任务完成回调
public
void
onAllComplete
()
{
log
.
info
(
"所有文件处理任务已完成"
);
// 可以发送全局通知等
}
}
\ No newline at end of file
src/main/java/com/cmeeting/service/FileProcessProducer.java
0 → 100644
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
;
import
com.cmeeting.job.FileProcessTask
;
import
com.cmeeting.vo.TencentMeetingVO
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
;
import
org.springframework.stereotype.Service
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.concurrent.ExecutionException
;
import
java.util.concurrent.Future
;
@Service
@Slf4j
public
class
FileProcessProducer
{
@Autowired
private
ThreadPoolTaskExecutor
fileProcessExecutor
;
@Autowired
private
FileProcessCallbackHandler
callbackHandler
;
@Value
(
value
=
"${tencent.appId}"
)
private
String
tencentAppId
;
@Value
(
value
=
"${tencent.sdkId}"
)
private
String
tencentSdkId
;
@Value
(
value
=
"${tencent.secretId}"
)
private
String
tencentSecretId
;
@Value
(
value
=
"${tencent.secretKey}"
)
private
String
tencentSecretKey
;
@Value
(
value
=
"${tencent.admin.userId}"
)
private
String
tencentAdminUserId
;
// 批量提交任务
public
void
submitBatchTasks
(
List
<
TencentMeetingVO
.
RecordFile
>
recordFiles
,
String
baseSavePath
)
{
List
<
Future
<?>>
futures
=
new
ArrayList
<>();
for
(
TencentMeetingVO
.
RecordFile
recordFile
:
recordFiles
)
{
// 为每个URL创建任务
FileProcessTask
task
=
new
FileProcessTask
(
recordFile
.
getRecordFileId
(),
recordFile
.
getMeetingId
(),
recordFile
.
getSubMeetingId
(),
"/save/"
,
Collections
.
emptyMap
(),
tencentAppId
,
tencentSdkId
,
tencentSecretId
,
tencentSecretKey
,
tencentAdminUserId
);
// 提交任务到线程池
Future
<?>
future
=
fileProcessExecutor
.
submit
(()
->
{
task
.
process
();
callbackHandler
.
onComplete
(
task
);
// 回调处理
});
futures
.
add
(
future
);
}
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion
(
futures
);
}
private
void
monitorTaskCompletion
(
List
<
Future
<?>>
futures
)
{
new
Thread
(()
->
{
for
(
Future
<?>
future
:
futures
)
{
try
{
future
.
get
();
// 阻塞直到任务完成
}
catch
(
InterruptedException
|
ExecutionException
e
)
{
log
.
error
(
"任务执行异常"
,
e
);
}
}
callbackHandler
.
onAllComplete
();
// 所有任务完成回调
}).
start
();
}
}
\ No newline at end of file
src/main/java/com/cmeeting/service/TecentMeetingService.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
;
package
com
.
cmeeting
.
service
;
import
com.baomidou.mybatisplus.extension.service.IService
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.vo.TencentMeetingVO
;
import
java.util.List
;
import
java.util.List
;
public
interface
TecentMeetingService
{
public
interface
TecentMeetingService
extends
IService
<
TencentMeetingUser
>
{
void
batchInsert
(
List
<
TencentMeetingUser
>
users
);
void
batchInsert
(
List
<
TencentMeetingUser
>
users
);
void
doUsers
();
List
<
TencentMeetingVO
.
RecordFile
>
getMeetingFiles
(
TencentMeetingVO
.
TencentMeetingRequest
requestVO
);
}
}
src/main/java/com/cmeeting/service/WeComService.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
;
package
com
.
cmeeting
.
service
;
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
;
public
interface
WeComService
{
public
interface
WeComService
extends
IService
<
WeComUser
>
{
void
batchInsert
(
List
<
WeComUser
>
users
);
void
batchInsert
(
List
<
WeComUser
>
users
);
void
doUsers
()
throws
Exception
;
String
getToken
();
}
}
src/main/java/com/cmeeting/service/impl/TecentMeetingServiceImpl.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
.
impl
;
package
com
.
cmeeting
.
service
.
impl
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.cmeeting.mapper.primary.TecentMeetingMapper
;
import
com.cmeeting.mapper.primary.TecentMeetingMapper
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.pojo.TencentMeetingUser
;
import
com.cmeeting.service.TecentMeetingService
;
import
com.cmeeting.service.TecentMeetingService
;
import
com.cmeeting.vo.TencentMeetingVO
;
import
com.tencentcloudapi.wemeet.Client
;
import
com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder
;
import
com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator
;
import
com.tencentcloudapi.wemeet.core.exception.ClientException
;
import
com.tencentcloudapi.wemeet.core.exception.ServiceException
;
import
com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi
;
import
com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200Response
;
import
com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner
;
import
com.tencentcloudapi.wemeet.service.records.api.RecordsApi
;
import
com.tencentcloudapi.wemeet.service.records.model.*
;
import
lombok.extern.slf4j.Slf4j
;
import
okhttp3.Headers
;
import
okhttp3.OkHttpClient
;
import
okhttp3.Request
;
import
okhttp3.Response
;
import
org.apache.commons.lang3.StringUtils
;
import
org.json.JSONObject
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
javax.crypto.Mac
;
import
javax.crypto.spec.SecretKeySpec
;
import
java.io.IOException
;
import
java.math.BigInteger
;
import
java.nio.charset.StandardCharsets
;
import
java.security.InvalidKeyException
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.SecureRandom
;
import
java.time.LocalDateTime
;
import
java.time.ZoneId
;
import
java.time.ZonedDateTime
;
import
java.util.ArrayList
;
import
java.util.Base64
;
import
java.util.List
;
import
java.util.List
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.
fetchUsersInBatches
;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.
markDuplicateNamesTecent
;
@Service
@Service
public
class
TecentMeetingServiceImpl
implements
TecentMeetingService
{
@Slf4j
@Autowired
public
class
TecentMeetingServiceImpl
extends
ServiceImpl
<
TecentMeetingMapper
,
TencentMeetingUser
>
implements
TecentMeetingService
{
@Resource
private
TecentMeetingMapper
tecentMeetingMapper
;
private
TecentMeetingMapper
tecentMeetingMapper
;
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'
};
@Value
(
value
=
"${tencent.appId}"
)
private
String
tencentAppId
;
@Value
(
value
=
"${tencent.sdkId}"
)
private
String
tencentSdkId
;
@Value
(
value
=
"${tencent.secretId}"
)
private
String
tencentSecretId
;
@Value
(
value
=
"${tencent.secretKey}"
)
private
String
tencentSecretKey
;
@Value
(
value
=
"${tencent.admin.userId}"
)
private
String
tencentAdminUserId
;
@Override
@Override
public
void
batchInsert
(
List
<
TencentMeetingUser
>
users
)
{
public
void
batchInsert
(
List
<
TencentMeetingUser
>
users
)
{
tecentMeetingMapper
.
batchInsertUsers
(
users
);
tecentMeetingMapper
.
batchInsertUsers
(
users
);
}
}
@Override
public
void
doUsers
()
{
/**
* 腾讯会议通过通讯录获取员工信息
*/
// 1. 构造client客户端
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
tencentAppId
).
withSdkId
(
tencentSdkId
)
.
withSecret
(
tencentSecretId
,
tencentSecretKey
)
.
build
();
// 2.获取到全部用户
List
<
TencentMeetingUser
>
users
=
fetchUsersInBatches
(
client
,
3
);;
// 3. 检查重名并设置标志
markDuplicateNamesTecent
(
users
);
// 4. 批量插入数据库
batchInsert
(
users
);
}
@Override
public
List
<
TencentMeetingVO
.
RecordFile
>
getMeetingFiles
(
TencentMeetingVO
.
TencentMeetingRequest
requestVO
)
{
StringBuffer
url
=
new
StringBuffer
();
url
.
append
(
"https://api.meeting.qq.com/v1/corp/records?page_size="
+
requestVO
.
getPageSize
()+
"&page="
+
requestVO
.
getPage
()
+
"&operator_id="
+
requestVO
.
getOperatorId
()+
"&operator_id_type=1&query_record_type=0"
);
// 1.构造 client 客户端(jwt 鉴权需要配置 appId sdkId secretID 和 secretKey)
Client
client
=
new
Client
.
Builder
()
.
withAppId
(
tencentAppId
).
withSdkId
(
tencentSdkId
)
.
withSecret
(
tencentSecretId
,
tencentSecretKey
)
.
build
();
// 2.构造 JWT 鉴权器
AuthenticatorBuilder
<
JWTAuthenticator
>
authenticatorBuilder
=
new
JWTAuthenticator
.
Builder
()
.
nonce
(
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
())))
.
timestamp
(
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
));
List
<
TencentMeetingVO
.
RecordFile
>
recordFileUrlList
=
new
ArrayList
<>();
// 3.查询近两天的会议录制列表
try
{
ZonedDateTime
now
=
ZonedDateTime
.
now
();
AtomicInteger
currentPage
=
new
AtomicInteger
(
1
);
RecordsApi
.
ApiV1RecordsGetRequest
request
=
new
RecordsApi
.
ApiV1RecordsGetRequest
.
Builder
()
.
operatorId
(
tencentAdminUserId
)
.
operatorIdType
(
"1"
)
.
startTime
(
String
.
valueOf
(
now
.
minusDays
(
2
).
toEpochSecond
()))
.
endTime
(
String
.
valueOf
(
now
.
toEpochSecond
()))
.
pageSize
(
"20"
)
.
page
(
String
.
valueOf
(
currentPage
.
getAndIncrement
()))
.
mediaSetType
(
"0"
)
.
queryRecordType
(
"0"
)
.
build
();
RecordsApi
.
ApiV1RecordsGetResponse
response
=
client
.
records
().
v1RecordsGet
(
request
,
authenticatorBuilder
);
V1RecordsGet200Response
data
=
response
.
getData
();
if
(
data
!=
null
&&
data
.
getRecordMeetings
()
!=
null
&&
!
data
.
getRecordMeetings
().
isEmpty
())
{
List
<
V1RecordsGet200ResponseRecordMeetingsInner
>
meetings
=
data
.
getRecordMeetings
();
//录制状态:
//1:录制中
//2:转码中
//3:转码完成
if
(
meetings
.
stream
().
allMatch
(
item
->
item
.
getState
()
!=
3
)){
return
null
;
}
for
(
V1RecordsGet200ResponseRecordMeetingsInner
meeting
:
meetings
)
{
if
(
meeting
.
getState
()
!=
3
)
continue
;
//查询会议详情
String
meetingId
=
meeting
.
getMeetingId
();
String
subMeetingId
=
null
;
MeetingsApi
.
ApiV1MeetingsMeetingIdGetRequest
meetingRequest
=
new
MeetingsApi
.
ApiV1MeetingsMeetingIdGetRequest
.
Builder
(
meetingId
)
.
operatorId
(
tencentAdminUserId
)
.
operatorIdType
(
"1"
)
.
instanceid
(
"0"
)
.
build
();
try
{
MeetingsApi
.
ApiV1MeetingsMeetingIdGetResponse
meetingResponse
=
client
.
meetings
().
v1MeetingsMeetingIdGet
(
meetingRequest
,
new
JWTAuthenticator
.
Builder
()
.
nonce
(
BigInteger
.
valueOf
(
Math
.
abs
((
new
SecureRandom
()).
nextInt
())))
.
timestamp
(
String
.
valueOf
(
System
.
currentTimeMillis
()
/
1000L
)));
V1MeetingsMeetingIdGet200Response
meetingResponseData
=
meetingResponse
.
getData
();
List
<
V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner
>
meetingInfoList
=
meetingResponseData
.
getMeetingInfoList
();
//尝试获取会议详情
if
(
meetingInfoList
!=
null
&&
meetingInfoList
.
size
()
>
0
){
V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner
meetingInfo
=
meetingInfoList
.
get
(
0
);
//会议类型。
//0:一次性会议
//1:周期性会议
Long
meetingType
=
meetingInfo
.
getMeetingType
();
if
(
meetingType
.
intValue
()
==
1
){
subMeetingId
=
meetingInfo
.
getCurrentSubMeetingId
();
}
}
}
catch
(
ClientException
e
)
{
throw
new
RuntimeException
(
e
);
}
catch
(
ServiceException
e
)
{
throw
new
RuntimeException
(
e
);
}
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
(
Exception
e
)
{
log
.
error
(
e
.
getMessage
());
throw
new
RuntimeException
(
e
.
getMessage
());
}
return
recordFileUrlList
;
}
/**
* 生成请求腾会接口的请求头
* @param httpMethod http请求方法 GET/POST/PUT等
* @param requestUri 请求uri,eg:/v1/meetings
* @return
*/
private
Headers
generateHeaders
(
String
httpMethod
,
String
requestUri
){
//生成签名的时间戳
String
headerTimestamp
=
String
.
valueOf
(
LocalDateTime
.
now
().
atZone
(
ZoneId
.
systemDefault
()).
toEpochSecond
());
//生成随机正整数(暂定八位
String
randomNumber
=
String
.
valueOf
((
int
)(
Math
.
random
()
*
99999999
)
+
1
);
//生成签名
String
signature
=
sign
(
tencentSecretId
,
tencentSecretKey
,
httpMethod
,
randomNumber
,
headerTimestamp
,
requestUri
,
"{}"
);
Headers
headers
=
new
Headers
.
Builder
()
.
add
(
"Content-Type"
,
"application/json"
)
.
add
(
"X-TC-Key"
,
tencentSecretId
)
.
add
(
"X-TC-Timestamp"
,
headerTimestamp
)
.
add
(
"X-TC-Nonce"
,
randomNumber
)
.
add
(
"X-TC-Signature"
,
signature
)
.
add
(
"AppId"
,
tencentAppId
)
.
add
(
"SdkId"
,
tencentSdkId
)
.
add
(
"X-TC-Registered"
,
"1"
)
.
build
();
return
headers
;
}
/**
* 生成签名,开发版本oracle jdk 1.8.0_221
*
* @param secretId 邮件下发的secret_id
* @param secretKey 邮件下发的secret_key
* @param httpMethod http请求方法 GET/POST/PUT等
* @param headerNonce X-TC-Nonce请求头,随机数
* @param headerTimestamp X-TC-Timestamp请求头,当前时间的秒级时间戳
* @param requestUri 请求uri,eg:/v1/meetings
* @param requestBody 请求体,没有的设为空串
* @return 签名,需要设置在请求头X-TC-Signature中
* @throws NoSuchAlgorithmException e
* @throws InvalidKeyException e
*/
private
static
String
sign
(
String
secretId
,
String
secretKey
,
String
httpMethod
,
String
headerNonce
,
String
headerTimestamp
,
String
requestUri
,
String
requestBody
)
{
String
tobeSig
=
httpMethod
+
"\nX-TC-Key="
+
secretId
+
"&X-TC-Nonce="
+
headerNonce
+
"&X-TC-Timestamp="
+
headerTimestamp
+
"\n"
+
requestUri
+
"\n"
+
requestBody
;
Mac
mac
;
try
{
mac
=
Mac
.
getInstance
(
HMAC_ALGORITHM
);
SecretKeySpec
secretKeySpec
=
new
SecretKeySpec
(
secretKey
.
getBytes
(
StandardCharsets
.
UTF_8
),
mac
.
getAlgorithm
());
mac
.
init
(
secretKeySpec
);
}
catch
(
Exception
e
)
{
log
.
error
(
"获取腾会签名错误:"
+
e
.
getMessage
());
throw
new
RuntimeException
(
"请求失败"
);
}
byte
[]
hash
=
mac
.
doFinal
(
tobeSig
.
getBytes
(
StandardCharsets
.
UTF_8
));
String
hexHash
=
bytesToHex
(
hash
);
String
signature
=
new
String
(
Base64
.
getEncoder
().
encode
(
hexHash
.
getBytes
(
StandardCharsets
.
UTF_8
)));
return
signature
;
}
private
static
String
bytesToHex
(
byte
[]
bytes
)
{
char
[]
buf
=
new
char
[
bytes
.
length
*
2
];
int
index
=
0
;
for
(
byte
b
:
bytes
)
{
buf
[
index
++]
=
HEX_CHAR
[
b
>>>
4
&
0xf
];
buf
[
index
++]
=
HEX_CHAR
[
b
&
0xf
];
}
return
new
String
(
buf
);
}
}
}
src/main/java/com/cmeeting/service/impl/WeComServiceImpl.java
浏览文件 @
8a23a94c
package
com
.
cmeeting
.
service
.
impl
;
package
com
.
cmeeting
.
service
.
impl
;
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.google.gson.JsonArray
;
import
com.google.gson.JsonObject
;
import
okhttp3.OkHttpClient
;
import
okhttp3.Request
;
import
okhttp3.Response
;
import
org.json.JSONObject
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
java.io.BufferedReader
;
import
java.io.IOException
;
import
java.io.InputStreamReader
;
import
java.net.HttpURLConnection
;
import
java.net.URL
;
import
java.util.Collection
;
import
java.util.List
;
import
java.util.List
;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.
convertJsonToWeComUsers
;
import
static
com
.
cmeeting
.
WeComAndTencentMeeting
.
markDuplicateNames
;
@Service
@Service
public
class
WeComServiceImpl
implements
WeComService
{
public
class
WeComServiceImpl
extends
ServiceImpl
<
WeComUserMapper
,
WeComUser
>
implements
WeComService
{
@Autowired
private
static
final
String
BASE_URL
=
"https://qyapi.weixin.qq.com/cgi-bin/user/simplelist"
;
private
static
final
String
ACCESS_TOKEN
=
"z8g0fkzlVBMKrwi67CRwE7gcnGqjOMrTK6LDih06vs1ixpV9BkrVWQJkoF3HFf5a2UrMvTKEBewOo7wPGvVxNjV_ZGOp_dUxTfnCG6kRUK9LKXPlhskGOI3mcI3ZZpGqpPcgjCsE877hh1BYTB_i3t5X_q1tZfv0K636o3a0HyAMf4GlzviCd1vQHfkGf3p15FjnATwDV8ddhMZo8U-_uA"
;
private
static
final
String
CORP_ID
=
"wx34544d057db97ffd"
;
private
static
final
String
CORP_SECRET
=
"7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0"
;
@Resource
private
WeComUserMapper
weComUserMapper
;
private
WeComUserMapper
weComUserMapper
;
@Override
@Override
public
void
batchInsert
(
List
<
WeComUser
>
users
)
{
public
void
batchInsert
(
List
<
WeComUser
>
users
)
{
weComUserMapper
.
batchInsert
(
users
);
weComUserMapper
.
batchInsert
(
users
);
}
}
@Override
public
void
doUsers
()
throws
Exception
{
// 示例:获取部门ID为6的用户列表
int
departmentId
=
6
;
JsonObject
departmentList
=
getDepartmentList
();
JsonObject
result
=
getUserListByDepartment
(
departmentId
);
// 处理返回结果
if
(
result
.
get
(
"errcode"
).
getAsInt
()
==
0
)
{
System
.
out
.
println
(
"企微获取用户列表成功:"
);
JsonArray
userList
=
result
.
getAsJsonArray
(
"userlist"
);
for
(
int
i
=
0
;
i
<
userList
.
size
();
i
++)
{
JsonObject
user
=
userList
.
get
(
i
).
getAsJsonObject
();
System
.
out
.
println
(
"姓名: "
+
user
.
get
(
"name"
).
getAsString
()
+
", UserID: "
+
user
.
get
(
"userid"
).
getAsString
());
}
// 2. 转换为WeComUser集合
List
<
WeComUser
>
users
=
convertJsonToWeComUsers
(
result
);
// 3. 检查重名并设置标志
markDuplicateNames
(
users
);
// 4. 批量插入数据库
batchInsert
(
users
);
}
}
@Override
public
String
getToken
()
{
//获取token
// String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo";
String
url
=
"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="
+
CORP_ID
+
"&corpsecret="
+
CORP_SECRET
;
String
accessToken
=
""
;
OkHttpClient
client
=
new
OkHttpClient
();
Request
request
=
new
Request
.
Builder
()
.
url
(
url
)
.
get
()
.
build
();
try
(
Response
response
=
client
.
newCall
(
request
).
execute
())
{
if
(
response
.
isSuccessful
())
{
String
responseBody
=
response
.
body
().
string
();
JSONObject
jsonResponse
=
new
JSONObject
(
responseBody
);
accessToken
=
jsonResponse
.
getString
(
"access_token"
);
}
else
{
throw
new
RuntimeException
(
"Failed to fetch token. HTTP Code: "
+
response
.
code
());
}
}
catch
(
IOException
e
)
{
throw
new
RuntimeException
(
e
);
}
return
accessToken
;
}
/**
* 获取企微部门列表
*
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public
JsonObject
getDepartmentList
()
throws
Exception
{
String
urlStr
=
"https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token="
+
getToken
();
URL
url
=
new
URL
(
urlStr
);
HttpURLConnection
conn
=
(
HttpURLConnection
)
url
.
openConnection
();
conn
.
setRequestMethod
(
"GET"
);
int
responseCode
=
conn
.
getResponseCode
();
if
(
responseCode
==
HttpURLConnection
.
HTTP_OK
)
{
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
conn
.
getInputStream
()));
String
inputLine
;
StringBuilder
response
=
new
StringBuilder
();
while
((
inputLine
=
in
.
readLine
())
!=
null
)
{
response
.
append
(
inputLine
);
}
in
.
close
();
// 使用Gson解析JSON响应
Gson
gson
=
new
Gson
();
return
gson
.
fromJson
(
response
.
toString
(),
JsonObject
.
class
);
}
else
{
throw
new
RuntimeException
(
"HTTP GET请求失败,错误码: "
+
responseCode
);
}
}
/**
* 根据企业微信部门ID获取用户列表
*
* @param departmentId 部门ID
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public
JsonObject
getUserListByDepartment
(
int
departmentId
)
throws
Exception
{
String
urlStr
=
BASE_URL
+
"?access_token="
+
ACCESS_TOKEN
+
"&department_id="
+
departmentId
;
URL
url
=
new
URL
(
urlStr
);
HttpURLConnection
conn
=
(
HttpURLConnection
)
url
.
openConnection
();
conn
.
setRequestMethod
(
"GET"
);
int
responseCode
=
conn
.
getResponseCode
();
if
(
responseCode
==
HttpURLConnection
.
HTTP_OK
)
{
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
conn
.
getInputStream
()));
String
inputLine
;
StringBuilder
response
=
new
StringBuilder
();
while
((
inputLine
=
in
.
readLine
())
!=
null
)
{
response
.
append
(
inputLine
);
}
in
.
close
();
// 使用Gson解析JSON响应
Gson
gson
=
new
Gson
();
return
gson
.
fromJson
(
response
.
toString
(),
JsonObject
.
class
);
}
else
{
throw
new
RuntimeException
(
"HTTP GET请求失败,错误码: "
+
responseCode
);
}
}
}
}
src/main/java/com/cmeeting/vo/TencentMeetingVO.java
0 → 100644
浏览文件 @
8a23a94c
// vo/LoginVO.java
package
com
.
cmeeting
.
vo
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
@Data
@Builder
public
class
TencentMeetingVO
{
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public
static
class
TencentMeetingRequest
{
private
Long
startTime
;
//开始结束时间戳
private
Long
endTime
;
private
Integer
pageSize
;
private
Integer
page
;
private
String
operatorId
;
private
Integer
operatorIdType
;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public
static
class
RecordFile
{
private
String
meetingId
;
//如果是周期会议,这个id表示主会议
private
String
subMeetingId
;
//如果是周期会议,这个id表示子会议
private
String
recordFileId
;
}
}
\ No newline at end of file
src/main/resources/application.properties
浏览文件 @
8a23a94c
...
@@ -28,4 +28,23 @@ mybatis.type-aliases-package=com.cmeeting.pojo\
...
@@ -28,4 +28,23 @@ mybatis.type-aliases-package=com.cmeeting.pojo\
# ??????
# ??????
mybatis.configuration.map-underscore-to-camel-case: true
mybatis.configuration.map-underscore-to-camel-case: true
logging.level.com.zaxxer.hikari
=
INFO
logging.level.com.zaxxer.hikari
=
INFO
\ No newline at end of file
# tencent meeting
# local
tencent.appId
=
211153201
tencent.sdkId
=
28370276340
tencent.secretId
=
BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6
tencent.secretKey
=
3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO
#tencent.admin.userId=woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA
tencent.admin.userId
=
woaJARCQAAhkyWGuf8n9InhZsxQstjjA
# prod
#tencent.appId=210468336
#tencent.sdkId=28790143843
#tencent.secretId=0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL
#tencent.secretKey=gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy
#tencent.admin.userId=woaJARCQAAftcvU6GGoOn66rdSZ4IrOA
email.sender
=
cmeeting_assistant@cimc.com
email.sender.pwd
=
scyou@xih45g6@xih4
email.smtp.host
=
smtp.office365.com
src/main/resources/mapper/primary/TecentMeetingMapper.xml
浏览文件 @
8a23a94c
...
@@ -14,6 +14,8 @@
...
@@ -14,6 +14,8 @@
#{user.isrepeatName}
#{user.isrepeatName}
)
)
</foreach>
</foreach>
ON DUPLICATE KEY UPDATE
user_name = VALUES(user_name)
</insert>
</insert>
<select
id=
"getAlluser"
resultType=
"com.cmeeting.pojo.TencentMeetingUser"
>
<select
id=
"getAlluser"
resultType=
"com.cmeeting.pojo.TencentMeetingUser"
>
...
...
src/main/resources/mapper/primary/WeComUserMapper.xml
浏览文件 @
8a23a94c
...
@@ -13,15 +13,18 @@
...
@@ -13,15 +13,18 @@
</insert>
</insert>
<insert
id=
"batchInsert"
parameterType=
"list"
>
<insert
id=
"batchInsert"
parameterType=
"list"
>
INSERT INTO user_wecom ( user_name, user_id, is
epeat_name
)
INSERT INTO user_wecom ( user_name, user_id, is
repeat_name,email
)
VALUES
VALUES
<foreach
collection=
"list"
item=
"user"
separator=
","
>
<foreach
collection=
"list"
item=
"user"
separator=
","
>
(
(
#{user.userName},
#{user.userName},
#{user.userId},
#{user.userId},
#{user.isRepeatName}
#{user.isRepeatName},
#{user.email}
)
)
</foreach>
</foreach>
ON DUPLICATE KEY UPDATE
user_name = VALUES(user_name)
</insert>
</insert>
<select
id=
"getAlluser"
resultType=
"com.cmeeting.pojo.WeComUser"
>
<select
id=
"getAlluser"
resultType=
"com.cmeeting.pojo.WeComUser"
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论