|
@@ -1,9 +1,11 @@
|
|
|
package com.fs.app.service;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
+import com.fs.common.core.redis.RedisCache;
|
|
|
import com.fs.common.utils.DateUtils;
|
|
|
import com.fs.company.service.ICompanyConfigService;
|
|
|
import com.fs.qw.domain.*;
|
|
|
+import com.fs.qw.mapper.QwCompanyMapper;
|
|
|
import com.fs.qw.mapper.QwUserMapper;
|
|
|
import com.fs.qw.param.QwAutoRulesTagsParams;
|
|
|
import com.fs.qw.service.*;
|
|
@@ -12,17 +14,33 @@ import com.fs.qwApi.Result.QwOpenidResult;
|
|
|
import com.fs.qwApi.domain.QwResult;
|
|
|
import com.fs.qwApi.param.QwEditUserTagParam;
|
|
|
import com.fs.qwApi.param.QwOpenidByUserParams;
|
|
|
+import com.google.gson.JsonObject;
|
|
|
+import com.google.gson.JsonParser;
|
|
|
+import com.tencent.wework.Finance;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.json.JSONObject;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.vosk.LibVosk;
|
|
|
+import org.vosk.LogLevel;
|
|
|
+import org.vosk.Model;
|
|
|
+import org.vosk.Recognizer;
|
|
|
import org.w3c.dom.Document;
|
|
|
import org.w3c.dom.Element;
|
|
|
import org.w3c.dom.NodeList;
|
|
|
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import java.io.*;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.KeyFactory;
|
|
|
+import java.security.PrivateKey;
|
|
|
+import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.*;
|
|
|
|
|
|
+@Slf4j
|
|
|
@Service
|
|
|
public class QwDataCallbackService {
|
|
|
|
|
@@ -55,6 +73,10 @@ public class QwDataCallbackService {
|
|
|
|
|
|
@Autowired
|
|
|
private IQwDeptService qwDeptService;
|
|
|
+ @Autowired
|
|
|
+ private RedisCache redisCache;
|
|
|
+ @Autowired
|
|
|
+ private QwCompanyMapper qwCompanyMapper;
|
|
|
|
|
|
|
|
|
|
|
@@ -476,6 +498,288 @@ public class QwDataCallbackService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 拉取存档内容
|
|
|
+ * @param
|
|
|
+ * @return 聊天内容列表
|
|
|
+ */
|
|
|
+ public List<String> getChatData(String corpId) {
|
|
|
+ QwCompany qwCompany= qwCompanyMapper.selectQwCompanyByCorpId(corpId);
|
|
|
+ // 初始化 SDK
|
|
|
+ long sdk = Finance.NewSdk();
|
|
|
+ long slice = Finance.NewSlice();
|
|
|
+ List<String> chatContents = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ long seq = 0L;
|
|
|
+ if (redisCache.redisTemplate.hasKey("seq:"+corpId+":")) {
|
|
|
+ seq = redisCache.getCacheObject("seq:"+corpId+":");
|
|
|
+ }
|
|
|
+ System.out.println("seq:"+seq);
|
|
|
+ // SDK 初始化参数
|
|
|
+ String secret = qwCompany.getMsgSecret();
|
|
|
+ long limit = 1000L; // 一次最多拉取 1000 条
|
|
|
+ String proxy = "";
|
|
|
+ String passwd = "";
|
|
|
+ long timeout = 15L;
|
|
|
+ long ret = Finance.Init(sdk, corpId, secret);
|
|
|
+ if (ret != 0) {
|
|
|
+ Finance.DestroySdk(sdk);
|
|
|
+ System.out.println("初始化 SDK 失败,错误码1:" + ret);
|
|
|
+ return chatContents;
|
|
|
+ }
|
|
|
+ // 拉取会话存档数据
|
|
|
+ ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
|
|
|
+ if (ret != 0) {
|
|
|
+ System.out.println("拉取会话存档失败,错误码:" + ret);
|
|
|
+ return chatContents;
|
|
|
+ }
|
|
|
+ // 获取 JSON 内容
|
|
|
+ String jsonContent = Finance.GetContentFromSlice(slice);
|
|
|
+ if (jsonContent == null || jsonContent.isEmpty()) {
|
|
|
+ System.out.println("没有获取到有效的会话存档数据");
|
|
|
+ return chatContents;
|
|
|
+ }
|
|
|
+ // 解析 JSON 内容
|
|
|
+ JSONObject json = new JSONObject(jsonContent);
|
|
|
+ System.out.println("解析的内容"+json.toString());
|
|
|
+ if (json.has("chatdata")) {
|
|
|
+ for (Object item : json.getJSONArray("chatdata")) {
|
|
|
+ JSONObject chatItem = (JSONObject) item;
|
|
|
+ String encrypt_random_key = chatItem.getString("encrypt_random_key"); // 获取加密的随机密钥
|
|
|
+ String encrypt_chat_msg = chatItem.getString("encrypt_chat_msg"); // 获取加密消息
|
|
|
+ // 更新下一次拉取的 seq
|
|
|
+ if (chatItem.has("seq")) {
|
|
|
+ long nextSeq = chatItem.getLong("seq");
|
|
|
+ System.out.println("将 seq 存入 Redis,key: seq:, value: " + nextSeq);
|
|
|
+ redisCache.setCacheObject("seq:"+corpId+":", nextSeq); // 确保 `seq` 更新到缓存
|
|
|
+ }
|
|
|
+ // 使用私钥解密 encrypt_random_key 获取 encrypt_key
|
|
|
+ String encrypt_key = decryptRandomKey(encrypt_random_key,qwCompany.getMsgPrivateKey());
|
|
|
+ if (encrypt_key != null) {
|
|
|
+ // 解密加密消息
|
|
|
+ String decryptedMsg = decryptChatMessage(sdk, encrypt_key, encrypt_chat_msg);
|
|
|
+ // 解析 JSON 字符串
|
|
|
+ JsonObject jsonObject = JsonParser.parseString(decryptedMsg).getAsJsonObject();
|
|
|
+ String plaintext = Finance.GetContentFromSlice(slice);
|
|
|
+ // 获取 msgtype 的值
|
|
|
+ String msgtype = jsonObject.get("msgtype").getAsString();
|
|
|
+ // 处理媒体消息
|
|
|
+ if (msgtype.equals("voice")) {
|
|
|
+ getMedia(sdk,decryptedMsg,corpId,secret);
|
|
|
+ }
|
|
|
+ System.out.println("解密后的消息:" + decryptedMsg);
|
|
|
+ chatContents.add(decryptedMsg);
|
|
|
+ } else {
|
|
|
+ System.out.println("解密随机密钥失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ } finally {
|
|
|
+ Finance.FreeSlice(slice);
|
|
|
+ Finance.DestroySdk(sdk);
|
|
|
+ }
|
|
|
+ return chatContents;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解密加密的随机密钥
|
|
|
+ * @param encryptedRandomKey 加密的随机密钥
|
|
|
+ * @return 解密后的随机密钥
|
|
|
+ */
|
|
|
+ public String decryptRandomKey(String encryptedRandomKey,String msgPrivateKey) {
|
|
|
+ try {
|
|
|
+ // 加载私钥
|
|
|
+ PrivateKey privateKey = loadPrivateKey(msgPrivateKey);
|
|
|
+
|
|
|
+ // 使用 RSA 解密
|
|
|
+ Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
|
|
+
|
|
|
+ // 解密数据
|
|
|
+ byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedRandomKey));
|
|
|
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取私钥
|
|
|
+ * @return 私钥
|
|
|
+ * @throws Exception 异常
|
|
|
+ */
|
|
|
+ public PrivateKey loadPrivateKey(String privateKey) throws Exception {
|
|
|
+ // 解析 PEM 格式的私钥
|
|
|
+ String privateKeyPEM = privateKey
|
|
|
+ .replace("-----BEGIN PRIVATE KEY-----", "")
|
|
|
+ .replace("-----END PRIVATE KEY-----", "")
|
|
|
+ .replaceAll("\\s", "");
|
|
|
+ byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
|
|
|
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
|
|
|
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
|
+ return keyFactory.generatePrivate(keySpec);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解密会话消息
|
|
|
+ * @param sdk SDK
|
|
|
+ * @param encrypt_key 解密的密钥
|
|
|
+ * @param encrypt_chat_msg 加密的聊天消息
|
|
|
+ * @return 解密后的消息内容
|
|
|
+ */
|
|
|
+ public String decryptChatMessage(long sdk, String encrypt_key, String encrypt_chat_msg) {
|
|
|
+ String decryptedMsg = "";
|
|
|
+ long msg = Finance.NewSlice();
|
|
|
+ try {
|
|
|
+ // 解密消息
|
|
|
+ int ret = Finance.DecryptData(sdk, encrypt_key, encrypt_chat_msg, msg);
|
|
|
+ if (ret == 0) {
|
|
|
+ decryptedMsg = Finance.GetContentFromSlice(msg); // 获取解密后的消息内容
|
|
|
+ decryptedMsg = new String(decryptedMsg.getBytes(), StandardCharsets.UTF_8);
|
|
|
+ } else {
|
|
|
+ System.out.println("解密失败,错误码:" + ret);
|
|
|
+ return null; // 返回 null,避免上游加入错误数据
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ return null;
|
|
|
+ } finally {
|
|
|
+ Finance.FreeSlice(msg);
|
|
|
+ }
|
|
|
+ return decryptedMsg;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载语音文件
|
|
|
+ * @param sdk SDK实例
|
|
|
+ * @param decryptedMsg 解密后的JSON字符串
|
|
|
+ * @return 转换以后的文字数据
|
|
|
+ */
|
|
|
+ public static String getMedia(long sdk, String decryptedMsg,String coreId,String secret) {
|
|
|
+ JsonObject jsonObject = JsonParser.parseString(decryptedMsg).getAsJsonObject();
|
|
|
+ if (!jsonObject.has("voice")) {
|
|
|
+ log.error("JSON 数据中缺少 voice 字段");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JsonObject voiceObj = jsonObject.getAsJsonObject("voice");
|
|
|
+ String sdkFileId = voiceObj.get("sdkfileid").getAsString();
|
|
|
+ long mediaData = Finance.NewMediaData();
|
|
|
+ byte[] amrData = downloadVoice(sdk, sdkFileId, "", mediaData,coreId,secret);
|
|
|
+ Finance.FreeMediaData(mediaData);
|
|
|
+ if (amrData == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ // 直接转换 AMR 数据到文本
|
|
|
+ try {
|
|
|
+ String s = convertAmrToText(amrData);
|
|
|
+ log.info("转换成功的文字{}",s);
|
|
|
+ return s;
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("语音转换失败: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * 递归下载语音文件
|
|
|
+ * @param sdk SDK实例
|
|
|
+ * @param sdkFileId 语音文件ID
|
|
|
+ * @param indexbuf 获取分片数据时的索引
|
|
|
+ * @param mediaData 存储语音数据的结构体
|
|
|
+ * @return 下载的语音文件字节
|
|
|
+ */
|
|
|
+ private static byte[] downloadVoice(long sdk, String sdkFileId, String indexbuf, long mediaData, String corpId, String secret) {
|
|
|
+ long ret = Finance.Init(sdk, corpId, secret);
|
|
|
+ if (ret != 0) {
|
|
|
+ Finance.DestroySdk(sdk);
|
|
|
+ System.out.println("初始化 SDK 失败,错误码:" + ret);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ ret = Finance.GetMediaData(sdk, indexbuf, sdkFileId, "", "", 15L, mediaData);
|
|
|
+ if (ret != 0) {
|
|
|
+ log.error("获取语音数据失败: ret={}, sdkFileId={}", ret, sdkFileId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ boolean isFinish = Finance.IsMediaDataFinish(mediaData) == 1;
|
|
|
+ String nextIndexBuf = Finance.GetOutIndexBuf(mediaData);
|
|
|
+ byte[] data = Finance.GetData(mediaData);
|
|
|
+ if (!isFinish) {
|
|
|
+ byte[] nextData = downloadVoice(sdk, sdkFileId, nextIndexBuf, mediaData,corpId,secret);
|
|
|
+ if (nextData != null) {
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ try {
|
|
|
+ outputStream.write(data);
|
|
|
+ outputStream.write(nextData);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("合并语音数据失败: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 ffmpeg 将 AMR 文件转换为 WAV 格式
|
|
|
+ * @return WAV 格式的音频数据
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private static String convertAmrToText(byte[] amrData) throws IOException {
|
|
|
+ // 启动 ffmpeg 进行转换
|
|
|
+ ProcessBuilder processBuilder = new ProcessBuilder(
|
|
|
+ "ffmpeg",
|
|
|
+ "-i", "pipe:0", // 从标准输入读取 AMR 数据
|
|
|
+ "-ac", "1",
|
|
|
+ "-ar", "16000",
|
|
|
+ "-sample_fmt", "s16",
|
|
|
+ "-f", "wav",
|
|
|
+ "pipe:1"
|
|
|
+ );
|
|
|
+ processBuilder.redirectErrorStream(true);
|
|
|
+ Process process = processBuilder.start();
|
|
|
+ // 向 ffmpeg 传输 AMR 数据
|
|
|
+ try (OutputStream outputStream = process.getOutputStream()) {
|
|
|
+ outputStream.write(amrData);
|
|
|
+ }
|
|
|
+ // 读取 ffmpeg 转换后的 WAV 数据
|
|
|
+ ByteArrayOutputStream wavOutput = new ByteArrayOutputStream();
|
|
|
+ try (InputStream inputStream = process.getInputStream()) {
|
|
|
+ byte[] buffer = new byte[4096];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
+ wavOutput.write(buffer, 0, bytesRead);
|
|
|
+ }
|
|
|
+ int exitCode = process.waitFor();
|
|
|
+ if (exitCode != 0) {
|
|
|
+ throw new IOException("ffmpeg 转换失败,退出码:" + exitCode);
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ throw new IOException("ffmpeg 转换过程中被中断", e);
|
|
|
+ }
|
|
|
+ return recognizeSpeech(wavOutput.toByteArray());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 Vosk 识别音频
|
|
|
+ */
|
|
|
+ private static String recognizeSpeech(byte[] wavData) throws IOException {
|
|
|
+ LibVosk.setLogLevel(LogLevel.DEBUG);
|
|
|
+ try (Model model = new Model("C:/vosk-model-small-cn-0.22");
|
|
|
+ //try (Model model = new Model("E:/vosk-model/vosk-model-small-cn-0.22");
|
|
|
+ InputStream ais = new ByteArrayInputStream(wavData);
|
|
|
+ Recognizer recognizer = new Recognizer(model, 16000)) {
|
|
|
+ byte[] buffer = new byte[4096];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = ais.read(buffer)) >= 0) {
|
|
|
+ recognizer.acceptWaveForm(buffer, bytesRead);
|
|
|
+ }
|
|
|
+ return recognizer.getFinalResult();
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
}
|