Procházet zdrojové kódy

益寿缘ai语音复刻接入豆包
-声纹录入按sys_config
-sop生成语音接入豆包
-企微ai回复接入豆包
今正对接银川互联网医院
-对接咨询问诊接口
-对接处方下发接口
-对接医生排班接口

lk před 1 měsícem
rodič
revize
669e036509
63 změnil soubory, kde provedl 5091 přidání a 363 odebrání
  1. 367 0
      fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java
  2. 154 0
      fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java
  3. 181 0
      fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java
  4. 23 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java
  5. 34 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java
  6. 27 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java
  7. 42 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java
  8. 21 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java
  9. 20 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java
  10. 33 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java
  11. 13 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java
  12. 64 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java
  13. 54 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java
  14. 12 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java
  15. 39 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java
  16. 57 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java
  17. 50 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java
  18. 488 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java
  19. 343 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java
  20. 44 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java
  21. 47 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java
  22. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  23. 99 0
      fs-service/src/main/java/com/fs/company/param/VcCompanyUser.java
  24. 33 0
      fs-service/src/main/java/com/fs/course/param/HsBookDoctorInfoParam.java
  25. 66 0
      fs-service/src/main/java/com/fs/course/param/HsDoctorSchedule.java
  26. 67 0
      fs-service/src/main/java/com/fs/course/param/HsDrug.java
  27. 41 0
      fs-service/src/main/java/com/fs/course/param/HsDrugExtend.java
  28. 35 0
      fs-service/src/main/java/com/fs/course/param/HsHealthyCondition.java
  29. 38 0
      fs-service/src/main/java/com/fs/course/param/HsRedirectParam.java
  30. 36 0
      fs-service/src/main/java/com/fs/course/param/HsUserInfoParam.java
  31. 86 0
      fs-service/src/main/java/com/fs/course/param/HsUserMember.java
  32. 36 0
      fs-service/src/main/java/com/fs/course/param/InquiryOrderHsLog.java
  33. 34 0
      fs-service/src/main/java/com/fs/course/param/SendInquiryParam.java
  34. 75 0
      fs-service/src/main/java/com/fs/course/param/SendInquiryToHSParam.java
  35. 9 4
      fs-service/src/main/java/com/fs/course/vo/FsInquiryPatientInfoListUVO.java
  36. 14 1
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  37. 4 4
      fs-service/src/main/java/com/fs/his/domain/FsDoctorPrescribe.java
  38. 4 6
      fs-service/src/main/java/com/fs/his/domain/FsDoctorPrescribeDrug.java
  39. 168 148
      fs-service/src/main/java/com/fs/his/domain/FsInquiryOrder.java
  40. 23 0
      fs-service/src/main/java/com/fs/his/domain/FsPatient.java
  41. 26 0
      fs-service/src/main/java/com/fs/his/mapper/FsInquiryOrderMapper.java
  42. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreProductMapper.java
  43. 21 0
      fs-service/src/main/java/com/fs/his/service/IFsInquiryPatientInfoService.java
  44. 27 12
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java
  45. 920 8
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryPatientInfoServiceImpl.java
  46. 27 0
      fs-service/src/main/java/com/fs/hisStore/domain/HsPrescribScrm.java
  47. 79 0
      fs-service/src/main/java/com/fs/hisStore/domain/MedicalRecord.java
  48. 46 0
      fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionDeliveryItem.java
  49. 66 0
      fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionInfo.java
  50. 63 0
      fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionInfoDetails.java
  51. 5 1
      fs-service/src/main/java/com/fs/hisStore/enums/SysConfigEnum.java
  52. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/FsPrescribeParam.java
  53. 10 7
      fs-service/src/main/java/com/fs/hisStore/service/IFsPrescribeScrmService.java
  54. 117 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsPrescribeScrmServiceImpl.java
  55. 1 1
      fs-service/src/main/java/com/fs/wxwork/dto/WxwSilkVoceDTO.java
  56. 2 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java
  57. 122 64
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java
  58. 16 0
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  59. 22 0
      fs-service/src/main/resources/mapper/course/FsCourseTrafficLogMapper.xml
  60. 42 0
      fs-service/src/main/resources/mapper/his/FsInquiryOrderMapper.xml
  61. 4 0
      fs-service/src/main/resources/mapper/his/FsStoreProductMapper.xml
  62. 420 99
      fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  63. 65 8
      fs-user-app/src/main/java/com/fs/app/controller/InquiryPatientInfoController.java

+ 367 - 0
fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java

@@ -0,0 +1,367 @@
+package com.fs.common.utils;
+
+import cn.hutool.json.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 红杉健康平台加解密工具类
+ */
+public class HsCryptoUtil {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
+
+    /**
+     * 加密方法
+     * @param data 待加密的数据对象
+     * @param appSecret 应用密钥,用于AES加密的IV
+     * @param publicKeyStr RSA公钥(Base64编码)
+     * @return 加密结果Map,包含content、key、sign三个字段
+     */
+    public static Map<String, String> encrypt(JSONObject data, String appSecret, String publicKeyStr) {
+        try {
+//            公钥解码base64
+            String publicKey = new String( Base64.decodeBase64(publicKeyStr), StandardCharsets.UTF_8);
+            publicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace(
+                    "\n" +
+                            "-----END PUBLIC KEY-----\n", ""
+            );
+            // 1. 将对象转换为按ASCII码排序的Map
+
+            Map<String, Object> sortedParams = new TreeMap<>();
+            /*处理掉空值,否则转jsonString要报错*/
+            for (Map.Entry<String, Object> entry : data.entrySet()) {
+                if ( entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    Object processedValue = processNestedObjects(entry.getValue());
+                    sortedParams.put(entry.getKey(), processedValue);
+                }
+            }
+            String contentStr = objectMapper.writeValueAsString(sortedParams);
+//            // 直接转换 JSONObject 为 Map
+//            Map<String, Object> sortedParams = convertToSortedMap(data);
+//
+//            // 2. 转换为JSON字符串(contentStr)
+//            String contentStr = objectMapper.writeValueAsString(sortedParams);
+
+            // 3. 生成32位随机密钥(randStr)
+            String randStr = generateRandStr(32);
+
+            // 4. AES-256-CBC加密contentStr
+            String encryptedContent = aesEncrypt(contentStr, randStr, appSecret);
+
+            // 5. 使用RSA公钥加密randStr
+            String encryptedKey = rsaEncrypt(randStr, publicKey);
+
+            // 6. 对contentStr进行MD5签名
+            String sign = DigestUtils.md5Hex(contentStr);
+
+            // 7. 构造返回结果
+            Map<String, String> result = new TreeMap<>();
+            result.put("content", encryptedContent);
+            result.put("key", encryptedKey);
+            result.put("sign", sign);
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("加密失败", e);
+        }
+    }
+
+    /**
+     * 递归处理嵌套的JSON对象 的jsonNull和null
+     */
+    private static Object processNestedObjects(Object value) {
+        if (value instanceof cn.hutool.json.JSONObject) {
+            // 处理嵌套的JSONObject
+            cn.hutool.json.JSONObject jsonObj = (cn.hutool.json.JSONObject) value;
+            Map<String, Object> nestedMap = new TreeMap<>();
+            for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
+                if (entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    nestedMap.put(entry.getKey(), processNestedObjects(entry.getValue()));
+                }
+            }
+            return nestedMap;
+        } else if (value instanceof cn.hutool.json.JSONArray) {
+            // 处理JSONArray
+            cn.hutool.json.JSONArray jsonArray = (cn.hutool.json.JSONArray) value;
+            List<Object> list = new ArrayList<>();
+            for (Object item : jsonArray) {
+                list.add(processNestedObjects(item));
+            }
+            return list;
+        } else {
+            // 基本类型直接返回
+            return value;
+        }
+    }
+
+    /**
+     * 将对象转换为按ASCII码排序的Map
+     */
+    private static Map<String, Object> convertToSortedMap(Object obj) throws Exception {
+        Map<String, Object> map = objectMapper.convertValue(obj, Map.class);
+        return new TreeMap<>(map);
+    }
+
+    /**
+     * 生成指定长度的随机字符串
+     */
+    private static String generateRandStr(int length) {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        SecureRandom random = new SecureRandom();
+        StringBuilder sb = new StringBuilder(length);
+
+        for (int i = 0; i < length; i++) {
+            sb.append(chars.charAt(random.nextInt(chars.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * AES-256-CBC加密
+     */
+    private static String aesEncrypt(String content, String randStr, String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+        // 使用appSecret的前16位作为IV
+        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+        byte[] encrypted = cipher.doFinal(content.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * RSA加密
+     */
+    private static String rsaEncrypt(String randStr, String publicKey) throws Exception {
+        byte[] decodedPublicKey = Base64.decodeBase64(publicKey);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedPublicKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PublicKey pubKey = keyFactory.generatePublic(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
+        byte[] encrypted = cipher.doFinal(randStr.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * 解密方法
+     * @param encryptedContent AES加密的内容(Base64编码)
+     * @param encryptedKey RSA加密的密钥(Base64编码)
+     * @param sign 签名(MD5)
+     * @param appSecret 应用密钥,用于AES解密的IV
+     * @param privateKeyStr RSA私钥(PEM格式,Base64编码)
+     * @return 解密后的原始数据对象
+     */
+    public static Map<String, Object> decrypt(String encryptedContent,
+                                              String encryptedKey,
+                                              String sign,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        try {
+            // 1. Base64解码私钥字符串,得到PEM格式的私钥
+            String privateKeyPEM = new String(
+                    Base64.decodeBase64(privateKeyStr),
+                    StandardCharsets.UTF_8
+            );
+            // 1. RSA解密获取AES密钥(randStr)
+            String randStr = rsaDecrypt(encryptedKey, privateKeyPEM);
+
+            // 2. AES解密获取contentStr
+            String contentStr = aesDecrypt(encryptedContent, randStr, appSecret);
+
+            // 3. 验证签名
+            String calculatedSign = DigestUtils.md5Hex(contentStr);
+            if (!calculatedSign.equals(sign)) {
+                throw new RuntimeException("签名验证失败,数据可能被篡改");
+            }
+
+            // 4. 将contentStr解析为Map
+            return objectMapper.readValue(contentStr, Map.class);
+
+        } catch (Exception e) {
+            throw new RuntimeException("解密失败", e);
+        }
+    }
+
+    /**
+     * 解密并验证的完整方法
+     * @param encryptedData 加密数据Map,包含content、key、sign三个字段
+     * @param appSecret 应用密钥
+     * @param privateKeyStr RSA私钥
+     * @return 解密后的原始数据Map
+     */
+    public static Map<String, Object> decrypt(JsonNode encryptedData,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        return decrypt(
+                encryptedData.get("content").asText(),
+                encryptedData.get("key").asText(),
+                encryptedData.get("sign").asText(),
+                appSecret,
+                privateKeyStr
+        );
+    }
+
+    /**
+     * RSA解密 - 使用私钥解密AES密钥
+     */
+    private static String rsaDecrypt(String encryptedKey, String privateKeyStr) throws Exception {
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+        // 处理PEM格式的私钥
+        String privateKeyPEM = privateKeyStr
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s+", "");
+
+        byte[] decodedPrivateKey = Base64.decodeBase64(privateKeyPEM);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedKey);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES-256-CBC解密
+     */
+    private static String aesDecrypt(String encryptedContent,
+                                     String randStr,
+                                     String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(StandardCharsets.UTF_8), "AES");
+
+        // 使用appSecret的前16位作为IV(与加密时一致)
+        String iv = appSecret.length() >= 16 ? appSecret.substring(0, 16) : appSecret;
+        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedContent);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 验证签名(不进行解密)
+     */
+    public static boolean verifySignature(String contentStr, String sign) {
+        String calculatedSign = DigestUtils.md5Hex(contentStr);
+        return calculatedSign.equals(sign);
+    }
+
+    /**
+     * 单独解密AES密钥(用于调试)
+     */
+    public static String decryptKeyOnly(String encryptedKey, String privateKeyStr) throws Exception {
+        return rsaDecrypt(encryptedKey, privateKeyStr);
+    }
+
+    /**
+     * 单独解密内容(需要提供AES密钥)
+     */
+    public static String decryptContentOnly(String encryptedContent,
+                                            String randStr,
+                                            String appSecret) throws Exception {
+        return aesDecrypt(encryptedContent, randStr, appSecret);
+    }
+
+
+//    /**
+//     * 解密方法
+//     * @param encryptedData 加密的数据Map,包含content、key、sign三个字段
+//     * @param appSecret 应用密钥,用于AES解密的IV
+//     * @param privateKeyStr RSA私钥(Base64编码)
+//     * @return 解密后的原始数据对象
+//     */
+//    public static Map<String, Object> decrypt(JsonNode encryptedData, String appSecret, String privateKeyStr) {
+//        //            私钥解码base64
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+//        privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----\n", "").replace(
+//                "\n" +
+//                        "-----END PRIVATE KEY-----", ""
+//        );
+//        try {
+//            String content = String.valueOf(encryptedData.get("content"));
+//            String key = String.valueOf(encryptedData.get("key"));
+//            String sign = String.valueOf(encryptedData.get("sign"));
+//
+//            // 1. 使用RSA私钥解密key字段,得到randStr
+//            String randStr = rsaDecrypt(key, privateKey);
+//
+//            // 2. 使用AES-256-CBC解密content字段
+//            String decryptedContent = aesDecrypt(content, randStr, appSecret);
+//
+//            // 3. 验证签名
+//            String calculatedSign = DigestUtils.md5Hex(decryptedContent);
+//            if (!calculatedSign.equals(sign)) {
+//                throw new RuntimeException("签名验证失败");
+//            }
+//
+//            // 4. 将解密后的JSON字符串转换为Map对象
+//            @SuppressWarnings("unchecked")
+//            Map<String, Object> result = objectMapper.readValue(decryptedContent, Map.class);
+//
+//            return result;
+//        } catch (Exception e) {
+//            throw new RuntimeException("解密失败", e);
+//        }
+//    }
+//
+//    /**
+//     * AES-256-CBC解密
+//     */
+//    private static String aesDecrypt(String content, String randStr, String appSecret) throws Exception {
+//        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+//        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+//        // 使用appSecret的前16位作为IV
+//        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+//        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(content));
+//        return new String(decrypted);
+//    }
+//
+//    /**
+//     * RSA解密
+//     */
+//    private static String rsaDecrypt(String encryptedKey, String privateKey) throws Exception {
+//        byte[] decodedPrivateKey = Base64.decodeBase64(privateKey);
+//        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+//        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+//        PrivateKey privKey = keyFactory.generatePrivate(keySpec);
+//
+//        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+//        cipher.init(Cipher.DECRYPT_MODE, privKey);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(encryptedKey));
+//        return new String(decrypted);
+//    }
+}

+ 154 - 0
fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java

@@ -0,0 +1,154 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.lang.TypeReference;
+import cn.hutool.json.JSONUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StringToMapUtil {
+    public static Map<String, Object> toMap(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 尝试JSON解析
+            return JSONUtil.toBean(input, new TypeReference<Map<String, Object>>() {}, false);
+        } catch (Exception e) {
+            // 如果失败,尝试Map.toString()格式解析
+            try {
+                // 检查是否是Map.toString()格式
+                if (input.trim().startsWith("{") && input.trim().endsWith("}") &&
+                        input.contains("=")) {
+                    return parseMapToString(input);
+                }
+            } catch (Exception ex) {
+                // 忽略,抛出原始异常
+            }
+            throw e;
+        }
+    }
+    /**
+     * 将Map.toString()格式的字符串转换为Map
+     * @param mapString 类似 {data={book_no=10381008622, ...}, msg=success, state=0} 的字符串
+     * @return 转换后的Map对象
+     */
+    private static Map<String, Object> parseMapToString(String mapString) {
+        Map<String, Object> resultMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapString.trim();
+        if (content.startsWith("{") && content.endsWith("}")) {
+            content = content.substring(1, content.length() - 1);
+        }
+
+        // 分割顶层键值对
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                if (value.startsWith("{") && value.endsWith("}")) {
+                    // 嵌套Map对象
+                    resultMap.put(key, parseNestedMap(value));
+                } else {
+                    // 简单值
+                    resultMap.put(key, value);
+                }
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 分割顶层键值对,避免分割嵌套对象内的逗号
+     */
+    private static List<String> splitTopLevelPairs(String content) {
+        List<String> pairs = new ArrayList<>();
+        int level = 0;
+        int start = 0;
+
+        for (int i = 0; i < content.length(); i++) {
+            char c = content.charAt(i);
+            if (c == '{') {
+                level++;
+            } else if (c == '}') {
+                level--;
+            } else if (c == ',' && level == 0) {
+                pairs.add(content.substring(start, i));
+                start = i + 1;
+            }
+        }
+
+        // 添加最后一个元素
+        if (start < content.length()) {
+            pairs.add(content.substring(start));
+        }
+
+        return pairs;
+    }
+
+    /**
+     * 解析嵌套的Map对象
+     */
+    private static Map<String, Object> parseNestedMap(String mapStr) {
+        Map<String, Object> nestedMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapStr.substring(1, mapStr.length() - 1);
+
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                // 尝试转换为合适的类型
+                nestedMap.put(key, convertValueType(value));
+            }
+        }
+
+        return nestedMap;
+    }
+
+    /**
+     * 根据值的特点转换为合适的类型
+     */
+    private static Object convertValueType(String value) {
+        // 数字
+        if (value.matches("-?\\d+")) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                // 忽略,继续处理
+            }
+        }
+
+        // 布尔值
+        if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+            return Boolean.parseBoolean(value);
+        }
+
+        // 嵌套对象
+        if (value.startsWith("{") && value.endsWith("}")) {
+            return parseNestedMap(value);
+        }
+
+        // 数组或列表(简化处理)
+        if (value.startsWith("[") && value.endsWith("]")) {
+            return value.substring(1, value.length() - 1).split(",");
+        }
+
+        // 默认作为字符串
+        return value;
+    }
+}

+ 181 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java

@@ -0,0 +1,181 @@
+package com.fs.aiSoundReplication;
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.common.core.domain.R;
+import com.fs.fastgptApi.vo.AudioVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/voice-clone")
+@Api(tags = "声音复刻API")
+public class VoiceCloneController {
+
+    @Autowired
+    private VoiceCloneService voiceCloneService;
+    @Autowired
+    private TtsService ttsService;
+
+    @PostMapping("/synthesize")
+    @ApiOperation("文本转语音")
+    public AudioVO synthesize(
+            @ApiParam(value = "TTS请求参数", required = true)
+            @RequestBody TtsRequest request) {
+        return ttsService.textToSpeech(request);
+    }
+
+    @PostMapping("/synthesize-simple")
+    @ApiOperation("简化版文本转语音")
+    public AudioVO synthesizeSimple(
+            @ApiParam(value = "要合成的文本", required = true)
+            @RequestParam String text,
+            @ApiParam(value = "音色ID", required = true)
+            @RequestParam String voiceType,
+            @ApiParam(value = "音频格式")
+            @RequestParam(required = false, defaultValue = "mp3") String format,
+            @ApiParam(value = "语速 (0-15)")
+            @RequestParam(required = false, defaultValue = "1") Integer speed
+    ) {
+
+        TtsRequest request = new TtsRequest(
+                "", "", voiceType, text); // AppID和Token会在Service中设置
+        request.setReqId(UUID.randomUUID().toString());
+        request.setFormat(format);
+        request.setSpeed(speed);
+        return ttsService.textToSpeech(request);
+    }
+
+//    @PostMapping("/synthesize-and-download")
+//    @ApiOperation("文本转语音并下载")
+//    public R synthesizeAndDownload(
+//            @ApiParam(value = "要合成的文本", required = true)
+//            @RequestParam String text,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            HttpServletRequest httpRequest) {
+//
+//        TtsRequest ttsRequest = new TtsRequest("", "", voiceType, text);
+//        ttsRequest.setReqId(UUID.randomUUID().toString());
+//
+//        String url = ttsService.textToSpeechStream(ttsRequest);
+//
+//        return R.ok();
+//    }
+
+//    @PostMapping("/batch-synthesize")
+//    @ApiOperation("批量文本转语音")
+//    public ResponseEntity<List<File>> batchSynthesize(
+//            @ApiParam(value = "文本列表", required = true)
+//            @RequestBody List<String> texts,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            @ApiParam(value = "是否打包下载")
+//            @RequestParam(required = false, defaultValue = "false") Boolean zip) {
+//
+//        List<File> audioFiles = ttsService.batchTextToSpeech(texts, voiceType);
+//
+//        if (zip && !audioFiles.isEmpty()) {
+//            // 这里可以添加ZIP打包逻辑
+//            // 返回ZIP文件的ResponseEntity
+//        }
+//
+//        return ResponseEntity.ok(audioFiles);
+//    }
+
+//    @PostMapping("/synthesize-with-params")
+//    @ApiOperation("带参数的文本转语音")
+//    public TtsResponse synthesizeWithParams(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String voiceType,
+//            @ApiParam(value = "文本内容", required = true) @RequestParam String text,
+//            @ApiParam(value = "语速 (0-15)") @RequestParam(required = false) Integer speed,
+//            @ApiParam(value = "音量 (0-15)") @RequestParam(required = false) Integer volume,
+//            @ApiParam(value = "音高 (0-15)") @RequestParam(required = false) Integer pitch,
+//            @ApiParam(value = "情感参数") @RequestParam(required = false) String emotion,
+//            @ApiParam(value = "说话风格") @RequestParam(required = false) String speakingStyle) {
+//
+//        TtsRequest request = new TtsRequest("", "", voiceType, text);
+//        request.setReqId(UUID.randomUUID().toString());
+//
+//        if (speed != null) request.setSpeed(speed);
+//        if (volume != null) request.setVolume(volume);
+//        if (pitch != null) request.setPitch(pitch);
+//        if (emotion != null) request.setEmotion(emotion);
+//        if (speakingStyle != null) request.setSpeakingStyle(speakingStyle);
+//
+//        return ttsService.textToSpeech(request);
+//    }
+
+    private String getContentType(String format) {
+        switch (format.toLowerCase()) {
+            case "mp3":
+                return "audio/mpeg";
+            case "wav":
+                return "audio/wav";
+            case "pcm":
+                return "audio/L16";
+            default:
+                return "application/octet-stream";
+        }
+    }
+    @PostMapping("/upload")
+    @ApiOperation("上传音频训练音色")
+    public UploadResponse uploadVoice(
+            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+            @ApiParam(value = "模型类型(1-ICL1.0, 4-ICL2.0)", defaultValue = "4")
+            @RequestParam(required = false) Integer modelType,
+            @ApiParam(value = "语种(0-中文, 1-英文)", defaultValue = "0")
+            @RequestParam(required = false) Integer language) {
+        return voiceCloneService.uploadVoice(speakerId, audioFile, modelType, language);
+    }
+
+    @GetMapping("/status/{speakerId}")
+    @ApiOperation("查询音色训练状态")
+    public StatusResponse getTrainingStatus(
+            @ApiParam(value = "音色ID", required = true)
+            @PathVariable String speakerId) {
+        return voiceCloneService.queryTrainingStatus(speakerId);
+    }
+
+//    @PostMapping("/upload-and-wait")
+//    @ApiOperation("上传并等待训练完成")
+//    public StatusResponse uploadAndWait(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+//            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+//            @ApiParam(value = "模型类型", defaultValue = "4")
+//            @RequestParam(required = false) Integer modelType,
+//            @ApiParam(value = "语种", defaultValue = "0")
+//            @RequestParam(required = false) Integer language,
+//            @ApiParam(value = "最大等待时间(秒)", defaultValue = "600")
+//            @RequestParam(required = false) Integer maxWaitSeconds) {
+//
+//        // 1. 上传音频
+//        UploadResponse uploadResponse = voiceCloneService.uploadVoice(
+//                speakerId, audioFile, modelType, language);
+//
+//        // 2. 计算轮询参数
+//        int maxPollingTimes = maxWaitSeconds != null ? maxWaitSeconds * 1000 / 10000 : 60;
+//
+//        // 3. 轮询训练状态
+//        return voiceCloneService.pollTrainingStatus(
+//                uploadResponse.getSpeakerId(), maxPollingTimes, 10000L);
+//    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java

@@ -0,0 +1,23 @@
+package com.fs.aiSoundReplication.config;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class OkHttpConfig {
+
+    @Bean
+    public OkHttpClient okHttpClient() {
+        return new OkHttpClient.Builder()
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(60, TimeUnit.SECONDS)
+                .writeTimeout(60, TimeUnit.SECONDS)
+                .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
+                .retryOnConnectionFailure(true)
+                .build();
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java

@@ -0,0 +1,34 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone.tts")
+public class TtsConfig {
+    // HTTP TTS接口地址
+    private String httpUrl = "https://openspeech.bytedance.com/api/v1/tts";
+
+    // 默认参数
+    private String defaultFormat = "mp3";
+    private Integer defaultSampleRate = 24000;
+    private Integer defaultSpeed = 10;
+    private Integer defaultVolume = 10;
+    private Integer defaultPitch = 10;
+    private String defaultCluster = "volcano_icl";
+
+    // 文本长度限制
+    private Integer maxTextLength = 500; // 最大文本长度
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 2000L;
+
+    // 音频保存路径
+    private String audioSavePath = "./audio/";
+
+    // 是否自动保存音频文件
+    private Boolean autoSave = true;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java

@@ -0,0 +1,27 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone")
+public class VoiceCloneConfig {
+    private String accessToken = "IVX_Rlt6r93upGb-_vy0QlxaK_dhQzDY";//正式环境需要换成公司豆包的信息
+    private String appId = "8243948690";//正式环境需要换成公司豆包的信息
+
+    // API地址
+    private String uploadUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
+    private String statusUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/status";
+
+    // 资源ID - 根据模型类型选择
+    private String resourceIdIcl1 = "seed-icl-1.0";
+    private String resourceIdIcl2 = "seed-icl-2.0";
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 5000L; // 重试间隔5秒
+    private Long pollingInterval = 10000L; // 轮询间隔10秒
+    private Integer maxPollingTimes = 60; // 最多轮询60次,约10分钟
+}

+ 42 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java

@@ -0,0 +1,42 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public enum ErrorCodeEnum {
+    SUCCESS(0, "成功"),
+    BAD_REQUEST_ERROR(1001, "请求参数有误"),
+    AUDIO_UPLOAD_ERROR(1101, "音频上传失败"),
+    ASR_ERROR(1102, "ASR转写失败"),
+    SID_ERROR(1103, "SID声纹检测失败"),
+    SID_FAIL_ERROR(1104, "声纹检测未通过"),
+    GET_AUDIO_DATA_ERROR(1105, "获取音频数据失败"),
+    SPEAKER_ID_DUPLICATION_ERROR(1106, "SpeakerID重复"),
+    SPEAKER_ID_NOT_FOUND_ERROR(1107, "SpeakerID未找到"),
+    AUDIO_CONVERT_ERROR(1108, "音频转码失败"),
+    WER_ERROR(1109, "WER检测错误"),
+    AED_ERROR(1111, "AED检测错误"),
+    SNR_ERROR(1112, "SNR检测错误"),
+    DENOISE_ERROR(1113, "降噪处理失败"),
+    AUDIO_QUALITY_ERROR(1114, "音频质量低"),
+    ASR_NO_SPEAKER_ERROR(1122, "未检测到人声"),
+    UPLOAD_LIMIT_ERROR(1123, "已达上传次数限制"),
+    UNKNOWN_ERROR(-1, "未知错误");
+
+    private final Integer code;
+    private final String message;
+
+    ErrorCodeEnum(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public static ErrorCodeEnum fromCode(Integer code) {
+        for (ErrorCodeEnum errorCode : values()) {
+            if (errorCode.getCode().equals(code)) {
+                return errorCode;
+            }
+        }
+        return UNKNOWN_ERROR;
+    }
+}

+ 21 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java

@@ -0,0 +1,21 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public class VoiceCloneException extends RuntimeException {
+    private final Integer errorCode;
+    private final String errorMessage;
+
+    public VoiceCloneException(Integer errorCode, String errorMessage) {
+        super(String.format("错误码: %d, 错误信息: %s", errorCode, errorMessage));
+        this.errorCode = errorCode;
+        this.errorMessage = errorMessage;
+    }
+
+    public VoiceCloneException(String message, Throwable cause) {
+        super(message, cause);
+        this.errorCode = -1;
+        this.errorMessage = message;
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java

@@ -0,0 +1,20 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class BaseResponse {
+    @JsonProperty("BaseResp")
+    private BaseResp baseResp;
+
+    @Data
+    public static class BaseResp {
+        @JsonProperty("StatusCode")
+        private Integer statusCode;
+
+        @JsonProperty("StatusMessage")
+        private String statusMessage;
+    }
+}

+ 33 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java

@@ -0,0 +1,33 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class StatusResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private Integer status; // 0-NotFound, 1-Training, 2-Success, 3-Failed, 4-Active
+
+    @JsonProperty("create_time")
+    private Long createTime;
+
+    private String version;
+
+    @JsonProperty("demo_audio")
+    private String demoAudio;
+
+    public String getStatusText() {
+        switch (status) {
+            case 0: return "NotFound";
+            case 1: return "Training";
+            case 2: return "Success";
+            case 3: return "Failed";
+            case 4: return "Active";
+            default: return "Unknown";
+        }
+    }
+}

+ 13 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java

@@ -0,0 +1,13 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class TrainingStatusRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 64 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java

@@ -0,0 +1,64 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TtsRequest {
+    @JsonProperty("appid")
+    private String appId;
+
+    @JsonProperty("token")
+    private String token;
+
+    @JsonProperty("cluster")
+    private String cluster = "volcano_icl"; // 声音复刻必须使用此cluster
+
+    @JsonProperty("voice_type")
+    private String voiceType; // 训练好的speaker_id
+
+    private String text; // 要合成的文本
+
+    private String format = "mp3"; // 音频格式: wav, mp3, pcm
+
+    private Integer sampleRate = 24000; // 采样率
+
+    private Integer speed = 1; // 语速 (0-15)
+
+    private Integer volume = 10; // 音量 (0-15)
+
+    private Integer pitch = 10; // 音高 (0-15)
+
+    @JsonProperty("audio_encode_type")
+    private String audioEncodeType = "raw"; // raw或wav
+
+    @JsonProperty("enable_subtitle")
+    private Boolean enableSubtitle = false; // 是否开启字幕
+
+    @JsonProperty("voice_id")
+    private String voiceId; // 音色ID (可选)
+
+    private String language = "zh"; // 语言: zh, en, ja等
+
+    @JsonProperty("reqid")
+    private String reqId; // 请求ID,需要保证唯一
+
+    @JsonProperty("emotion")
+    private String emotion; // 情感参数
+
+    @JsonProperty("speaking_style")
+    private String speakingStyle; // 说话风格
+
+    // 构造函数
+    public TtsRequest(String appId, String token, String voiceType, String text) {
+        this.appId = appId;
+        this.token = token;
+        this.voiceType = voiceType;
+        this.text = text;
+        this.reqId = java.util.UUID.randomUUID().toString();
+    }
+}

+ 54 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java

@@ -0,0 +1,54 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TtsResponse extends BaseResponse {
+    private AudioData data;
+
+    @Data
+    public static class AudioData {
+        private String audio; // Base64编码的音频数据
+
+        private Double duration; // 音频时长(秒)
+
+        @JsonProperty("subtitle_info")
+        private SubtitleInfo subtitleInfo;
+
+        @JsonProperty("subtitle_url")
+        private String subtitleUrl; // 字幕文件URL
+    }
+
+    @Data
+    public static class SubtitleInfo {
+        @JsonProperty("word_list")
+        private List<WordInfo> wordList;
+    }
+
+    @Data
+    public static class WordInfo {
+        private String word; // 词语
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+
+        @JsonProperty("phone_list")
+        private List<PhoneInfo> phoneList;
+    }
+
+    @Data
+    public static class PhoneInfo {
+        private String phone; // 音素
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java

@@ -0,0 +1,12 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class UploadResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 39 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java

@@ -0,0 +1,39 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class VoiceCloneRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private List<AudioInfo> audios;
+
+    private Integer source = 2; // 固定值2
+
+    private Integer language = 0; // 0-中文, 1-英文
+
+    @JsonProperty("model_type")
+    private Integer modelType = 4; // 默认使用ICL 2.0
+
+    @JsonProperty("extra_params")
+    private String extraParams = "{}";
+
+    @Data
+    public static class AudioInfo {
+        @JsonProperty("audio_bytes")
+        private String audioBytes; // Base64编码的音频
+
+        @JsonProperty("audio_format")
+        private String audioFormat; // wav, mp3等
+
+        private String text; // 可选,朗读文本用于校验
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java

@@ -0,0 +1,57 @@
+package com.fs.aiSoundReplication.service;
+
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.fastgptApi.vo.AudioVO;
+import org.springframework.core.io.Resource;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface TtsService {
+
+    /**
+     * 文本转语音
+     * @param request TTS请求参数
+     * @return TTS响应
+     */
+    AudioVO textToSpeech(TtsRequest request);
+
+    /**
+     * 简化版文本转语音
+     * @param text 要合成的文本
+     * @param voiceType 音色ID
+     * @return TTS响应
+     */
+//    TtsResponse textToSpeech(String text, String voiceType);
+
+    /**
+     * 文本转语音并保存为文件
+     * @param request TTS请求参数
+     * @param savePath 保存路径
+     * @return 保存的音频文件
+     */
+//    File textToSpeechAndSave(TtsRequest request, String savePath);
+
+    /**
+     * 文本转语音并获取字节数组
+     * @param request TTS请求参数
+     * @return 音频字节数组
+     */
+//    byte[] textToSpeechBytes(TtsRequest request);
+
+    /**
+     * 流式返回音频数据
+     * @param request TTS请求参数
+     * @return 音频资源
+     */
+//    String textToSpeechStream(TtsRequest request);
+
+    /**
+     * 批量文本转语音
+     * @param texts 文本列表
+     * @param voiceType 音色ID
+     * @return 音频文件列表
+     */
+//    java.util.List<File> batchTextToSpeech(java.util.List<String> texts, String voiceType);
+}

+ 50 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java

@@ -0,0 +1,50 @@
+package com.fs.aiSoundReplication.service;
+
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface VoiceCloneService {
+
+    /**
+     * 上传音频训练音色
+     * @param speakerId 音色ID
+     * @param audioFile 音频文件
+     * @param modelType 模型类型 1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0
+     * @param language 语种 0-中文, 1-英文等
+     * @return 上传响应
+     */
+    UploadResponse uploadVoice(String speakerId, MultipartFile audioFile,
+                               Integer modelType, Integer language);
+
+    /**
+     * 上传音频训练音色(使用文件路径)
+     */
+    UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                     Integer modelType, Integer language);
+
+    /**
+     * 查询音色训练状态
+     * @param speakerId 音色ID
+     * @return 状态响应
+     */
+    StatusResponse queryTrainingStatus(String speakerId);
+
+    /**
+     * 轮询音色训练状态
+     * @param speakerId 音色ID
+     * @param maxPollingTimes 最大轮询次数
+     * @param pollingInterval 轮询间隔(毫秒)
+     * @return 最终状态响应
+     */
+    StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                      Long pollingInterval);
+
+    /**
+     * 获取资源ID(根据模型类型)
+     * @param modelType 模型类型
+     * @return 资源ID
+     */
+    String getResourceIdByModelType(Integer modelType);
+}

+ 488 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java

@@ -0,0 +1,488 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.TtsConfig;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.fastgptApi.util.AudioUtils;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.Base64;
+
+import static com.fs.fastgptApi.util.AudioUtils.getDurations;
+import static com.fs.fastgptApi.util.AudioUtils.transferAudioSilk;
+
+@Service
+@Slf4j
+public class TtsServiceImpl implements TtsService {
+
+    @Autowired
+    private TtsConfig ttsConfig;
+
+    @Autowired
+    private VoiceCloneConfig voiceCloneConfig;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private final ExecutorService executorService = Executors.newFixedThreadPool(5);
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+    @Override
+    public AudioVO textToSpeech(TtsRequest request) {
+        try {
+            // 1. 参数校验
+            validateTtsRequest(request);
+
+            // 2. 设置默认值
+            setDefaultValues(request);
+
+            // 3. 构建请求体
+            String requestBody = buildRequestBody(request);
+
+            // 4. 构建HTTP请求
+            Request httpRequest = buildHttpRequest(requestBody);
+
+            // 5. 发送请求(带重试)
+            byte[] bytes = executeTtsRequest(httpRequest);
+
+            // 6. 检查音频数据
+            if (bytes == null || bytes.length == 0) {
+                throw new VoiceCloneException(-1, "音频数据为空");
+            }
+
+            // 7. 自动保存音频文件
+            // 创建临时文件
+            File tempFile = File.createTempFile("tts_", ".wav");
+            try (FileOutputStream fos = new FileOutputStream(tempFile)) {
+                fos.write(bytes);
+            }
+            // 上传到OSS
+            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
+                //直接转silk然后传桶,返回url     优化-需要wav格式
+                CloudStorageService storage = OSSFactory.build();
+                String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
+//                AudioVO audioVO = AudioUtils.transferAudioSilkFromUrl(wavUrl, false);
+                Integer durations = getDurations(tempFile.getParent()+"\\"+tempFile.getName());
+                String silkUrl = transferAudioSilk(tempFile.getParent()+"\\", tempFile.getName(), false);
+                AudioVO audioVO = new AudioVO();
+                audioVO.setDuration(durations);
+                audioVO.setUrl(silkUrl);
+                audioVO.setWavUrl(wavUrl);
+                log.info("音频文件上传OSS成功: {}", audioVO.getUrl());
+                return audioVO;
+            } finally {
+                // 删除临时文件
+                tempFile.delete();
+            }
+
+        } catch (Exception e) {
+            log.error("TTS合成失败,reqId: {}, 错误: {}",
+                    request.getReqId(), e.getMessage());
+            throw e instanceof VoiceCloneException ?
+                    (VoiceCloneException) e :
+                    new VoiceCloneException("TTS合成失败", e);
+        }
+    }
+
+//    @Override
+//    public String textToSpeech(String text, String voiceType) {
+//        // 创建简化版请求
+//        TtsRequest request = new TtsRequest(
+//                voiceCloneConfig.getAppId(),
+//                voiceCloneConfig.getAccessToken(),
+//                voiceType,
+//                text
+//        );
+//
+//        // 设置默认参数
+//        request.setFormat(ttsConfig.getDefaultFormat());
+//        request.setSampleRate(ttsConfig.getDefaultSampleRate());
+//        request.setCluster(ttsConfig.getDefaultCluster());
+//
+//        return textToSpeech(request);
+//    }
+
+//    @Override
+//    public File textToSpeechAndSave(TtsRequest request, String savePath) {
+//        try {
+//            // 1. 执行TTS合成
+//            TtsResponse response = textToSpeech(request);
+//
+//            if (response.getData() == null || response.getData().getAudio() == null) {
+//                throw new VoiceCloneException(-1, "音频数据为空");
+//            }
+//
+//            // 2. 解码Base64音频数据
+//            byte[] audioBytes = Base64.getDecoder().decode(response.getData().getAudio());
+//
+//            // 3. 确定保存路径
+//            String finalSavePath = savePath != null ? savePath : ttsConfig.getAudioSavePath();
+//
+//            // 创建目录
+//            Path directory = Paths.get(finalSavePath);
+//            if (!Files.exists(directory)) {
+//                Files.createDirectories(directory);
+//            }
+//
+//            // 4. 生成文件名
+//            String fileName = String.format("%s_%s.%s",
+//                    request.getVoiceType(),
+//                    request.getReqId().substring(0, 8),
+//                    request.getFormat());
+//
+//            File audioFile = new File(finalSavePath, fileName);
+//
+//            // 5. 保存文件
+//            try (FileOutputStream fos = new FileOutputStream(audioFile)) {
+//                fos.write(audioBytes);
+//            }
+//
+//            log.info("音频文件保存成功: {}, 大小: {}KB",
+//                    audioFile.getAbsolutePath(), audioBytes.length / 1024);
+//
+//            return audioFile;
+//
+//        } catch (IOException e) {
+//            log.error("保存音频文件失败", e);
+//            throw new VoiceCloneException("保存音频文件失败", e);
+//        }
+//    }
+
+//    @Override
+//    public byte[] textToSpeechBytes(TtsRequest request) {
+//         textToSpeech(request);
+//    }
+
+//    @Override
+//    public String textToSpeechStream(TtsRequest request) {
+//        byte[] audioBytes = textToSpeechBytes(request);
+//
+//    }
+
+//    @Override
+//    public List<File> batchTextToSpeech(List<String> texts, String voiceType) {
+//        List<CompletableFuture<File>> futures = new ArrayList<>();
+//        List<File> results = new ArrayList<>();
+//
+//        for (int i = 0; i < texts.size(); i++) {
+//            final String text = texts.get(i);
+//            final int index = i;
+//
+//            CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
+//                try {
+//                    TtsRequest request = new TtsRequest(
+//                            voiceCloneConfig.getAppId(),
+//                            voiceCloneConfig.getAccessToken(),
+//                            voiceType,
+//                            text
+//                    );
+//                    request.setReqId(String.format("batch_%s_%d",
+//                            UUID.randomUUID().toString().substring(0, 8), index));
+//
+//                    return textToSpeechAndSave(request, null);
+//                } catch (Exception e) {
+//                    log.error("批量TTS处理失败,文本索引: {}, 错误: {}", index, e.getMessage());
+//                    return null;
+//                }
+//            }, executorService);
+//
+//            futures.add(future);
+//        }
+//
+//        // 等待所有任务完成
+//        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+//
+//        // 收集结果
+//        for (CompletableFuture<File> future : futures) {
+//            try {
+//                File file = future.get();
+//                if (file != null) {
+//                    results.add(file);
+//                }
+//            } catch (Exception e) {
+//                log.error("获取批量处理结果失败", e);
+//            }
+//        }
+//
+//        log.info("批量TTS处理完成,成功: {}/{}", results.size(), texts.size());
+//        return results;
+//    }
+
+    // ============ 私有方法 ============
+
+    private void validateTtsRequest(TtsRequest request) {
+        if (request == null) {
+            throw new VoiceCloneException(1001, "请求参数不能为空");
+        }
+
+        if (request.getText() == null || request.getText().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "文本内容不能为空");
+        }
+
+        if (request.getText().length() > ttsConfig.getMaxTextLength()) {
+            throw new VoiceCloneException(1001,
+                    String.format("文本长度超过限制(%d字符)", ttsConfig.getMaxTextLength()));
+        }
+
+        if (request.getVoiceType() == null || request.getVoiceType().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "音色ID不能为空");
+        }
+
+        if (request.getReqId() == null || request.getReqId().trim().isEmpty()) {
+            request.setReqId(UUID.randomUUID().toString());
+        }
+    }
+
+    private void setDefaultValues(TtsRequest request) {
+        if (request.getAppId() == null || request.getAppId().equals("")) {
+            request.setAppId(voiceCloneConfig.getAppId());
+        }
+
+        if (request.getToken() == null || request.getToken().equals("")) {
+            request.setToken(voiceCloneConfig.getAccessToken());
+        }
+
+        if (request.getCluster() == null) {
+            request.setCluster(ttsConfig.getDefaultCluster());
+        }
+
+        if (request.getFormat() == null) {
+            request.setFormat(ttsConfig.getDefaultFormat());
+        }
+
+        if (request.getSampleRate() == null) {
+            request.setSampleRate(ttsConfig.getDefaultSampleRate());
+        }
+
+        if (request.getSpeed() == null) {
+            request.setSpeed(ttsConfig.getDefaultSpeed());
+        }
+
+        if (request.getVolume() == null) {
+            request.setVolume(ttsConfig.getDefaultVolume());
+        }
+
+        if (request.getPitch() == null) {
+            request.setPitch(ttsConfig.getDefaultPitch());
+        }
+    }
+
+    private String buildRequestBody(TtsRequest request) throws IOException {
+        Map<String, Object> requestBody = new HashMap<>();
+
+        // 必填参数
+        HashMap<String, Object> app = new HashMap<String, Object>() {{
+            put("appid", request.getAppId());
+            put("token", request.getToken());
+            put("cluster", request.getCluster());
+        }};
+        HashMap<String, Object> user = new HashMap<String, Object>() {{
+            put("uid","01");
+        }};
+        HashMap<String, Object> audio = new HashMap<String, Object>() {{
+            put("voice_type", request.getVoiceType());
+        }};
+        if (request.getFormat() != null)audio.put("encoding", request.getFormat());
+        if (request.getSpeed() != null)audio.put("speed_ratio", request.getSpeed());
+
+        HashMap<String, Object> requestMap = new HashMap<String, Object>() {{
+            put("reqid", request.getReqId());
+            put("text", request.getText());
+            put("operation","query");
+        }};
+
+        requestBody.put("app",app);
+        requestBody.put("user", user);
+        requestBody.put("audio", audio);
+        requestBody.put("request", requestMap);
+
+        // 可选参数
+//        if (request.getFormat() != null) {
+//            requestBody.put("format", request.getFormat());
+//        }
+
+//        if (request.getSampleRate() != null) {
+//            requestBody.put("sample_rate", request.getSampleRate());
+//        }
+
+//        if (request.getSpeed() != null) {
+//            requestBody.put("speed", request.getSpeed());
+//        }
+
+//        if (request.getVolume() != null) {
+//            requestBody.put("volume", request.getVolume());
+//        }
+
+//        if (request.getPitch() != null) {
+//            requestBody.put("pitch", request.getPitch());
+//        }
+
+//        if (request.getAudioEncodeType() != null) {
+//            requestBody.put("audio_encode_type", request.getAudioEncodeType());
+//        }
+
+//        if (request.getEnableSubtitle() != null) {
+//            requestBody.put("enable_subtitle", request.getEnableSubtitle());
+//        }
+
+//        if (request.getVoiceId() != null) {
+//            requestBody.put("voice_id", request.getVoiceId());
+//        }
+
+//        if (request.getLanguage() != null) {
+//            requestBody.put("language", request.getLanguage());
+//        }
+
+//        if (request.getEmotion() != null) {
+//            requestBody.put("emotion", request.getEmotion());
+//        }
+
+//        if (request.getSpeakingStyle() != null) {
+//            requestBody.put("speaking_style", request.getSpeakingStyle());
+//        }
+
+        return objectMapper.writeValueAsString(requestBody);
+    }
+
+    private Request buildHttpRequest(String requestBody) {
+        RequestBody body = RequestBody.create(JSON,requestBody );
+
+        return new Request.Builder()
+                .url(ttsConfig.getHttpUrl())
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + voiceCloneConfig.getAccessToken())
+                .addHeader("Content-Type", "application/json")
+                .build();
+    }
+
+    private byte[] executeTtsRequest(Request httpRequest) {
+        IOException lastException = null;
+
+        for (int i = 0; i < ttsConfig.getMaxRetryTimes(); i++) {
+            try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+                if (!response.isSuccessful()) {
+                    throw new IOException("HTTP请求失败,状态码: " + response.code());
+                }
+
+                String responseBody = response.body().string();
+                log.debug("TTS API响应: {}", responseBody);
+
+                Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
+                Integer code = (Integer) responseMap.get("code");
+                if (code != null && code != 3000) {
+                    String message = (String) responseMap.get("message");
+                    throw new VoiceCloneException(code != null ? code : -1,
+                            String.format("TTS合成失败: %s (错误码: %d)", message, code != null ? code : -1));
+                }
+                // 获取data字段(base64编码的音频数据)
+                Object data = responseMap.get("data");
+                if (data == null) {
+                    throw new VoiceCloneException(-1, "TTS音频数据为空");
+                }
+
+                if (!(data instanceof String)) {
+                    throw new VoiceCloneException(-1, "TTS音频数据格式错误");
+                }
+
+                String base64Audio = (String) data;
+
+                // 解码base64音频数据
+                try {
+                    return Base64.getDecoder().decode(base64Audio);
+                } catch (IllegalArgumentException e) {
+                    log.error("Base64解码失败", e);
+                    throw new VoiceCloneException("音频数据解码失败", e);
+                }
+
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次TTS请求失败: {}", i + 1, e.getMessage());
+
+                if (i < ttsConfig.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(ttsConfig.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("TTS请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkTtsResponse(TtsResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "TTS响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            throw new VoiceCloneException(statusCode,
+                    String.format("TTS合成失败: %s (错误码: %d)", errorMessage, statusCode));
+        }
+
+        if (response.getData() == null) {
+            throw new VoiceCloneException(-1, "TTS音频数据为空");
+        }
+
+        if (response.getData().getAudio() == null) {
+            throw new VoiceCloneException(-1, "Base64音频数据为空");
+        }
+    }
+
+    private void autoSaveAudio(String base64Audio, String reqId, String format) {
+        try {
+            // 解码音频
+            byte[] audioBytes = Base64.getDecoder().decode(base64Audio);
+
+            // 创建保存目录
+            Path saveDir = Paths.get(ttsConfig.getAudioSavePath());
+            if (!Files.exists(saveDir)) {
+                Files.createDirectories(saveDir);
+            }
+
+            // 生成文件名
+            String fileName = String.format("auto_save_%s.%s",
+                    reqId.substring(0, 8), format);
+            Path filePath = saveDir.resolve(fileName);
+
+            // 保存文件
+            Files.write(filePath, audioBytes);
+
+            log.debug("音频自动保存成功: {}", filePath);
+
+        } catch (Exception e) {
+            log.warn("音频自动保存失败: {}", e.getMessage());
+            // 不抛出异常,自动保存失败不影响主要功能
+        }
+    }
+}

+ 343 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java

@@ -0,0 +1,343 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.ErrorCodeEnum;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.param.*;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.aiSoundReplication.util.FileUtil;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.*;
+
+@Service
+@Slf4j
+public class VoiceCloneServiceImpl implements VoiceCloneService {
+
+    @Autowired
+    private VoiceCloneConfig config;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String RESOURCE_ID_HEADER = "Resource-Id";
+
+    @Override
+    public UploadResponse uploadVoice(String speakerId, MultipartFile audioFile,
+                                      Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            validateUploadParams(speakerId, audioFile, modelType);
+
+            // 2. 构建请求
+            VoiceCloneRequest request = buildUploadRequest(speakerId, audioFile, modelType, language);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 构建请求头
+            String resourceId = getResourceIdByModelType(modelType);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            // 4. 发送请求(带重试机制)
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+
+            // 5. 检查响应
+            checkResponse(response);
+
+            log.info("音色上传成功,speakerId: {}", response.getSpeakerId());
+            return response;
+
+        } catch (JsonProcessingException e) {
+            log.error("JSON序列化失败", e);
+            throw new VoiceCloneException("JSON序列化失败", e);
+        } catch (IOException e) {
+            log.error("文件处理失败", e);
+            throw new VoiceCloneException("文件处理失败", e);
+        }
+    }
+
+    @Override
+    public UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                            Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            validateUploadParams(speakerId, null, modelType);
+
+            // 2. 读取文件并转换为Base64
+            String base64Audio = FileUtil.fileToBase64(filePath);
+            String fileExtension = FileUtil.getFileExtension(filePath);
+
+            // 3. 构建音频信息
+            VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+            audioInfo.setAudioBytes(base64Audio);
+            audioInfo.setAudioFormat(fileExtension);
+
+            VoiceCloneRequest request = new VoiceCloneRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            request.setAudios(Collections.singletonList(audioInfo));
+            request.setModelType(modelType);
+            request.setLanguage(language != null ? language : 0);
+            request.setSource(2);
+
+            // 4. 构建请求头并发送
+            String resourceId = getResourceIdByModelType(modelType);
+            String requestBody = objectMapper.writeValueAsString(request);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+            checkResponse(response);
+
+            log.info("音色上传成功(文件路径方式),speakerId: {}", response.getSpeakerId());
+            return response;
+
+        } catch (IOException e) {
+            log.error("文件处理失败", e);
+            throw new VoiceCloneException("文件处理失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse queryTrainingStatus(String speakerId) {
+        try {
+            // 1. 参数校验
+            if (speakerId == null || speakerId.trim().isEmpty()) {
+                throw new VoiceCloneException(1001, "speakerId不能为空");
+            }
+
+            // 2. 构建请求
+            TrainingStatusRequest request = new TrainingStatusRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 根据历史记录确定资源ID,默认使用ICL 2.0
+            String resourceId = config.getResourceIdIcl2();
+            Request httpRequest = buildHttpRequest(config.getStatusUrl(), resourceId, requestBody);
+
+            // 4. 发送请求
+            StatusResponse response = executeRequest(httpRequest, StatusResponse.class);
+            checkResponse(response);
+
+            log.debug("训练状态查询成功,speakerId: {}, 状态: {}",
+                    response.getSpeakerId(), response.getStatusText());
+            return response;
+
+        } catch (IOException e) {
+            log.error("JSON序列化失败", e);
+            throw new VoiceCloneException("JSON序列化失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                             Long pollingInterval) {
+        if (maxPollingTimes == null) {
+            maxPollingTimes = config.getMaxPollingTimes();
+        }
+        if (pollingInterval == null) {
+            pollingInterval = config.getPollingInterval();
+        }
+
+        StatusResponse finalResponse = null;
+
+        for (int i = 0; i < maxPollingTimes; i++) {
+            try {
+                // 查询状态
+                StatusResponse response = queryTrainingStatus(speakerId);
+                finalResponse = response;
+
+                // 检查状态
+                Integer status = response.getStatus();
+                if (status == 2 || status == 4) {
+                    log.info("音色训练完成,speakerId: {}, 状态: {}",
+                            speakerId, response.getStatusText());
+                    return response;
+                } else if (status == 3) {
+                    log.error("音色训练失败,speakerId: {}", speakerId);
+                    throw new VoiceCloneException(ErrorCodeEnum.fromCode(status).getCode(),
+                            "训练失败,状态码: " + status);
+                } else if (status == 1) {
+                    log.info("训练中... (第{}次轮询)", i + 1);
+                    // 等待指定间隔
+                    Thread.sleep(pollingInterval);
+                } else {
+                    log.warn("未知状态,停止轮询,状态码: {}", status);
+                    return response;
+                }
+
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new VoiceCloneException("轮询被中断", e);
+            } catch (Exception e) {
+                log.error("第{}次轮询失败", i + 1, e);
+                // 非最后一次失败,继续尝试
+                if (i < maxPollingTimes - 1) {
+                    try {
+                        Thread.sleep(pollingInterval);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("轮询被中断", ie);
+                    }
+                } else {
+                    throw e instanceof VoiceCloneException ?
+                            (VoiceCloneException) e :
+                            new VoiceCloneException("轮询失败", e);
+                }
+            }
+        }
+
+        log.warn("达到最大轮询次数仍未完成,speakerId: {}", speakerId);
+        return finalResponse;
+    }
+
+    @Override
+    public String getResourceIdByModelType(Integer modelType) {
+        if (modelType == null) {
+            return config.getResourceIdIcl2(); // 默认使用ICL 2.0
+        }
+
+        switch (modelType) {
+            case 1: // ICL 1.0
+                return config.getResourceIdIcl1();
+            case 2: // DiT标准版(使用ICL 1.0)
+            case 3: // DiT还原版(使用ICL 1.0)
+                return config.getResourceIdIcl1();
+            case 4: // ICL 2.0
+                return config.getResourceIdIcl2();
+            default:
+                log.warn("未知的modelType: {},使用默认ICL 2.0", modelType);
+                return config.getResourceIdIcl2();
+        }
+    }
+
+    // ============ 私有方法 ============
+
+    private void validateUploadParams(String speakerId, MultipartFile audioFile,
+                                      Integer modelType) {
+        if (speakerId == null || speakerId.trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "speakerId不能为空");
+        }
+
+        if (audioFile != null && audioFile.isEmpty()) {
+            throw new VoiceCloneException(1001, "音频文件不能为空");
+        }
+
+        if (modelType != null && modelType < 0 || modelType > 4) {
+            throw new VoiceCloneException(1001, "modelType参数错误,应为1-4");
+        }
+    }
+
+    private VoiceCloneRequest buildUploadRequest(String speakerId, MultipartFile audioFile,
+                                                 Integer modelType, Integer language) throws IOException {
+        VoiceCloneRequest request = new VoiceCloneRequest();
+        request.setAppid(config.getAppId());
+        request.setSpeakerId(speakerId);
+        request.setModelType(modelType != null ? modelType : 4);
+        request.setLanguage(language != null ? language : 0);
+        request.setSource(2);
+
+        // 构建音频信息
+        VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+        audioInfo.setAudioBytes(FileUtil.multipartFileToBase64(audioFile));
+
+        // 获取文件扩展名
+        String originalFilename = audioFile.getOriginalFilename();
+        String fileExtension = FileUtil.getFileExtension(originalFilename);
+        if (fileExtension != null){
+            if (fileExtension.equals("m4a") || fileExtension.equals("pcm"))
+                audioInfo.setAudioFormat(fileExtension);
+        }
+        request.setAudios(Collections.singletonList(audioInfo));
+
+//        // 设置额外参数-降噪
+//        Map<String, Object> extraParams = new HashMap<>();
+//        // ICL 2.0默认关闭降噪以获得更多细节
+//        if (modelType == null || modelType == 4) {
+//            extraParams.put("enable_audio_denoise", false);
+//        } else {
+//            extraParams.put("enable_audio_denoise", true);
+//        }
+//        request.setExtraParams(objectMapper.writeValueAsString(extraParams));
+
+        return request;
+    }
+
+    private Request buildHttpRequest(String url, String resourceId, String requestBody) {
+        RequestBody body = RequestBody.create(
+                MediaType.get("application/json; charset=utf-8"),requestBody
+        );
+
+        return new Request.Builder()
+                .url(url)
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + config.getAccessToken())
+                .addHeader(RESOURCE_ID_HEADER, resourceId)
+                .build();
+    }
+
+    private <T extends BaseResponse> T executeRequest(Request httpRequest, Class<T> responseType)
+            throws IOException {
+        try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("HTTP请求失败,状态码: " + response.code());
+            }
+
+            String responseBody = response.body().string();
+            log.debug("API响应: {}", responseBody);
+
+            return objectMapper.readValue(responseBody, responseType);
+        }
+    }
+
+    private <T extends BaseResponse> T executeRequestWithRetry(Request httpRequest,
+                                                               Class<T> responseType) {
+        IOException lastException = null;
+
+        for (int i = 0; i < config.getMaxRetryTimes(); i++) {
+            try {
+                return executeRequest(httpRequest, responseType);
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次请求失败,准备重试: {}", i + 1, e.getMessage());
+
+                if (i < config.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(config.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkResponse(BaseResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            ErrorCodeEnum errorCode = ErrorCodeEnum.fromCode(statusCode);
+
+            throw new VoiceCloneException(statusCode,
+                    String.format("%s (错误码: %d)", errorCode.getMessage(), statusCode));
+        }
+    }
+}

+ 44 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java

@@ -0,0 +1,44 @@
+package com.fs.aiSoundReplication.util;
+
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class FileToMultipartConverterUtil {
+    /*file转成multipartFile*/
+    public static MultipartFile convert(File file) throws IOException {
+        if (file == null || !file.exists()) {
+            throw new IllegalArgumentException("文件不存在");
+        }
+
+        try (FileInputStream input = new FileInputStream(file)) {
+            return new MockMultipartFile(
+                    file.getName(),           // 文件名
+                    file.getName(),           // 原始文件名(通常与文件名相同)
+                    getContentType(file),     // 内容类型
+                    input                     // 文件输入流
+            );
+        }
+    }
+
+    private static String getContentType(File file) {
+        String fileName = file.getName();
+        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
+            return "image/jpeg";
+        } else if (fileName.endsWith(".png")) {
+            return "image/png";
+        } else if (fileName.endsWith(".pdf")) {
+            return "application/pdf";
+        } else if (fileName.endsWith(".txt")) {
+            return "text/plain";
+        } else if (fileName.endsWith(".mp3")) {
+            return "audio/mpeg";
+        } else if (fileName.endsWith(".mp4")) {
+            return "video/mp4";
+        }
+        // 默认类型
+        return "application/octet-stream";
+    }
+}

+ 47 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java

@@ -0,0 +1,47 @@
+package com.fs.aiSoundReplication.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Base64;
+
+@Slf4j
+public class FileUtil {
+
+    public static String fileToBase64(String filePath) throws IOException {
+        File file = new File(filePath);
+        if (!file.exists() || !file.isFile()) {
+            throw new IOException("文件不存在: " + filePath);
+        }
+
+        if (file.length() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = Files.readAllBytes(file.toPath());
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    public static String getFileExtension(String fileName) {
+        if (fileName == null || !fileName.contains(".")) {
+            return "";
+        }
+        return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+    }
+
+    public static String multipartFileToBase64(MultipartFile file) throws IOException {
+        if (file.isEmpty()) {
+            throw new IOException("文件为空");
+        }
+
+        if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = file.getBytes();
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+}

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -5,6 +5,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.param.CompanyUserAreaParam;
 import com.fs.company.param.CompanyUserQwParam;
+import com.fs.company.param.VcCompanyUser;
 import com.fs.company.vo.*;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.vo.CompanyUserQwVO;
@@ -352,4 +353,8 @@ public interface CompanyUserMapper
     List<com.fs.hisStore.domain.FsUserScrm> selectBoundFsUsersByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
     CompanyUser selectCompanyUserByQwUserId(@Param("qwUserId") Long id);
+
+    VcCompanyUser selectVcCompanyUserByCompanyUserId(@Param("companyUserId")Long companyUserId);
+
+    int updateVcCompanyUser(@Param("vcCompanyUser") VcCompanyUser vcCompanyUser);
 }

+ 99 - 0
fs-service/src/main/java/com/fs/company/param/VcCompanyUser.java

@@ -0,0 +1,99 @@
+package com.fs.company.param;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 公司用户音色表实体类
+ *
+ * @author
+ * @since 2024-XX-XX
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("vc_company_user")
+public class VcCompanyUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 销售已上传声音次数(单个音色只给单个销售用,而且只能传5次)
+     */
+    @TableField("times")
+    private Integer times;
+
+    /**
+     * 上传豆包的音色id
+     */
+    @TableField("speaker_id")
+    private String speakerId;
+
+    /**
+     * 销售传入的声纹地址
+     */
+    @TableField("upload_url")
+    private String uploadUrl;
+
+    /**
+     * 上传的语音时长(秒)
+     */
+    @TableField("upload_time")
+    private Double uploadTime;
+
+    /**
+     * 用户id
+     */
+    @TableField("company_user_id")
+    private Long companyUserId;
+
+    /**
+     * 最后一次文字转语音生成的url
+     */
+    @TableField("latest_text_to_speech_url")
+    private String latestTextToSpeechUrl;
+
+    // 下面是可选添加的方法和字段
+
+    /**
+     * 判断是否还可以上传声音(最大5次)
+     * @return true: 可以上传,false: 已达上限
+     */
+    public boolean canUpload() {
+        return times == null || times < 5;
+    }
+
+    /**
+     * 增加上传次数
+     */
+    public void incrementTimes() {
+        if (times == null) {
+            times = 1;
+        } else {
+            times++;
+        }
+    }
+
+    /**
+     * 获取剩余可上传次数
+     */
+    public Integer getRemainingTimes() {
+        if (times == null) {
+            return 5;
+        }
+        return Math.max(0, 5 - times);
+    }
+}

+ 33 - 0
fs-service/src/main/java/com/fs/course/param/HsBookDoctorInfoParam.java

@@ -0,0 +1,33 @@
+package com.fs.course.param;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiModel;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+
+@Data
+@ApiModel(description = "红杉问诊申请参数-挂号医生信息")
+@Accessors(chain = true)
+public class HsBookDoctorInfoParam {
+
+    @ApiModelProperty(value = "医生id: 医生排班接口的医生id")
+    @JsonProperty("doctor_id")
+    private Integer doctorId;
+
+    @ApiModelProperty(value = "挂号日期: 医生排班接口的排班日期(例如:2025-04-23)")
+    @JsonProperty("visit_date")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private String visitDate;
+
+    @ApiModelProperty(value = "预约时间段: 医生排班接口的排班时间段(例如:18:00-18:30)")
+    @JsonProperty("visit_range")
+    @DateTimeFormat(pattern = "HH:mm-HH:mm")
+    private String visitRange;
+
+    @ApiModelProperty(value = "预约科室: 医生排班接口的医生科室id")
+    @JsonProperty("branch")
+    private Integer branch;
+}

+ 66 - 0
fs-service/src/main/java/com/fs/course/param/HsDoctorSchedule.java

@@ -0,0 +1,66 @@
+package com.fs.course.param;
+
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+@Data
+@Accessors(chain = true)
+@ApiModel(description = "红杉医生排班信息")
+public class HsDoctorSchedule {
+    @ApiModelProperty(value = "医生id")
+    private Integer doctor;
+
+    @ApiModelProperty(value = "医生姓名")
+    private String doctor_name;
+
+    @ApiModelProperty(value = "科室id")
+    private Integer branch;
+
+    @ApiModelProperty(value = "科室名称")
+    private String branch_name;
+
+    @ApiModelProperty(value = "排班日期")
+    private String work_time;
+
+    @ApiModelProperty(value = "当天可预约总数")
+    private Integer book_total;
+
+    @ApiModelProperty(value = "具体可预约时间段")
+    private Map<String, Integer> schedule;
+
+
+    @ApiModelProperty(value = "擅长")
+    private String expert;
+
+    @ApiModelProperty(value = "擅长的症状")
+    private String skilled_symptoms;
+
+    @ApiModelProperty(value = "简介")
+    private String introduction;
+
+    @ApiModelProperty(value = "预估等待时长")
+    private Integer estimate_wait_time;
+
+    @ApiModelProperty(value = "预计等待群组数量")
+    private Integer estimate_wait_group;
+
+    @ApiModelProperty(value = "视频问诊费用")
+    private BigDecimal video_fee;
+
+    @ApiModelProperty(value = "图文问诊费用")
+    private BigDecimal image_text_fee;
+
+    @ApiModelProperty(value = "头像")
+    private String avatar;
+
+    @ApiModelProperty(value = "职称")
+    private String position;
+}

+ 67 - 0
fs-service/src/main/java/com/fs/course/param/HsDrug.java

@@ -0,0 +1,67 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉药品信息")
+@Accessors(chain = true)
+public class HsDrug {
+
+    @ApiModelProperty(value = "药品类型: 3-西药 6-中药")
+    @JsonProperty("type")
+    private Integer type;
+
+    @ApiModelProperty(value = "商品标识码: type=3时为69码 type=6时为third_code")
+    @JsonProperty("code")
+    private String code;
+
+    @ApiModelProperty(value = "药品名称")
+    @JsonProperty("title")
+    private String title;
+
+    @ApiModelProperty(value = "购买数量")
+    @JsonProperty("total")
+    private Integer total;
+
+    @ApiModelProperty(value = "用药天数: type=3时必需")
+    @JsonProperty("days")
+    private String days;
+
+    @ApiModelProperty(value = "单次用量")
+    @JsonProperty("dose")
+    private Integer dose;
+
+    @ApiModelProperty(value = "单位: 如盒、袋、g、ml等")
+    @JsonProperty("unit")
+    private String unit;
+
+    @ApiModelProperty(value = "特殊用法: type=6时必需 如先煎、后下等")
+    @JsonProperty("requirements")
+    private String requirements;
+
+    @ApiModelProperty(value = "扩展字段: type=6时必需 包含每副药的信息")
+    @JsonProperty("extend")
+    private HsDrugExtend extend;
+
+    @ApiModelProperty(value = "症状描述: 患者症状 支持两种格式")
+    @JsonProperty("symptoms")
+    private List<Object> symptoms;
+
+    @ApiModelProperty(value = "疾病诊断: 根据type自动匹配诊断类型")
+    @JsonProperty("diagnosis")
+    private List<Object> diagnosis;
+
+    @ApiModelProperty(value = "中医诊断: 非必填 type=6时可用")
+    @JsonProperty("diagnosis_ch")
+    private List<Object> diagnosisCh;
+
+    @ApiModelProperty(value = "是否用过该药品: 1-是 0-否 默认1")
+    @JsonProperty("former_used")
+    private Integer formerUsed;
+}

+ 41 - 0
fs-service/src/main/java/com/fs/course/param/HsDrugExtend.java

@@ -0,0 +1,41 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@ApiModel(description = "红杉中药扩展信息")
+@Accessors(chain = true)
+public class HsDrugExtend {
+
+    @ApiModelProperty(value = "药品名: 以医院维护的信息为最终展示")
+    @JsonProperty("title")
+    private String title;
+
+    @ApiModelProperty(value = "每日剂量")
+    @JsonProperty("dose")
+    private Double dose;
+
+    @ApiModelProperty(value = "每日剂量单位")
+    @JsonProperty("unit")
+    private String unit;
+
+    @ApiModelProperty(value = "用药要求: 先煎、后下等备注")
+    @JsonProperty("requirements")
+    private String requirements;
+
+    @ApiModelProperty(value = "用法")
+    @JsonProperty("usage")
+    private String usage;
+
+    @ApiModelProperty(value = "用药频次")
+    @JsonProperty("frequency")
+    private String frequency;
+
+    @ApiModelProperty(value = "总剂数: 若未提供默认1")
+    @JsonProperty("total")
+    private Integer total;
+}

+ 35 - 0
fs-service/src/main/java/com/fs/course/param/HsHealthyCondition.java

@@ -0,0 +1,35 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.HashMap;
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+@ApiModel(description = "红杉问诊申请参数-健康情况")
+public class HsHealthyCondition {
+    @ApiModelProperty(value = "肝肾功能是否正常: 0不正常 1正常")
+    @JsonProperty("liver_kidney_normal")
+    private Integer liverKidneyNormal;
+
+    @ApiModelProperty(value = "妊娠哺乳情况: 0-无;1-备孕中;2-怀孕中;3-哺乳期;9-非女性患者;99-其他")
+    @JsonProperty("pregnancy")
+    private Integer pregnancy;//todo 我们系统中是处方表有妊娠字段
+
+    @ApiModelProperty(value = "过敏史: 支持多条,对象元素结构为code-码表编码;name-实际信息")
+    @JsonProperty("allergy")
+    private List<HashMap<String,String>> allergy;
+
+    @ApiModelProperty(value = "疾病史(现病史): 支持多条,对象元素结构为code-码表编码;name-实际信息")
+    @JsonProperty("illness_history")
+    private List<HashMap<String,String>> illnessHistory;
+
+    @ApiModelProperty(value = "家族史: 格式和疾病史相同")
+    @JsonProperty("family_history")
+    private List<HashMap<String,String>> familyHistory;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/course/param/HsRedirectParam.java

@@ -0,0 +1,38 @@
+package com.fs.course.param;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiModel;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@ApiModel(description = "红杉问诊申请参数-跳转参数")
+public class HsRedirectParam {
+
+    @ApiModelProperty(value = "入口来源类型: 0-不实际跳转;1-小程序;2-app;3-H5")
+    @JsonProperty("from_type")
+    private Integer fromType;
+
+    @ApiModelProperty(value = "入口标识: 根据from_type决定不同意义")
+    @JsonProperty("from_tag")
+    private String fromTag;
+
+    @ApiModelProperty(value = "返回地址类型: 0-不实际跳转;1-小程序;2-app;3-H5")
+    @JsonProperty("back_type")
+    private Integer backType;
+
+    @ApiModelProperty(value = "返回应用标识: 根据back_type决定不同意义")
+    @JsonProperty("back_tag")
+    private String backTag;
+
+    @ApiModelProperty(value = "返回地址")
+    @JsonProperty("back_uri")
+    private String backUri;
+
+    @ApiModelProperty(value = "跳转问诊方式: 0-不实际跳转;1-小程序;3-H5")
+    @JsonProperty("jump_type")
+    private Integer jumpType;
+}
+

+ 36 - 0
fs-service/src/main/java/com/fs/course/param/HsUserInfoParam.java

@@ -0,0 +1,36 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@ApiModel(description = "红杉问诊申请参数-Hs用户信息")
+public class HsUserInfoParam {
+    @ApiModelProperty(value = "三方账号类型: 11-微信小程序;33-手机应用;99-其他")
+    @JsonProperty("acc_type")
+    private String accType;
+
+    @ApiModelProperty(value = "三方入口程序唯一标识: 针对不同入口的唯一标识")
+    @JsonProperty("acc_appid")
+    private String accAppid;//todo
+
+    @ApiModelProperty(value = "三方程序用户唯一标识: 针对不同入口应用的用户唯一标识")
+    @JsonProperty("acc_openid")
+    private String accOpenid;//todo
+
+    @ApiModelProperty(value = "用户手机号: 创建聊天诊室的换取用户信息使用")
+    @JsonProperty("user_mp")
+    private String userMp;
+
+    @ApiModelProperty(value = "用户三方唯一标识: 在三方系统中对该用户的唯一标识")
+    @JsonProperty("user_third_union_code")
+    private String userThirdUnionCode;
+
+    @ApiModelProperty(value = "用户头像地址: 诊室内聊天的头像")
+    @JsonProperty("header_img")
+    private String headerImg;
+}

+ 86 - 0
fs-service/src/main/java/com/fs/course/param/HsUserMember.java

@@ -0,0 +1,86 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@ApiModel(description = "红杉问诊申请参数-Hs家庭成员")
+public class HsUserMember {
+
+    @ApiModelProperty(value = "患者姓名")
+    @JsonProperty("real_name")
+    private String realName;
+
+    @ApiModelProperty(value = "患者手机号")
+    @JsonProperty("mp")
+    private String mp;
+
+    @ApiModelProperty(value = "患者证件信息")
+    @JsonProperty("cert_code")
+    private String certCode;
+
+    @ApiModelProperty(value = "年龄: 身份证必填,未填写年龄和生日时用证件信息换算填充")
+    @JsonProperty("age")
+    private Integer age;
+
+    @ApiModelProperty(value = "年龄单位: 1-岁;2-月;3-天,默认1,即默认年龄为岁")
+    @JsonProperty("age_unit")
+    private String ageUnit;
+
+    @ApiModelProperty(value = "关系: 用户与患者关系(0-本人,1-父母,2-子女,3-夫妻,4-亲属,5-朋友,6-其他)")
+    @JsonProperty("relation")
+    private String relation;
+
+    @ApiModelProperty(value = "性别: 1=男 2=女 0=未知")
+    @JsonProperty("sex")
+    private String sex;
+
+    @ApiModelProperty(value = "监护人身份证: 14岁以下需要填写监护人")
+    @JsonProperty("guardian_cert_code")
+    private String guardianCertCode;
+
+    @ApiModelProperty(value = "监护人姓名: 14岁以下需要填写监护人")
+    @JsonProperty("guardian_name")
+    private String guardianName;
+
+    @ApiModelProperty(value = "监护人关系: 14岁以下需要填写监护人;监护人与患者关系(0-本人,1-父母,2-子女,3-夫妻,4-亲属,5-朋友,6-其他)")
+    @JsonProperty("guardian_relation")
+    private String guardianRelation;
+
+    @ApiModelProperty(value = "体重: 14岁以下需要填写患者体重;患者体重(kg)")
+    @JsonProperty("weight")
+    private String weight;
+
+    @ApiModelProperty(value = "省份")
+    @JsonProperty("province")
+    private String province;
+
+    @ApiModelProperty(value = "市")
+    @JsonProperty("city")
+    private String city;
+
+    @ApiModelProperty(value = "区县")
+    @JsonProperty("county")
+    private String county;
+
+    @ApiModelProperty(value = "行政区划编码")
+    @JsonProperty("area_code")
+    private String areaCode;
+
+    @ApiModelProperty(value = "地址")
+    @JsonProperty("address")
+    private String address;
+
+    @ApiModelProperty(value = "生日: 出生年月(格式规则:2024-12-31)")
+    @JsonProperty("birth_date")
+    private String birthDate;
+
+    @ApiModelProperty(value = "民族: 民族字典")
+    @JsonProperty("nation")
+    private Integer nation;
+}
+

+ 36 - 0
fs-service/src/main/java/com/fs/course/param/InquiryOrderHsLog.java

@@ -0,0 +1,36 @@
+package com.fs.course.param;
+
+import com.fs.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@ApiModel(description = "红杉问诊响应记录表")
+@Accessors(chain = true)
+@Data
+public class InquiryOrderHsLog extends BaseEntity {
+    @ApiModelProperty(value = "主键id")
+    private Long id;
+
+    @ApiModelProperty(value = "红杉响应的json")
+    private String responseJson;
+
+    @ApiModelProperty(value = "问诊单id")
+    private Long inquiryOrderId;
+
+    @ApiModelProperty(value = "患者id")
+    private Long patientId;
+
+    @ApiModelProperty(value = "用户id")
+    private Long userId;
+
+    @ApiModelProperty(value = "记录类型1.发起问诊接收的请求2.通知回调3.")
+    private Integer type;
+
+    @ApiModelProperty(value = "解密后的内容")
+    private String decodeJson;
+
+    @ApiModelProperty(value = "红杉的订单号")
+    private String bookNo;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/course/param/SendInquiryParam.java

@@ -0,0 +1,34 @@
+package com.fs.course.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+
+@ApiModel(description = "发送问诊申请给红杉-参数")
+@Data
+public class SendInquiryParam {
+    @ApiModelProperty(value = "患者id")
+    private Long patientId;
+
+    @ApiModelProperty(value = "问诊单id")
+    private Long inquiryOrderId;
+
+    @ApiModelProperty(value = "用户id")
+    private String userId;
+
+    @ApiModelProperty(value = "appId")
+    private String appId;
+
+    @ApiModelProperty(value = "问诊类型(1.常规2.购药)")
+    private Integer inquiryType;
+
+    @ApiModelProperty(value = "医生id")
+    private Integer doctor;
+
+    @ApiModelProperty(value = "就诊时间")
+    private String visitRange;
+    @ApiModelProperty(value = "科室id")
+    private Integer branch;
+}
+

+ 75 - 0
fs-service/src/main/java/com/fs/course/param/SendInquiryToHSParam.java

@@ -0,0 +1,75 @@
+package com.fs.course.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉问诊申请参数")
+@Accessors(chain = true)
+public class SendInquiryToHSParam {
+
+    @ApiModelProperty(value = "处方分类: 1普通处方 2门诊统筹 3慢病处方")
+    @JsonProperty("prescription_type")
+    private Integer prescriptionType;//todo
+
+    @ApiModelProperty(value = "就诊类型: picture-图文; video-视频")
+    @JsonProperty("register_type")
+    private String registerType;
+
+    @ApiModelProperty(value = "本次申请的唯一标识,当本条信息成功接收后不可二次使用")
+    @JsonProperty("union_code")
+    private String unionCode;
+
+    @ApiModelProperty(value = "用户的账户信息,小程序操作人信息,影响后续业务查询")
+    @JsonProperty("user_info")
+    private HsUserInfoParam userInfo;
+
+    @ApiModelProperty(value = "家庭成员: 当前就诊人信息")
+    @JsonProperty("user_member")
+    private HsUserMember userMember;// todo
+
+    @ApiModelProperty(value = "健康情况: 健康情况信息")
+    @JsonProperty("healthy_condition")
+    private HsHealthyCondition healthyCondition;
+
+    @ApiModelProperty(value = "购药情况: 购药信息 非购药咨询问诊无需带次参数")
+    @JsonProperty("drug")
+    private List<HsDrug> drug;
+
+    @ApiModelProperty(value = "挂号医生信息: 医生信息和挂号时间等")
+    @JsonProperty("book_doctor_info")
+    private HsBookDoctorInfoParam bookDoctorInfo;
+
+    @ApiModelProperty(value = "患者主诉: 当提供主诉优先使用")
+    @JsonProperty("book_description")
+    private String bookDescription;
+
+    @ApiModelProperty(value = "复诊资料: 证明复诊的图片资料")
+    @JsonProperty("subsequent_file")
+    private List<String> subsequentFile;
+
+    @ApiModelProperty(value = "处方开具后流转通知: 当成功开具处方后流转处方触发的回调")
+    @JsonProperty("notify_uri")
+    private String notifyUri;
+
+    @ApiModelProperty(value = "问诊生命周期回调通知: 全生命周期给改地址进行通知回调")
+    @JsonProperty("callback_notification")
+    private String callbackNotification;
+
+    @ApiModelProperty(value = "去取药的地址: 问诊过程中,发送卡片消息去取药的跳转地址")
+    @JsonProperty("dispensary_uri")
+    private String dispensaryUri;
+
+    @ApiModelProperty(value = "问诊结束后进入三方小程序或H5页面的地址")
+    @JsonProperty("redirect_uri")
+    private String redirectUri;
+
+    @ApiModelProperty(value = "复杂情况的跳转参数: 跳转进行问诊的信息")
+    @JsonProperty("redirect_param")
+    private HsRedirectParam redirectParam;
+}

+ 9 - 4
fs-service/src/main/java/com/fs/course/vo/FsInquiryPatientInfoListUVO.java

@@ -13,18 +13,23 @@ public class FsInquiryPatientInfoListUVO {
 
     private String patientName;
 
-    /** 出生年月 */
+    /**
+     * 出生年月
+     */
     @JsonFormat(pattern = "yyyy-MM-dd")
     @Excel(name = "出生年月", width = 30, dateFormat = "yyyy-MM-dd")
     private Date birthday;
 
-    /** 性别 */
+    /**
+     * 性别
+     */
     @Excel(name = "性别")
     private Integer sex;
 
 
-
-    /** 手机号 */
+    /**
+     * 手机号
+     */
     @Excel(name = "手机号")
     private String mobile;
 

+ 14 - 1
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -45,6 +45,8 @@ import com.fs.his.mapper.FsStoreMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.enums.SysConfigEnum;
 import com.fs.im.dto.OpenImMsgDTO;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.qw.domain.*;
@@ -182,6 +184,8 @@ public class AiHookServiceImpl implements AiHookService {
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
 
     private final String DELAY_MSG = "delayMsg";
+    @Autowired
+    private ConfigUtil configUtil;
 
 
     /** Ai半小时未回复提醒 **/
@@ -1014,8 +1018,17 @@ public class AiHookServiceImpl implements AiHookService {
         if (content.isEmpty()){
             return;
         }
+        WxwSilkVoceDTO silkVoice;
+        /*判断是否走豆包语音*/
+        com.alibaba.fastjson.JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+        if (vcConfig != null && !vcConfig.isEmpty() &&
+//                !vcConfig.equals(new com.alibaba.fastjson.JSONObject()) &&
+                "2".equals(vcConfig.getString("type"))){
+            silkVoice = wxWorkService.getSilkVoiceDoubao(content, user.getCompanyUserId());
+        }else {
+            silkVoice= wxWorkService.getSilkVoice(content, user.getCompanyUserId());
+        }
 
-        WxwSilkVoceDTO silkVoice = wxWorkService.getSilkVoice(content, user.getCompanyUserId());
         if (silkVoice == null){
             return;
         }

+ 4 - 4
fs-service/src/main/java/com/fs/his/domain/FsDoctorPrescribe.java

@@ -39,13 +39,13 @@ public class FsDoctorPrescribe extends BaseEntity
     @Excel(name = "使用JSON")
     private String usageJson;
 
-
+    /*备注*/
     private String remark;
-
+    /*制作类型 0-颗粒剂 1-膏方*/
     private Integer recipeType;
-
+    /*用药周期*/
     private Integer cycle;
-
+    /**/
     private String icdCode;
 
 }

+ 4 - 6
fs-service/src/main/java/com/fs/his/domain/FsDoctorPrescribeDrug.java

@@ -3,8 +3,6 @@ package com.fs.his.domain;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
 
 import java.math.BigDecimal;
 
@@ -80,12 +78,12 @@ public class FsDoctorPrescribeDrug extends BaseEntity
     /** 药品图片 */
     @Excel(name = "药品图片")
     private String drugImgUrl;
-
+    /*规格ID*/
     private Long productAttrValueId;
-
+    /*备注*/
     private String remark;
-
+    /*1西药 2中药*/
     private Integer drugType;
-
+    /*是否药品*/
     private Integer isDrug;
 }

+ 168 - 148
fs-service/src/main/java/com/fs/his/domain/FsInquiryOrder.java

@@ -15,113 +15,164 @@ import java.util.Date;
  * @author fs
  * @date 2023-09-15
  */
-public class FsInquiryOrder extends BaseEntity
-{
+public class FsInquiryOrder extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
-    /** ID */
+    /**
+     * ID
+     */
     private Long orderId;
 
-    /** 订单号 */
+    /**
+     * 订单号
+     */
     @Excel(name = "订单号")
     private String orderSn;
 
-    /** 问诊标题 */
+    /**
+     * 问诊标题
+     */
     @Excel(name = "问诊标题")
     private String title;
 
-    /** 病例组图 */
+    /**
+     * 病例组图
+     */
     @Excel(name = "病例组图")
     private String imgs;
 
-    /** 会员ID */
+    /**
+     * 会员ID
+     */
     @Excel(name = "会员ID")
     private Long userId;
 
-    /** 病人ID */
+    /**
+     * 病人ID
+     */
     @Excel(name = "病人ID")
     private Long patientId;
 
-    /** 订单类型 1图文 2语音 */
+    /**
+     * 订单类型 1图文 2语音
+     */
     @Excel(name = "订单类型 1图文 2语音")
     private Integer orderType;
 
-    /** 订单金额 */
+    /**
+     * 订单金额
+     */
     @Excel(name = "订单金额")
     private BigDecimal money;
 
-    /** 支付金额 */
+    /**
+     * 支付金额
+     */
     @Excel(name = "支付金额")
     private BigDecimal payMoney;
 
-    /** 支付类型 1微信支付 */
+    /**
+     * 支付类型 1微信支付
+     */
     @Excel(name = "支付类型 1微信支付")
     private Integer payType;
 
-    /** 是否支付 */
+    /**
+     * 是否支付
+     */
     @Excel(name = "是否支付")
     private Integer isPay;
 
-    /** 医生ID */
+    /**
+     * 医生ID
+     */
     @Excel(name = "医生ID")
     private Long doctorId;
 
-    /** 支付时间 */
+    /**
+     * 支付时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date payTime;
 
-    /** 状态 1待支付 2已支付 3已完成  -1已关闭 -2已退款 */
+    /**
+     * 状态 1待支付 2已支付 3已完成  -1已关闭 -2已退款
+     */
     @Excel(name = "状态 1待支付 2已支付 3已完成  -1已关闭 -2已退款")
     private Integer status;
 
-    /** 问诊开始时间 */
+    /**
+     * 问诊开始时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @Excel(name = "问诊开始时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date startTime;
 
-    /** 结束时间 */
+    /**
+     * 结束时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date finishTime;
 
-    /** 是否评价 */
+    /**
+     * 是否评价
+     */
     @Excel(name = "是否评价")
     private Integer isPing;
 
-    /** 科室ID */
+    /**
+     * 科室ID
+     */
     @Excel(name = "科室ID")
     private Long departmentId;
 
-    /** 问诊类型  1专家 2极速 3开药问诊 */
+    /**
+     * 问诊类型  1专家 2极速 3开药问诊
+     */
     @Excel(name = "问诊类型  1专家 2极速 3开药问诊")
     private Integer inquiryType;
 
-    /** 问诊子类型 1凯蒙名医 2健康草本 3御君方 3不良反应 */
+    /**
+     * 问诊子类型 1凯蒙名医 2健康草本 3御君方 3不良反应
+     */
     @Excel(name = "问诊子类型 1凯蒙名医 2健康草本 3御君方 3不良反应")
     private Integer inquirySubType;
 
-    /** 患者信息 */
+    /**
+     * 患者信息
+     */
     @Excel(name = "患者信息")
     private String patientJson;
 
-    /** $column.columnComment */
+    /**
+     * $column.columnComment
+     */
     @Excel(name = "患者信息")
     private Integer isReceive;
 
-    /** 支付订单号 */
+    /**
+     * 支付订单号
+     */
     @Excel(name = "支付订单号")
     private String tradeNo;
 
-    /** 是否审核 */
+    /**
+     * 是否审核
+     */
     @Excel(name = "是否审核")
     private Integer isAudit;
 
-    /** 审核人 */
+    /**
+     * 审核人
+     */
     @Excel(name = "审核人")
     private Long auditUserId;
 
-    /** 审核时间 */
+    /**
+     * 审核时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd")
     @Excel(name = "审核时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date auditTime;
@@ -251,259 +302,228 @@ public class FsInquiryOrder extends BaseEntity
         this.discountMoney = discountMoney;
     }
 
-    public void setOrderId(Long orderId)
-    {
+    public void setOrderId(Long orderId) {
         this.orderId = orderId;
     }
 
-    public Long getOrderId()
-    {
+    public Long getOrderId() {
         return orderId;
     }
-    public void setOrderSn(String orderSn)
-    {
+
+    public void setOrderSn(String orderSn) {
         this.orderSn = orderSn;
     }
 
-    public String getOrderSn()
-    {
+    public String getOrderSn() {
         return orderSn;
     }
-    public void setTitle(String title)
-    {
-        if(StringUtils.isNotEmpty(title)){
-            this.title= EmojiParser.parseToHtmlDecimal(title);
-        }
-        else{
-            this.title= title;
+
+    public void setTitle(String title) {
+        if (StringUtils.isNotEmpty(title)) {
+            this.title = EmojiParser.parseToHtmlDecimal(title);
+        } else {
+            this.title = title;
         }
     }
 
-    public String getTitle()
-    {
-        if(StringUtils.isNotEmpty(title)){
+    public String getTitle() {
+        if (StringUtils.isNotEmpty(title)) {
             return EmojiParser.parseToUnicode(title);
-        }
-        else{
+        } else {
             return title;
         }
     }
-    public void setImgs(String imgs)
-    {
+
+    public void setImgs(String imgs) {
         this.imgs = imgs;
     }
 
-    public String getImgs()
-    {
+    public String getImgs() {
         return imgs;
     }
-    public void setUserId(Long userId)
-    {
+
+    public void setUserId(Long userId) {
         this.userId = userId;
     }
 
-    public Long getUserId()
-    {
+    public Long getUserId() {
         return userId;
     }
-    public void setPatientId(Long patientId)
-    {
+
+    public void setPatientId(Long patientId) {
         this.patientId = patientId;
     }
 
-    public Long getPatientId()
-    {
+    public Long getPatientId() {
         return patientId;
     }
-    public void setOrderType(Integer orderType)
-    {
+
+    public void setOrderType(Integer orderType) {
         this.orderType = orderType;
     }
 
-    public Integer getOrderType()
-    {
+    public Integer getOrderType() {
         return orderType;
     }
-    public void setMoney(BigDecimal money)
-    {
+
+    public void setMoney(BigDecimal money) {
         this.money = money;
     }
 
-    public BigDecimal getMoney()
-    {
+    public BigDecimal getMoney() {
         return money;
     }
-    public void setPayMoney(BigDecimal payMoney)
-    {
+
+    public void setPayMoney(BigDecimal payMoney) {
         this.payMoney = payMoney;
     }
 
-    public BigDecimal getPayMoney()
-    {
+    public BigDecimal getPayMoney() {
         return payMoney;
     }
-    public void setPayType(Integer payType)
-    {
+
+    public void setPayType(Integer payType) {
         this.payType = payType;
     }
 
-    public Integer getPayType()
-    {
+    public Integer getPayType() {
         return payType;
     }
-    public void setIsPay(Integer isPay)
-    {
+
+    public void setIsPay(Integer isPay) {
         this.isPay = isPay;
     }
 
-    public Integer getIsPay()
-    {
+    public Integer getIsPay() {
         return isPay;
     }
-    public void setDoctorId(Long doctorId)
-    {
+
+    public void setDoctorId(Long doctorId) {
         this.doctorId = doctorId;
     }
 
-    public Long getDoctorId()
-    {
+    public Long getDoctorId() {
         return doctorId;
     }
-    public void setPayTime(Date payTime)
-    {
+
+    public void setPayTime(Date payTime) {
         this.payTime = payTime;
     }
 
-    public Date getPayTime()
-    {
+    public Date getPayTime() {
         return payTime;
     }
-    public void setStatus(Integer status)
-    {
+
+    public void setStatus(Integer status) {
         this.status = status;
     }
 
-    public Integer getStatus()
-    {
+    public Integer getStatus() {
         return status;
     }
-    public void setStartTime(Date startTime)
-    {
+
+    public void setStartTime(Date startTime) {
         this.startTime = startTime;
     }
 
-    public Date getStartTime()
-    {
+    public Date getStartTime() {
         return startTime;
     }
-    public void setFinishTime(Date finishTime)
-    {
+
+    public void setFinishTime(Date finishTime) {
         this.finishTime = finishTime;
     }
 
-    public Date getFinishTime()
-    {
+    public Date getFinishTime() {
         return finishTime;
     }
-    public void setIsPing(Integer isPing)
-    {
+
+    public void setIsPing(Integer isPing) {
         this.isPing = isPing;
     }
 
-    public Integer getIsPing()
-    {
+    public Integer getIsPing() {
         return isPing;
     }
-    public void setDepartmentId(Long departmentId)
-    {
+
+    public void setDepartmentId(Long departmentId) {
         this.departmentId = departmentId;
     }
 
-    public Long getDepartmentId()
-    {
+    public Long getDepartmentId() {
         return departmentId;
     }
-    public void setInquiryType(Integer inquiryType)
-    {
+
+    public void setInquiryType(Integer inquiryType) {
         this.inquiryType = inquiryType;
     }
 
-    public Integer getInquiryType()
-    {
+    public Integer getInquiryType() {
         return inquiryType;
     }
-    public void setInquirySubType(Integer inquirySubType)
-    {
+
+    public void setInquirySubType(Integer inquirySubType) {
         this.inquirySubType = inquirySubType;
     }
 
-    public Integer getInquirySubType()
-    {
+    public Integer getInquirySubType() {
         return inquirySubType;
     }
-    public void setPatientJson(String patientJson)
-    {
-        if(StringUtils.isNotEmpty(patientJson)){
-            this.patientJson= EmojiParser.parseToHtmlDecimal(patientJson);
-        }
-        else{
-            this.patientJson= patientJson;
+
+    public void setPatientJson(String patientJson) {
+        if (StringUtils.isNotEmpty(patientJson)) {
+            this.patientJson = EmojiParser.parseToHtmlDecimal(patientJson);
+        } else {
+            this.patientJson = patientJson;
         }
 
     }
 
-    public String getPatientJson()
-    {
-        if(StringUtils.isNotEmpty(patientJson)){
+    public String getPatientJson() {
+        if (StringUtils.isNotEmpty(patientJson)) {
             return EmojiParser.parseToUnicode(patientJson);
-        }
-        else{
+        } else {
             return patientJson;
         }
     }
-    public void setIsReceive(Integer isReceive)
-    {
+
+    public void setIsReceive(Integer isReceive) {
         this.isReceive = isReceive;
     }
 
-    public Integer getIsReceive()
-    {
+    public Integer getIsReceive() {
         return isReceive;
     }
-    public void setTradeNo(String tradeNo)
-    {
+
+    public void setTradeNo(String tradeNo) {
         this.tradeNo = tradeNo;
     }
 
-    public String getTradeNo()
-    {
+    public String getTradeNo() {
         return tradeNo;
     }
-    public void setIsAudit(Integer isAudit)
-    {
+
+    public void setIsAudit(Integer isAudit) {
         this.isAudit = isAudit;
     }
 
-    public Integer getIsAudit()
-    {
+    public Integer getIsAudit() {
         return isAudit;
     }
-    public void setAuditUserId(Long auditUserId)
-    {
+
+    public void setAuditUserId(Long auditUserId) {
         this.auditUserId = auditUserId;
     }
 
-    public Long getAuditUserId()
-    {
+    public Long getAuditUserId() {
         return auditUserId;
     }
-    public void setAuditTime(Date auditTime)
-    {
+
+    public void setAuditTime(Date auditTime) {
         this.auditTime = auditTime;
     }
 
-    public Date getAuditTime()
-    {
+    public Date getAuditTime() {
         return auditTime;
     }
 

+ 23 - 0
fs-service/src/main/java/com/fs/his/domain/FsPatient.java

@@ -1,5 +1,9 @@
 package com.fs.his.domain;
 
+import java.time.LocalDate;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
@@ -257,4 +261,23 @@ public class FsPatient extends BaseEntity
             .append("updateTime", getUpdateTime())
             .toString();
     }
+
+    //获取年龄,有出生日期就用出生日期,否则用身份证号
+    public final int getAge(){
+        if (this.getBirthday()!=null){
+            LocalDate currentDate = LocalDate.now();
+            return Period.between(this.getBirthday().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), currentDate).getYears();
+        }else {
+            String idCard = this.getIdCard();
+            String birthDateStr = idCard.substring(6, 14);
+            String formattedBirthDate = birthDateStr.substring(0, 4) + "-" +
+                    birthDateStr.substring(4, 6) + "-" +
+                    birthDateStr.substring(6, 8);
+
+            LocalDate birthDate = LocalDate.parse(formattedBirthDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+            LocalDate currentDate = LocalDate.now();
+            return Period.between(birthDate, currentDate).getYears();
+
+        }
+    }
 }

+ 26 - 0
fs-service/src/main/java/com/fs/his/mapper/FsInquiryOrderMapper.java

@@ -1,6 +1,9 @@
 package com.fs.his.mapper;
 
 import java.util.List;
+import java.util.Map;
+
+import com.fs.course.param.InquiryOrderHsLog;
 import com.fs.his.domain.FsInquiryOrder;
 import com.fs.his.domain.FsInquiryOrderDTO;
 import com.fs.his.domain.FsInquiryOrderMsg;
@@ -389,4 +392,27 @@ public interface FsInquiryOrderMapper
     void closeOrder(Long orderId);
 
     List<FsInquiryOrderDTO> selectNeedUploadData(String date);
+
+    int insertHsLog(@Param("responseBody") String responseBody,
+                     @Param("inquiryOrderId") Long inquiryOrderId,
+                     @Param("patientId") Long patientId,
+                     @Param("userId") String userId,
+                     @Param("status") int i,
+                     @Param("responseData") String stringObjectMap,
+                     @Param("bookNo") String bookNo);
+
+    InquiryOrderHsLog selectHsLog(@Param("inquiryOrderId") String inquiryOrderId);
+
+    void updateHsLog(@Param("orderId")Long orderId,@Param("jsonStr")String jsonStr);
+
+    @Select("SELECT id,response_json," +
+            "inquiry_order_id," +
+            "patient_id," +
+            "user_id," +
+            "type," +
+            "decode_json," +
+            "book_no," +
+            "create_time," +
+            "update_time FROM fs_inquiry_order_hs_log WHERE book_no = #{bookNo}")
+    InquiryOrderHsLog selectHsLogByBookNo(@Param("bookNo") String bookNo);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStoreProductMapper.java

@@ -281,4 +281,6 @@ public interface FsStoreProductMapper {
             "</if>" +
             "</script>"})
     List<FsStoreProductListVO> liveList(@Param("maps") LiveGoods maps);
+
+    FsStoreProduct selectFsStoreProductByBarCode(@Param("barCode") String barCode);
 }

+ 21 - 0
fs-service/src/main/java/com/fs/his/service/IFsInquiryPatientInfoService.java

@@ -1,13 +1,20 @@
 package com.fs.his.service;
 
+import cn.hutool.http.HttpRequest;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fs.common.core.domain.R;
 import com.fs.course.param.FsInquiryPatientInfoUParam;
+import com.fs.course.param.HsDoctorSchedule;
+import com.fs.course.param.SendInquiryParam;
 import com.fs.course.vo.FsInquiryPatientInfoListUVO;
 import com.fs.his.domain.FsInquiryPatientInfo;
 import com.fs.his.param.FsInquiryPatientParam;
 import com.fs.his.vo.FsInquiryPatientVO;
 import org.apache.ibatis.annotations.Param;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 问诊患者信息Service接口
@@ -24,4 +31,18 @@ public interface IFsInquiryPatientInfoService {
     List<FsInquiryPatientInfoListUVO> selectFsInquiryPatientInfoListUVO(FsInquiryPatientInfoUParam param);
 
     FsInquiryPatientInfoListUVO getInfo(Long id);
+
+    R sendInquiryToHS(SendInquiryParam param) throws Exception;
+
+    R receiveInquiryFromHsCallbackNotification(HttpServletRequest param) throws Exception;
+
+    List<HsDoctorSchedule> getDoctorScheduleFromHs() throws Exception;
+
+    Map redirectToHs(String inquiryOrderId) throws Exception;
+
+    R cancelInquiryToHs(String inquiryOrderId);
+
+//    void updateFsInquiryPatientInfoFromHsRedirect(HttpServletRequest param) throws JsonProcessingException;
+
+//    void recieveInquiryFromHsDispensary(HttpServletRequest param) throws JsonProcessingException;
 }

+ 27 - 12
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java

@@ -16,18 +16,17 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.ImTypeEnum;
 import com.fs.common.exception.CustomException;
-import com.fs.common.service.impl.SmsServiceImpl;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.Company;
-import com.fs.company.domain.CompanyMoneyLogs;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyMoneyLogsMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.config.WxPayProperties;
 import com.fs.core.utils.OrderCodeUtils;
@@ -38,7 +37,10 @@ import com.fs.event.TemplateEvent;
 import com.fs.event.TemplateListenEnum;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
-import com.fs.his.dto.*;
+import com.fs.his.dto.FsInquiryOrderPatientDTO;
+import com.fs.his.dto.FsPackagePatientDTO;
+import com.fs.his.dto.FsPriceDTO;
+import com.fs.his.dto.InquiryConfigDTO;
 import com.fs.his.enums.FsInquiryOrderStatusEnum;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.*;
@@ -50,26 +52,25 @@ import com.fs.his.vo.*;
 import com.fs.huifuPay.domain.HuiFuRefundResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayRefundRequest;
 import com.fs.huifuPay.service.HuiFuService;
-import com.fs.im.config.IMConfig;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.dto.*;
 import com.fs.im.service.IImService;
 import com.fs.im.service.OpenIMService;
 import com.fs.jpush.service.JpushService;
-import com.fs.repeat.vo.RepeatUploadVo;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
-import com.fs.tzBankPay.doman.*;
+import com.fs.system.service.ISysConfigService;
+import com.fs.tzBankPay.TzBankService.TzBankService;
+import com.fs.tzBankPay.doman.PayType;
+import com.fs.tzBankPay.doman.RefundParam;
+import com.fs.tzBankPay.doman.TzBankResult;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.ybPay.domain.RefundResult;
 import com.fs.ybPay.dto.OrderQueryDTO;
 import com.fs.ybPay.dto.RefundDTO;
 import com.fs.ybPay.service.IPayService;
-import com.fs.system.service.ISysConfigService;
-import com.fs.tzBankPay.TzBankService.TzBankService;
-import com.fs.tzBankPay.config.TzBankConfig;
 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
 import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
 import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryResult;
@@ -95,13 +96,11 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
-import static com.fs.common.utils.SecurityUtils.getUserId;
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
 
 /**
@@ -195,6 +194,9 @@ public class FsInquiryOrderServiceImpl implements IFsInquiryOrderService
     private ConfigUtil configUtil;
     @Autowired
     private OpenIMService openIMService;
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     /**
      * 查询问诊订单
      *
@@ -777,7 +779,20 @@ public class FsInquiryOrderServiceImpl implements IFsInquiryOrderService
                 String redisKey = String.valueOf(StrUtil.format("{}{}", FsConstants.REDIS_INQUIRY_ORDER_OUTTIME_UNPAY, order.getOrderId()));
                 redisCache.setCacheObject(redisKey,order.getOrderId(),configDTO.getUnPayCancelTime(), TimeUnit.MINUTES);
             }
-
+            /*今正科技需要调用红杉*/
+//            if (("今正科技".equals(cloudHostProper.getCompanyName()))) {
+//                order.getOrderId();
+//                param.getUserId();
+//                IFsInquiryPatientInfoService fsInquiryPatientInfoService = SpringUtils.getBean(IFsInquiryPatientInfoService.class);
+//                try {
+//                    cloudHostProper.getCompanyName()
+//                    fsInquiryPatientInfoService.sendInquiryToHS(new SendInquiryParam(param.getPatientId(),order.getOrderId(),param.getUserId(),param));
+//                  //todo目前让前端直接调用
+//                } catch (Exception e) {
+//                    e.printStackTrace();
+//                    return R.error("创建失败"+e);
+//                }
+//            }
             return R.ok().put("order",order);
         }
         else{

+ 920 - 8
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryPatientInfoServiceImpl.java

@@ -1,26 +1,68 @@
 package com.fs.his.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.BeanCopyUtils;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.ImTypeEnum;
 import com.fs.common.exception.CustomException;
-import com.fs.course.param.FsInquiryPatientInfoUParam;
+import com.fs.common.utils.HsCryptoUtil;
+import com.fs.common.utils.StringToMapUtil;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.param.*;
 import com.fs.course.vo.FsInquiryPatientInfoListUVO;
-import com.fs.his.domain.FsInquiryPatientInfo;
-import com.fs.his.mapper.FsInquiryOrderMapper;
-import com.fs.his.mapper.FsInquiryPatientInfoMapper;
+import com.fs.his.domain.*;
+import com.fs.his.dto.FsInquiryOrderPatientDTO;
+import com.fs.his.dto.PayloadDTO;
+import com.fs.his.mapper.*;
 import com.fs.his.param.FsInquiryPatientParam;
-import com.fs.his.service.IFsInquiryPatientInfoService;
+import com.fs.his.service.*;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.DoctorAdviceVO;
 import com.fs.his.vo.FsInquiryPatientVO;
-import org.checkerframework.checker.units.qual.A;
+import com.fs.hisStore.domain.HsPrescribScrm;
+import com.fs.hisStore.domain.PrescriptionInfoDetails;
+import com.fs.im.config.ImTypeConfig;
+import com.fs.im.dto.OpenImMsgDTO;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.service.OpenIMService;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
-import java.util.Collections;
-import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
 
 /**
  * 问诊患者信息Service业务层处理
@@ -34,6 +76,71 @@ public class FsInquiryPatientInfoServiceImpl implements IFsInquiryPatientInfoSer
 
     @Autowired
     private FsInquiryOrderMapper inquiryOrderMapper;
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private FsPatientMapper fsPatientMapper;
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    private IFsDoctorPrescribeService doctorPrescribeService;
+    @Autowired
+    private IFsDoctorPrescribeDrugService doctorPrescribeDrugService;
+    @Autowired
+    private IFsDoctorService doctorService;
+    @Autowired
+    private IFsPrescribeService prescribeService;
+    @Autowired
+    private IFsPrescribeDrugService prescribeDrugService;
+    @Autowired
+    private OpenIMService openIMService;
+
+    //咨询问诊域名
+    private final String hsInquiryOrderUrl = "https://dev.om.yfttech.cn";
+    //咨询问诊url
+    private final String hsInquiryOrderInquiryUrl = "/open_platform/hospital/suppliers/v2/book_order";
+    //购药问诊url
+    private final String hsInquiryOrderMedicineUrl = "/open_platform/hospital/suppliers/v1/book_order";
+
+    //应用秘钥
+    private final String appSecret = "ExbfudweIeSCFLtp";
+    // 红杉平台RSA公钥(加密AES密钥用)
+    private final String platformPublicKey = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDL2dEZXlQMENzTmI4Vzc3aWVxU0poTlpHeQo3bGpLOVFKZ1kwcHR1cDJ3NUJHZFR1TGpJRFU5ZzFHMkIxNlgrdXZWa0gwYWY5SWpBdGs3ZWRBQnRpK05OUjI0CmlBUDdDa2MrRk9NQjEyYUFXVHA3R0VPS0pZQS9ESkJEdWsrc1pMa2dRUDFubXJuL28xNGJLQk1SQVhMKzltcFAKWERkNUNENUhPUWNJT1JkeVl3SURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo";
+    // 三方平台RSA私钥(解密响应密钥用)
+    private final String thirdPrivateKey = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUNkd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQW1Fd2dnSmRBZ0VBQW9HQkFMWmxsblI4SE5Qb0N2WFkKVFBCTmtLdFArejc0NVA4ZzlzZUVHVDZUbysxdmp4Sk9jc2VRNWpFNnR4Rm5IQlJsc3psam5sUHdYT25HYStFLwo3Q2phbElIZjdTZ2p0SzlKSCtpckd5eW1yaVN6cEYyMFpKT2wxQXhsams2UUVFdGRRS0VqYWI2NjFGVHN2clRDCjhaRzAzS05NOVVjM2F6SGY2bVRRUXJtRWp1WjlBZ01CQUFFQ2dZRUF0Zjc5dG5OVkRIaWYzeGthQkRsUkhpOHIKWW5WVmdlRHhmTGtwdTAvMEpPbkkxNXBoV3hJUkxwUUlzUnV5WUFQdVpsZ3BWbFlqVDd5R1RuYksvU1RGUW5YVwpGS05pUEtjQ1pXdXhBdkNhRFFCZ0ZHdXZpMjB6bjBBZ2hEU0tycEIrYU9NL1VGdjhFZWx0cE42WDA0Vis1RlY2CitwWGFsdkZjUlhUWEVHMXBTUUVDUVFEbnVFT2VmcUxKb1pDK3lwMlovMTFFRm0yaWxOL2VKOFBpdzRFUm5WdTkKRU5TWm9OdURIUjFSeVFlYWVzeXU3WlFadzV2d0VvZkFlL1MzV2ZhVlFBM0JBa0VBeVlKRVJEcmdLSHF3YzYyOQpkLys4RGNVOEtZei9hQ3ZqVm9kUGRmMVMwbXZpdUlUS2txYVZhdDFCTis2MjZMcFU2c1BsNVVHeWZ5aXlVc3FTCnpmSi92UUpBRDhGUGw2ODBrbEVSN21jSVlEZ2t0MFJ2SCtiUGNlTnlSakRVemNYTlB3V3Q3dVFwQ0xrcURTMkYKL3RMcXA5b3ZmN0QxSVZXaE5VMDRUbDhuak81V0FRSkJBS01wS0M5NjRJL0dMK09xbFJSNTdJSFY1dzNaemVCQwpVUlI2QVZ3UEh5V2tGM0xDaXVmTm5JUm4zR3YyalFIS0JnSUZWcnVYdzNqMHNkY1prVjdTY0owQ1FGVGJJTnRCCi9OcW5laWI3U3d4ZCt5NHZ4UkRHZVRxZUFjNXE5ZU5RNy8vbUFOWlZyUTZKSEJnZ1NjT3h6ZFhFZFZ5UTk5Z1AKYmQ3c2Z2Mlk2WGdhd3FFPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg";
+    //appid唯一标识
+    private final String appId = "TpmBATELpdeMbjtjdaVIGbznlJDRjLDw";
+    private final String localUrl = "192.168.31.1:8113/app/inquiryPatient";
+    //红杉已开具处方
+    private final String prescriptionHsEnd = "prescription_pdf_end";
+    /*获取红杉诊室url*/
+    private final String hsConsultingRoomUrl = "/open_platform/hospital/suppliers/v2/book_order/group";
+    /*红杉跳转小程序版本 develop 开发版, trial 体验版, release 正式版*/
+    private final String hsEnvVersion = "trial";
+    /*银川互联网医院处方查询url*/
+    private final String hsPrescriptionUrl = "/open_platform/hospital/suppliers/v1/book_order/prescription";
+
+    /**
+     * 向红杉发起问诊申请
+     */
+    public static final String LOCK_KEY_Inquiry_HS = "inquiry:hs:lock:%d";
+    //    /**
+//     * 向红杉发起问诊申请后返回的bookNo
+//     */
+//    public static final String CACHE_BOOK_NO_HS = "cache:hs:book:no:%d";
+    /*红杉医生排班缓存key*/
+    private final String CACHE_HS_DOCTOR_SCHEDULE_LIST = "cache:doctor:schedule:hs:list";
+    private final String CACHE_HS_DOCTOR_SCHEDULE_HASH = "cache:doctor:schedule:hs:hash";
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    private SpringUtils springUtils;
+    @Autowired
+    private FsStoreProductMapper fsStoreProductMapper;
+    @Qualifier("redisTemplate")
+
+
     @Override
     public int insertFsInquiryPatientInfo(FsInquiryPatientParam param) {
         FsInquiryPatientInfo fsInquiryPatientInfo = new FsInquiryPatientInfo();
@@ -75,4 +182,809 @@ public class FsInquiryPatientInfoServiceImpl implements IFsInquiryPatientInfoSer
     public FsInquiryPatientInfoListUVO getInfo(Long id) {
         return fsInquiryPatientInfoMapper.getInfo(id);
     }
+
+    @Override
+    public R sendInquiryToHS(SendInquiryParam param) throws Exception {
+        //参数校验
+        if (param.getPatientId() == null || param.getInquiryOrderId() == null || param.getUserId() == null) {
+            throw new RuntimeException("参数错误");
+        }
+        FsInquiryOrder fsInquiryOrder = inquiryOrderMapper.selectFsInquiryOrderByOrderId(param.getInquiryOrderId());
+        if (fsInquiryOrder == null) {
+            return R.error("未查询到该问诊单");
+        }
+        FsPatient fsPatient = fsPatientMapper.selectFsPatientByPatientId(param.getPatientId());
+        if (fsPatient == null) {
+            return R.error("未查询到该患者");
+        }
+        RLock lock = redissonClient.getLock(String.format(LOCK_KEY_Inquiry_HS, param.getInquiryOrderId()));
+        try {
+            boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
+
+            if (!locked) {
+                logger.warn("问诊单正在处理中,获取锁失败, 订单号: {}", param.getInquiryOrderId());
+                return R.error("订单正在处理中,请勿重复提交");
+            }
+
+            SendInquiryToHSParam hsParam = getHsParam(param.getUserId(), fsInquiryOrder, fsPatient, param.getAppId(), param.getInquiryType(), param.getDoctor(), param.getVisitRange(), param.getBranch());
+            // 使用ObjectMapper转换,会自动处理@JsonProperty注解
+            ObjectMapper mapper = new ObjectMapper();
+            Map<String, Object> map = mapper.convertValue(hsParam, Map.class);
+            JSONObject jsonObject = new JSONObject(map);
+            String request = JSONUtil.toJsonStr(encryptRequest(jsonObject));
+            String urlWithParams = hsInquiryOrderUrl + (param.getInquiryType() == 1 ? hsInquiryOrderInquiryUrl : hsInquiryOrderMedicineUrl);
+            String responseBody = "";
+            try {
+                responseBody = sendPostRequest(request, urlWithParams,"POST");
+            } catch (Exception e) {
+                redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_LIST);
+                redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_HASH);
+                logger.error("请求失败:{}", e.getMessage());
+                return R.error("向红杉发起咨询问诊失败");
+            }
+            Map<String, Object> stringObjectMap = decryptResponse(responseBody);
+            Map<String, Object> data = (Map) stringObjectMap.get("data");
+            if (data.get("book_no") == null) {
+                redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_LIST);
+                redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_HASH);
+                return R.error("咨询问诊单在红衫异常,请联系管理员");
+            }
+            //把红杉响应数据存数据库,方便扯皮
+            inquiryOrderMapper.insertHsLog(responseBody, param.getInquiryOrderId(), param.getPatientId(), param.getUserId(), 1, stringObjectMap.toString(), (String) data.get("book_no"));
+
+            R.ok(String.valueOf(param.getInquiryOrderId()));
+        } catch (InterruptedException e) {
+            logger.error("问诊申请的过程被中断, 问诊单号: {}", param.getInquiryOrderId(), e);
+            return R.error("问诊申请被中断,请稍后重试");
+        } catch (Throwable e) {
+            logger.error("问诊申请过程中发生异常, 问诊单号: {}", param.getInquiryOrderId(), e);
+            return R.error("问诊申请过程中发生异常,请稍后重试");
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.debug("问诊申请锁已释放, 问诊单号: {}", param.getInquiryOrderId());
+            }
+
+        }
+        return R.error();
+    }
+
+
+    @Override
+    public R receiveInquiryFromHsCallbackNotification(HttpServletRequest param) throws Exception {
+//        解析请求
+//         1. 读取请求体内容
+        StringBuilder requestBody = new StringBuilder();
+        try (BufferedReader reader = param.getReader()) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                requestBody.append(line);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+//         2. 解析JSON
+        Map<String, Object> jsonMap = new ObjectMapper().readValue(
+                requestBody.toString(), Map.class
+        );
+        //判断行为是否为已开处方
+        String action = (String) jsonMap.get("action");
+        String bookNo = null;
+        if (action.equals(prescriptionHsEnd)) {
+            Map<String, Object> map = decryptResponse(String.valueOf(requestBody));
+            if (map != null && !map.isEmpty() && map.containsKey("book_info")) {
+                if (map.get("book_info") != null && map.get("book_info") instanceof Map) {
+                    Object o = map.get("book_info");
+                    if (ObjectUtil.isNotEmpty(o) && o instanceof Map) {
+                        if (((Map<?, ?>) o).containsKey("book_no")) {
+                            bookNo = ((Map<?, ?>) o).get("book_no").toString();
+                        }
+                    }
+                }
+            }
+            if (bookNo == null) {
+                logger.error("银川互联网医院通知回调处方开具异常,订单号为空");
+                return R.error("银川互联网医院通知回调处方开具异常,订单号为空");
+            }
+            /*根据bookNo查hslog*/
+            InquiryOrderHsLog hsLog = inquiryOrderMapper.selectHsLogByBookNo(bookNo);
+            /*插入hsLog*/
+            inquiryOrderMapper.insertHsLog(requestBody.toString(), hsLog.getInquiryOrderId(), hsLog.getPatientId(),
+                    String.valueOf(hsLog.getUserId()), 2, map.toString(), bookNo);
+            /*用orderid和bookno拼参发起处方查询*/
+
+            HashMap<String, Object> objectObjectHashMap = MapUtil.newHashMap();
+            objectObjectHashMap.put("union_code", hsLog.getInquiryOrderId());
+            objectObjectHashMap.put("book_no", bookNo);
+            JSONObject jsonObject = new JSONObject(objectObjectHashMap);
+            String request = JSONUtil.toJsonStr(encryptRequest(jsonObject));
+            String urlWithParams = hsInquiryOrderUrl + hsPrescriptionUrl;
+            String responseBody = "";
+            try {
+                responseBody = sendPostRequest(request, urlWithParams, "GET");
+            } catch (Exception e) {
+                logger.error("请求失败:{}", e.getMessage());
+                return R.error("请求失败:" + e.getMessage());
+            }
+            Map<String, Object> stringObjectMap = decryptResponse(responseBody);
+            if (stringObjectMap.isEmpty()) {
+                logger.error("处方查询响应异常");
+                return R.error("处方查询响应异常");
+            }
+            Map<String, Object> data = (Map) stringObjectMap.get("data");
+            HsPrescribScrm hsPrescribScrm = BeanUtil.toBean(data, HsPrescribScrm.class);
+            //todo 新增处方和药单
+            /*走doctorapp.PrescribeController#createPrescribe*/
+            /*拼医生处方,注释掉的是未在红杉找到对应数据的*/
+            FsDoctorPrescribe doctorPrescribe = new FsDoctorPrescribe();
+//            doctorPrescribe.setPrescribeType();
+            doctorPrescribe.setDiagnose(hsPrescribScrm.getMedicalRecord().getDiagnose().toString());
+//            doctorPrescribe.setDoctorId();
+//            doctorPrescribe.setStoreId();
+            doctorPrescribe.setUsageJson(hsPrescribScrm.getPrescriptionInfo().getDetails().toString());
+//            doctorPrescribe.setRecipeType();
+            doctorPrescribe.setCycle(hsPrescribScrm.getPrescriptionInfo().getDetails().get(0).getTotal());//用的总数,未知响应数据
+            doctorPrescribeService.insertFsDoctorPrescribe(doctorPrescribe);
+
+            for (PrescriptionInfoDetails hsDrug : hsPrescribScrm.getPrescriptionInfo().getDetails()
+            ) {
+                FsStoreProduct fsStoreProduct = fsStoreProductMapper.selectFsStoreProductByBarCode(hsDrug.getBarCode());
+
+                FsDoctorPrescribeDrug drug = new FsDoctorPrescribeDrug();
+                drug.setPrescribeId(doctorPrescribe.getPrescribeId());
+                drug.setDrugName(hsDrug.getName());
+                drug.setDrugSpec(hsDrug.getFormat());
+                drug.setUsageMethod(hsDrug.getUsage());
+                drug.setUsageFrequencyUnit(hsDrug.getFrequency());
+                drug.setUsagePerUseCount(hsDrug.getDose());
+                drug.setUsagePerUseUnit(hsDrug.getUnit());
+//                drug.setUsageDays();
+                drug.setDrugPrice(fsStoreProduct.getPrice());
+                drug.setDrugNum(hsDrug.getTotal().longValue());
+                drug.setDrugUnit(hsDrug.getUnit());
+                drug.setInstructions(hsDrug.getRequirements());
+                /*fs_store_product为药品表,甲方自己维护,双方系统互通*/
+                drug.setProductId(fsStoreProduct.getProductId());
+                drug.setDrugImgUrl(fsStoreProduct.getImgUrl());
+                drug.setProductAttrValueId(fsStoreProduct.getSpecType().longValue());
+                drug.setRemark(hsDrug.getLog().toString());
+                drug.setDrugType(fsStoreProduct.getProductType());
+                drug.setIsDrug(fsStoreProduct.getIsDrug());
+                doctorPrescribeDrugService.insertFsDoctorPrescribeDrug(drug);
+            }
+
+            //订单生成
+            FsDoctorPrescribeDrug drugMap=new FsDoctorPrescribeDrug();
+            drugMap.setPrescribeId(doctorPrescribe.getPrescribeId());
+            FsInquiryOrder inquiryOrder=inquiryOrderMapper.selectFsInquiryOrderByOrderId(hsLog.getInquiryOrderId());
+            List<FsDoctorPrescribeDrug> doctorPrescribeDrugs=doctorPrescribeDrugService.selectFsDoctorPrescribeDrugList(drugMap);
+
+            String orderSn = OrderCodeUtils.getOrderSn();
+            if (StringUtils.isEmpty(orderSn)) {
+                return R.error("订单生成失败,请重试");
+            }
+            FsPrescribe prescribe = new FsPrescribe();
+            BeanUtils.copyProperties(doctorPrescribe, prescribe);
+            prescribe.setInquiryOrderId(inquiryOrder.getOrderId());
+            prescribe.setUserId(inquiryOrder.getUserId());
+            prescribe.setPatientId(inquiryOrder.getPatientId());
+            prescribe.setPrescribeCode(orderSn);
+            prescribe.setIcdCode(doctorPrescribe.getIcdCode());
+            FsInquiryOrderPatientDTO patientDTO = JSONUtil.toBean(inquiryOrder.getPatientJson(), FsInquiryOrderPatientDTO.class);
+            prescribe.setPatientAge(patientDTO.getAge());
+            prescribe.setPatientName(patientDTO.getPatientName());
+            prescribe.setPatientAge(patientDTO.getAge());
+            prescribe.setPatientDescs(patientDTO.getTitle());
+            prescribe.setPatientTel(patientDTO.getMobile());
+            prescribe.setPatientGender(patientDTO.getSex().toString());
+            prescribe.setPatientBirthday(patientDTO.getBirthday());
+            prescribe.setRecordPic(inquiryOrder.getImgs());
+            prescribe.setDiagnose(prescribe.getDiagnose());
+            prescribe.setDoctorId(inquiryOrder.getDoctorId());
+            prescribe.setStatus(0);
+            prescribe.setUsageJson(prescribe.getUsageJson());
+            prescribe.setCycle(doctorPrescribe.getCycle());
+            prescribe.setRemark(prescribe.getRemark());
+            if (inquiryOrder.getSource() != null) {
+                prescribe.setSource(inquiryOrder.getSource());
+            }
+
+            FsDoctor doctor = doctorService.selectFsDoctorByDoctorId(1L);//hs医生排班接口获取
+            if (doctor != null && StringUtils.isNotEmpty(doctor.getSignUrl())) {
+                prescribe.setDoctorSignUrl(doctor.getSignUrl());
+            }
+            if (doctor != null && doctor.getIsPrescribeDoctor() != null && doctor.getIsPrescribeDoctor() == 1) {
+                prescribe.setPrescribeDoctorId(doctor.getDoctorId());
+                prescribe.setPrescribeDoctorSignUrl(doctor.getSignUrl());
+            } else {
+
+                if (doctor.getPrescribeDoctorId() != null) {
+                    FsDoctor prescribeDoctor = doctorService.selectFsDoctorByDoctorId(doctor.getPrescribeDoctorId());
+                    if (prescribeDoctor != null) {
+                        prescribe.setPrescribeDoctorId(prescribeDoctor.getDoctorId());
+                        prescribe.setPrescribeDoctorSignUrl(prescribeDoctor.getSignUrl());
+                    }
+                }
+            }
+            if (prescribeService.insertFsPrescribe(prescribe) > 0) {
+                //写入药品
+                for (FsDoctorPrescribeDrug drug : doctorPrescribeDrugs) {
+                    FsPrescribeDrug prescribeDrug = new FsPrescribeDrug();
+                    BeanUtils.copyProperties(drug, prescribeDrug);
+                    prescribeDrug.setProductAttrValueId(drug.getProductAttrValueId());
+                    prescribeDrug.setPrescribeId(prescribe.getPrescribeId());
+                    prescribeDrugService.insertFsPrescribeDrug(prescribeDrug);
+                }
+                if (ImTypeConfig.IMTYPE == ImTypeEnum.OPENIM) {
+                    logger.info("拼接消息体");
+                    //部署到正式环境以后下面这段代码要注释,使用定时任务执行auditPrescribe审核处方以后发送消息
+                    ObjectMapper objectMapper = new ObjectMapper();
+                    OpenImMsgDTO openImMsgDTO = new OpenImMsgDTO();
+                    openImMsgDTO.setSendID("D" + inquiryOrder.getDoctorId());
+                    openImMsgDTO.setRecvID("U" + inquiryOrder.getUserId());
+                    openImMsgDTO.setContentType(110);
+                    openImMsgDTO.setSenderPlatformID(5);
+                    openImMsgDTO.setSessionType(1);
+                    OpenImMsgDTO.Content content = new OpenImMsgDTO.Content();
+                    //content.setContent(ext);
+                    PayloadDTO payload = new PayloadDTO();
+                    payload.setData("prescribe");
+                    PayloadDTO.Extension extension = new PayloadDTO.Extension();
+                    extension.setDiagnose(inquiryOrder.getTitle());
+                    extension.setPrescribeId(prescribe.getPrescribeId().toString());
+                    payload.setExtension(extension);
+                    extension.setStatus(prescribe.getStatus());
+                    OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
+
+                    imData.setPayload(payload);
+
+                    String imJson = objectMapper.writeValueAsString(imData);
+                    content.setData(imJson);
+                    openImMsgDTO.setContent(content);
+
+                    JSONObject jsonObjectNew = new JSONObject(openImMsgDTO);
+                    logger.info("开始发送消息");
+                    OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
+                    offlinePushInfo.setDesc("处方单");
+                    offlinePushInfo.setTitle(doctor.getDoctorName());
+                    offlinePushInfo.setIOSBadgeCount(true);
+                    offlinePushInfo.setIOSPushSound("");
+                    openImMsgDTO.setOfflinePushInfo(offlinePushInfo);
+                    OpenImResponseDTO openImResponseDTO = openIMService.openIMSendMsg(openImMsgDTO);
+                }
+            }
+        }
+        return R.ok();
+        }
+
+    @Override
+    public List<HsDoctorSchedule> getDoctorScheduleFromHs() throws Exception {
+        ArrayList<HsDoctorSchedule> hsDoctorSchedules = new ArrayList<>();
+        List range = redisTemplate.opsForList().range(CACHE_HS_DOCTOR_SCHEDULE_LIST, 0, -1);
+        if (range != null && !range.isEmpty()){
+            ArrayList<Integer> doctorIdList = new ArrayList<>(range);
+            doctorIdList.forEach(i -> {
+                Object o = redisTemplate.opsForHash().get(CACHE_HS_DOCTOR_SCHEDULE_HASH, i + "");
+                if (o != null) {
+                    hsDoctorSchedules.add(JSONUtil.toBean(o.toString(), HsDoctorSchedule.class));
+                }
+            });
+            /*要做一次筛选,存入我们doctor表*/
+            return hsDoctorSchedules;
+        }
+
+        HashMap<String, Object> map = new HashMap<>();
+        LocalDate today = LocalDate.now();
+        String todayFormat = today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        LocalDate tomorrow = today.plusDays(1);
+        String tomorrowFormat = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        map.put("start_date", todayFormat);
+        map.put("end_date", tomorrowFormat);
+        map.put("symptom", Arrays.asList(""));
+        JSONObject jsonObject = new JSONObject(map);
+
+        Map<String, String> stringStringMap = encryptRequest(jsonObject);
+//        stringStringMap.put("_method", "GET");//按他们要求填参异常
+        String requestBody = JSONUtil.toJsonStr(stringStringMap);
+        String responseBody = null;
+
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            // 构建URL
+            HttpGet httpGet = new HttpGet(hsInquiryOrderUrl + "/open_platform/hospital/suppliers/v1/hsadmin/schedule");
+
+            // 设置请求头
+            httpGet.setHeader("appid", appId);
+            httpGet.setHeader("Accept", "application/json");
+            httpGet.setHeader("Content-Type", "application/json");
+            httpGet.setHeader("User-Agent", "Mozilla/5.0");
+
+            // 设置请求体
+            StringEntity entity = new StringEntity(requestBody, StandardCharsets.UTF_8);
+            httpGet.setEntity(entity);
+
+            // 执行请求
+            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+                int statusCode = response.getCode();
+                System.out.println("响应码: " + statusCode);
+                responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+
+            }
+
+        } catch (Exception e) {
+            logger.error("向红杉发起请求失败: " + e.getMessage());
+            e.printStackTrace();
+            throw e;
+        }
+        Map<String, Object> stringObjectMap = decryptResponse(responseBody);
+        if (ObjectUtil.isNotEmpty(stringObjectMap) && stringObjectMap.get("data") != null) {
+            List<Map<String, Object>> rawDataList = (List<Map<String, Object>>) stringObjectMap.get("data");
+            rawDataList.forEach(o -> {
+                HsDoctorSchedule item = JSONUtil.toBean(JSONUtil.toJsonStr(o), HsDoctorSchedule.class);
+                Object scheduleObj = o.get("schedule");
+                if (scheduleObj != null) {
+                    if (scheduleObj instanceof String) {
+                        // 如果是字符串,则解析为 Map
+                        try {
+                            JSONObject jsonObj = JSONUtil.parseObj((String) scheduleObj);
+                            Map<String, Integer> scheduleMap = new HashMap<>();
+                            for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
+                                if (entry.getValue() instanceof Number) {
+                                    scheduleMap.put(entry.getKey(), ((Number) entry.getValue()).intValue());
+                                } else if (entry.getValue() instanceof String) {
+                                    try {
+                                        scheduleMap.put(entry.getKey(), Integer.parseInt((String) entry.getValue()));
+                                    } catch (NumberFormatException e) {
+                                        scheduleMap.put(entry.getKey(), 0);
+                                    }
+                                }
+                            }
+                            item.setSchedule(scheduleMap);
+                        } catch (Exception e) {
+                            logger.warn("解析 schedule 字符串失败: {}", scheduleObj, e);
+                        }
+                    } else if (scheduleObj instanceof Map) {
+                        // 如果已经是 Map,则直接转换类型
+                        try {
+                            Map<String, Integer> scheduleMap = new HashMap<>();
+                            Map<?, ?> rawMap = (Map<?, ?>) scheduleObj;
+                            for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+                                if (entry.getKey() instanceof String && entry.getValue() instanceof Number) {
+                                    scheduleMap.put((String) entry.getKey(), ((Number) entry.getValue()).intValue());
+                                }
+                            }
+                            item.setSchedule(scheduleMap);
+                        } catch (Exception e) {
+                            logger.warn("转换 schedule Map 失败: {}", scheduleObj, e);
+                        }
+                    }
+
+                    if (item.getSchedule() != null) {
+                        String currentTimeSlot = getCurrentValidTimeSlot(item.getSchedule());
+                        if (currentTimeSlot != null) {
+                            hsDoctorSchedules.add(item);
+                        }
+                    }
+                }
+            });
+        }
+        if (!hsDoctorSchedules.isEmpty()){
+            /*直接把排班数据的医生存入fs_doctor*/
+//            FsDoctorServiceImpl
+        }
+        List<Integer> collect = hsDoctorSchedules.stream().map(o -> o.getDoctor()).collect(Collectors.toList());
+        try {
+            // 先清除旧的缓存数据
+            redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_LIST);
+            redisTemplate.delete(CACHE_HS_DOCTOR_SCHEDULE_HASH);
+            // 存储医生ID列表
+            Long listResult = redisTemplate.opsForList().leftPushAll(CACHE_HS_DOCTOR_SCHEDULE_LIST, collect);
+            if (listResult > 0) {
+                // 存储每个医生的详细排班信息到hash中
+                for (HsDoctorSchedule schedule : hsDoctorSchedules) {
+                    redisTemplate.opsForHash().put(
+                            CACHE_HS_DOCTOR_SCHEDULE_HASH,
+                            schedule.getDoctor().toString(),
+                            JSONUtil.toJsonStr(schedule)
+                    );
+                }
+//                long expireSeconds = Duration.between(LocalDateTime.now(),
+//                        LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MIDNIGHT)).getSeconds();
+                // 为缓存设置过期时间每天00点过期
+                redisTemplate.expire(CACHE_HS_DOCTOR_SCHEDULE_LIST, 1, TimeUnit.MINUTES);
+                redisTemplate.expire(CACHE_HS_DOCTOR_SCHEDULE_HASH, 1, TimeUnit.MINUTES);
+                logger.info("医生排班缓存更新成功,共{}个医生", listResult);
+            } else {
+                logger.error("添加医生缓存失败");
+            }
+        } catch (Exception e) {
+            logger.error("Redis缓存操作异常", e);
+        }
+
+        return hsDoctorSchedules;
+    }
+
+    /**
+     * 修复常见的JSON格式问题
+     */
+    private String fixJsonString(String json) {
+        if (json == null) return null;
+        // 移除可能的BOM标记和其他非法字符
+        return json.trim()
+                .replaceAll("^\uFEFF", "") // 移除BOM
+                .replaceAll("[\\x00-\\x1F&&[^\\r\\n\\t]]", ""); // 移除控制字符
+    }
+
+
+    @Override
+    public Map redirectToHs(String inquiryOrderId) throws Exception {
+        InquiryOrderHsLog inquiryOrderHsLog = inquiryOrderMapper.selectHsLog(inquiryOrderId);
+        if (ObjectUtil.isEmpty(inquiryOrderHsLog)) {
+            throw new RuntimeException("未找到对应的问诊单");
+        }
+        HashMap<String, Object> data = new HashMap<>();
+        String jsonString = inquiryOrderHsLog.getDecodeJson();
+        if (jsonString != null && !jsonString.trim().isEmpty()) {
+            HashMap<String, Object> map = (HashMap<String, Object>) StringToMapUtil.toMap(jsonString);
+            data = (HashMap<String, Object>) map.get("data");
+//        HashMap hashMap = JSONUtil.toBean(jsonString, HashMap.class);
+//        if (hashMap != null && hashMap.get("data") != null)
+//            data = (HashMap<String, Object>) hashMap.get("data");
+        }
+        try {
+            Map<String, Object> finalMap = MapUtil.newHashMap();
+            Map<String, Object> stringObjectMap1 = MapUtil.newHashMap();
+            //需要创建诊室
+            if (data.get("user_sign_token") != null && data.get("book_no") != null) {
+                //响应存在token和bookno就调用创建诊室接口
+                HashMap<String, String> requestMap = new HashMap<>();
+                requestMap.put("user_sign_token", data.get("user_sign_token").toString());
+                requestMap.put("book_no", data.get("book_no").toString());
+                Map<String, String> stringStringMap = encryptRequest(new JSONObject(requestMap));
+                String s = "";
+                try {
+                    s = sendPostRequest(JSONUtil.toJsonStr(stringStringMap), hsInquiryOrderUrl + hsConsultingRoomUrl,"POST");
+                } catch (Exception e) {
+                    logger.error("请求失败:{}", e.getMessage());
+                    return R.error("向红杉发起诊室获取失败");
+                }
+                stringObjectMap1 = decryptResponse(s);
+                Map<String, Object> map1 = (Map) stringObjectMap1.get("data");
+                if (map1.get("book_no") != null && map1.get("user_sign_token") != null) {
+                    Map extraData = new HashMap<String, String>();
+                    extraData.put("book_no", map1.get("book_no").toString());
+                    extraData.put("user_sign_token", map1.get("user_sign_token").toString());
+                    finalMap.put("extraData", extraData);
+                    /*测试环境tempAppid*/
+                    finalMap.put("appId", "wx4093138269dbaf41");
+                    Map map2 = (Map) data.get("jump_param");
+                    if (map2 != null && map2.get("jump_uri") != null)
+                        finalMap.put("path", map2.get("jump_uri"));
+                    finalMap.put("envVersion", hsEnvVersion);
+                }
+
+            }
+            if (MapUtil.isEmpty(finalMap)) return R.error("向红杉发起咨询问诊失败-诊室信息为空");
+            /*创建诊室的响应解密*/
+            inquiryOrderMapper.updateHsLog(Long.valueOf(inquiryOrderId), JSONUtil.toJsonStr(stringObjectMap1));
+            return R.ok(finalMap);
+
+        } catch (InterruptedException e) {
+            logger.error("问诊申请的过程被中断, 问诊单号: {}", inquiryOrderId, e);
+            return R.error("问诊申请被中断,请稍后重试");
+        } catch (Throwable e) {
+            logger.error("问诊申请过程中发生异常, 问诊单号: {}", inquiryOrderId, e);
+            return R.error("问诊申请过程中发生异常,请稍后重试");
+        }
+    }
+
+    @Override
+    public R cancelInquiryToHs(String inquiryOrderId) {
+        InquiryOrderHsLog inquiryOrderHsLog = inquiryOrderMapper.selectHsLog(inquiryOrderId);
+        if (ObjectUtil.isEmpty(inquiryOrderHsLog)) {
+            return R.error("未找到对应的问诊单");
+        }
+        inquiryOrderHsLog.getBookNo();//第三方接口不通,需要对接组长是否做这个功能
+        return null;
+    }
+
+
+    /**
+     * 获取当前时间所在的时间段(value >= 1)
+     *
+     * @param schedule 时间段映射
+     * @return 当前时间段的key,如果没有找到则返回null
+     */
+    public static String getCurrentValidTimeSlot(Map<String, Integer> schedule) {
+        LocalTime now = LocalTime.now();
+
+        for (Map.Entry<String, Integer> entry : schedule.entrySet()) {
+            String timeSlot = entry.getKey();
+            Integer value = entry.getValue();
+
+            // 只考虑value大于等于1的时间段
+            if (value >= 1) {
+                // 解析时间段,例如"08:00-08:30"
+                String[] times = timeSlot.split("-");
+                if (times.length == 2) {
+                    try {
+                        LocalTime startTime = LocalTime.parse(times[0]);
+                        LocalTime endTime = LocalTime.parse(times[1]);
+                        // 判断当前时间是否在这个时间段内
+                        if (!now.isBefore(startTime) && now.isBefore(endTime)) {
+                            return timeSlot;
+                        }
+                    } catch (Exception e) {
+                        // 忽略解析错误的时间格式
+                        continue;
+                    }
+                }
+            }
+        }
+
+        return null; // 没有找到合适的时间段
+    }
+
+
+//    @Override
+//    public void updateFsInquiryPatientInfoFromHsRedirect(HttpServletRequest param) throws JsonProcessingException {
+//解析请求
+// 1. 读取请求体内容
+//        StringBuilder requestBody = new StringBuilder();
+//        try (BufferedReader reader = param.getReader()) {
+//            String line;
+//            while ((line = reader.readLine()) != null) {
+//                requestBody.append(line);
+//            }
+//        } catch (IOException e) {
+//            throw new RuntimeException(e);
+//        }
+// 2. 解析JSON
+//        Map<String, Object> jsonMap = new ObjectMapper().readValue(
+//                requestBody.toString(),Map.class
+//        );
+
+//发送失败存数据库
+//        fsInquiryPatientInfoMapper.updateFsInquiryPatientInfoFromHsRedirect();
+//失败消息发给用户
+
+//todo 暂时不用处理,用户前端可以直接看到问诊失败
+//    }
+
+//    @Override
+//    public void recieveInquiryFromHsDispensary(HttpServletRequest param) throws JsonProcessingException {
+////         1. 读取请求体内容
+//        StringBuilder requestBody = new StringBuilder();
+//        try (BufferedReader reader = param.getReader()) {
+//            String line;
+//            while ((line = reader.readLine()) != null) {
+//                requestBody.append(line);
+//            }
+//        } catch (IOException e) {
+//            throw new RuntimeException(e);
+//        }
+////         2. 解析JSON
+//        Map<String, Object> jsonMap = new ObjectMapper().readValue(
+//                requestBody.toString(),Map.class
+//        );
+//
+//
+//    }
+
+    /**
+     * 银川互联网医院要求,都用post,请求体加“_method”标识请求方法
+     * @param request 请求体
+     * @param urlWithParams 跳转的url
+     * @param method 跳转的方法
+     * @return
+     * @throws IOException
+     * @throws ParseException
+     */
+
+    private String sendPostRequest(String request, String urlWithParams,String method) throws IOException, ParseException {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            // 构建URL
+            HttpPost httpPost = new HttpPost(urlWithParams);
+
+            // 设置请求头
+            httpPost.setHeader("appid", appId);
+            httpPost.setHeader("Accept", "application/json");
+            httpPost.setHeader("Content-Type", "application/json");
+            httpPost.setHeader("User-Agent", "Mozilla/5.0");
+
+            // 设置请求体
+            JSONObject jsonObject = JSONUtil.parseObj(request);
+            // 添加_method参数
+            jsonObject.put("_method", method);
+            // 重新生成请求体字符串
+            String requestBody = JSONUtil.toJsonStr(jsonObject);
+            StringEntity entity = new StringEntity(requestBody, StandardCharsets.UTF_8);
+
+            httpPost.setEntity(entity);
+
+            // 执行请求
+            CloseableHttpResponse response = httpClient.execute(httpPost);
+            int statusCode = response.getCode();
+            System.out.println("响应码: " + statusCode);
+            String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+            return responseBody;
+
+
+        }
+    }
+
+    private Map<String, String> encryptRequest(JSONObject hsParam) throws Exception {
+        Map<String, String> encrypt = HsCryptoUtil.encrypt(hsParam, appSecret, platformPublicKey);
+        return encrypt;
+    }
+
+    private Map<String, Object> decryptResponse(String response) throws Exception {
+        JsonNode jsonNode = new ObjectMapper().readTree(response);
+        JsonNode data = jsonNode.get("data");
+        if (data == null) {
+            throw new RuntimeException("未收到响应的结果");
+        }
+        Map<String, Object> decrypt = HsCryptoUtil.decrypt(data, appSecret, thirdPrivateKey);
+
+        return decrypt;
+    }
+
+    private List<HashMap<String, String>> getHistroyAllergic(String historyAllergic) {
+        /*过敏史*/
+        List<HashMap<String, String>> histroyAllergic = new ArrayList<>();
+        if (!historyAllergic.equals("") && historyAllergic != null) {
+            String[] split = historyAllergic.split(",");
+            Map<String, String> allergy = new HashMap<String, String>() {{
+                put("无", "1");
+                put("青霉素", "2");
+                put("磺胺", "3");
+                put("链霉素", "4");
+            }};
+            for (String s : split
+            ) {
+                s = s.trim();
+                HashMap<String, String> stringStringHashMap = new HashMap<>();
+                stringStringHashMap.put("code", allergy.get(s) != null ? allergy.get(s) : "99");
+                stringStringHashMap.put("name", s);
+                histroyAllergic.add(stringStringHashMap);
+            }
+        } else {
+            histroyAllergic.add(new HashMap<String, String>() {{
+                put("1", "无");
+            }});
+        }
+        return histroyAllergic;
+    }
+
+    private List<HashMap<String, String>> getIllnessHistroy(String selfMedHistory) {
+        /*疾病(现病)史*/
+        List<HashMap<String, String>> illnessHistroy = new ArrayList<>();
+        if (!selfMedHistory.equals("") && selfMedHistory != null) {
+            String[] split = selfMedHistory.split(",");
+            Map<String, String> illnessMap = new HashMap<String, String>() {{
+                put("无", "1");
+                put("骨质疏松", "16");
+                put("心肌病", "17");
+                put("脉管炎", "18");
+                put("肺结核", "19");
+                put("消化道出血", "20");
+                put("胃溃疡", "21");
+                put("肾功能不全", "22");
+                put("泌尿系结石", "23");
+                put("前列腺炎", "24");
+                put("甲状腺病(甲亢、甲减、甲状腺炎、结节)", "25");
+                put("痛风", "26");
+                put("关节炎", "27");
+                put("红斑狼疮", "28");
+                put("贫血", "29");
+                put("脑出血", "30");
+                put("脑梗塞", "31");
+                put("帕金森氏综合症", "32");
+                put("重症肌无力", "33");
+                put("癫痫", "34");
+                put("结肠炎", "35");
+                put("胆囊炎", "36");
+                put("胆结石", "37");
+                put("肝硬化", "38");
+                put("肾炎", "39");
+                put("美尼尔氏综合征", "40");
+                put("老年性痴呆", "41");
+                put("抑郁症", "42");
+                put("焦虑症", "43");
+                put("肿瘤", "44");
+                put("白内障", "45");
+                put("青光眼", "46");
+                put("皮肤病", "47");
+                put("慢性支气管炎", "48");
+                put("肺气肿", "49");
+                put("肺源性心脏病", "50");
+                put("支气管哮喘、扩张", "51");
+                put("气胸", "52");
+                put("慢性心衰", "53");
+                put("心律失常", "54");
+                put("心脏瓣膜病", "55");
+                put("认知障碍", "56");
+                put("其他", "99");
+            }};
+
+            for (String s : split
+            ) {
+                s = s.trim();
+                HashMap<String, String> stringStringHashMap = new HashMap<>();
+                stringStringHashMap.put("code", illnessMap.get(s) != null ? illnessMap.get(s) : "99");
+                stringStringHashMap.put("name", s);
+                illnessHistroy.add(stringStringHashMap);
+            }
+        } else {
+            illnessHistroy.add(new HashMap<String, String>() {{
+                put("1", "无");
+            }});
+        }
+        return illnessHistroy;
+    }
+
+    private SendInquiryToHSParam getHsParam(String userId, FsInquiryOrder fsInquiryOrder, FsPatient fsPatient, String appId, Integer inquiryType, Integer doctor, String visitRange, Integer branch) {
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(Long.valueOf(userId));
+        //没有appid,暂时随便填一个
+        if (appId == null) appId = "wx4115995705bbtest";
+        List<HashMap<String, String>> histroyAllergic = getHistroyAllergic(fsPatient.getHistoryAllergic());
+        List<HashMap<String, String>> illnessHistroy = getIllnessHistroy(fsPatient.getSelfMedHistory());
+        List<HashMap<String, String>> familyHistory = getIllnessHistroy(fsPatient.getFamilyMedHistory());
+        /*测试暂时取了个测试环境的医生信息,正式环境需要高频调医生排班接口*/
+        HsBookDoctorInfoParam hsBookDoctorInfoParam = new HsBookDoctorInfoParam();
+        if (doctor == null) {
+            hsBookDoctorInfoParam
+                    .setDoctorId(16405)
+                    .setVisitDate("2025-12-22")
+                    .setVisitRange("16:00-16:30")
+                    .setBranch(1377);
+        } else {
+            hsBookDoctorInfoParam.setDoctorId(doctor)
+                    .setVisitDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
+                    .setVisitRange(visitRange)
+                    .setBranch(branch);
+        }
+
+
+        /*跳转信息*/
+        HsRedirectParam hsRedirectParam = new HsRedirectParam();
+        hsRedirectParam.setFromType(1).setFromTag("1").setBackType(1).setBackTag("1").setJumpType(1).setBackUri(localUrl);
+
+        SendInquiryToHSParam sendInquiryToHSParam = new SendInquiryToHSParam()
+                .setPrescriptionType(1)//todo 需要咨询处方分类这个业务
+                .setUnionCode(String.valueOf(fsInquiryOrder.getOrderId()))
+                .setRegisterType(ObjectUtil.isNotNull(fsInquiryOrder) ? fsInquiryOrder.getOrderType() == 1 ? "picture" : fsInquiryOrder.getOrderType() == 2 ? "video" : "" : "")
+                .setUserInfo(new HsUserInfoParam()
+                        //微信小程序
+                        .setAccType("11")
+                        .setUserMp(fsUser.getPhone().length() > 11 ? PhoneUtil.decryptPhone(fsUser.getPhone()) : fsUser.getPhone())
+                        .setAccOpenid(fsUser.getMaOpenId())
+                        .setAccAppid(appId)
+                )
+                .setUserMember(new HsUserMember().setRealName(fsPatient.getPatientName())
+                        .setMp(fsPatient.getMobile().length() > 11 ? PhoneUtil.decryptPhone(fsPatient.getMobile()) : fsPatient.getMobile())
+                        .setCertCode(fsPatient.getIdCard()).setAge(fsPatient.getAge()).setAgeUnit("1")
+                        .setRelation("0").setSex(fsPatient.getSex().toString()))
+                .setHealthyCondition(new HsHealthyCondition()
+                        .setLiverKidneyNormal(fsPatient.getLiverUnusual().equals("正常") && fsPatient.getRenalUnusual().equals("正常") ? 1 : 0)
+                        .setAllergy(histroyAllergic)
+                        .setIllnessHistory(illnessHistroy)
+                        .setFamilyHistory(familyHistory)
+                        .setPregnancy(fsPatient.getSex() == 1 ? 9 : 0))
+//                .setDrug(Arrays.asList())//todo 购药问诊需要
+                .setBookDescription(fsInquiryOrder.getTitle())
+                .setSubsequentFile(Arrays.asList())
+                .setNotifyUri(localUrl + "")//todo 处方开具后流转通知
+                .setRedirectUri(localUrl + "/recieveInquiryFromHsRedirect")//todo 鉴权失败或进入诊室失败返回地址
+                .setDispensaryUri(localUrl + "")//todo 去取药的跳转地址,一般提供三方小程序购药订单或支付处方
+                .setCallbackNotification(localUrl + "/receiveInquiryFromHsCallbackNotification")//todo 问诊生命周期回调通知
+                .setBookDoctorInfo(hsBookDoctorInfoParam)
+                .setRedirectParam(hsRedirectParam);
+//        if (inquiryType==2){
+//            sendInquiryToHSParam.setDrug(Arrays.asList(new HsDrug()));
+//        } //todo 暂时未知我们系统的药品表
+
+        return sendInquiryToHSParam;
+    }
+
+
 }

+ 27 - 0
fs-service/src/main/java/com/fs/hisStore/domain/HsPrescribScrm.java

@@ -0,0 +1,27 @@
+package com.fs.hisStore.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉-完整医疗信息")
+@Accessors(chain = true)
+public class HsPrescribScrm {
+
+    @ApiModelProperty(value = "问诊信息")
+    @JsonProperty("medical_record")
+    private MedicalRecord medicalRecord;
+
+    @ApiModelProperty(value = "处方信息")
+    @JsonProperty("prescription_info")
+    private PrescriptionInfo prescriptionInfo;
+
+    @ApiModelProperty(value = "处方流转信息列表")
+    @JsonProperty("prescription_delivery")
+    private List<PrescriptionDeliveryItem> prescriptionDelivery;
+}
+

+ 79 - 0
fs-service/src/main/java/com/fs/hisStore/domain/MedicalRecord.java

@@ -0,0 +1,79 @@
+package com.fs.hisStore.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉-医院问诊信息")
+@Accessors(chain = true)
+public class MedicalRecord {
+
+    @ApiModelProperty(value = "医院名")
+    @JsonProperty("hospital_name")
+    private String hospitalName;
+
+    @ApiModelProperty(value = "科室名")
+    @JsonProperty("branch_title")
+    private String branchTitle;
+
+    @ApiModelProperty(value = "院内问诊标识")
+    @JsonProperty("book_no")
+    private String bookNo;
+
+    @ApiModelProperty(value = "患者姓名")
+    @JsonProperty("real_name")
+    private String realName;
+
+    @ApiModelProperty(value = "患者性别 1=男 2=女 0=未知")
+    @JsonProperty("sex")
+    private Integer sex;
+
+    @ApiModelProperty(value = "患者年龄")
+    @JsonProperty("age")
+    private Integer age;
+
+    @ApiModelProperty(value = "年龄单位 1-岁;2-月;3-天")
+    @JsonProperty("age_unit")
+    private Integer ageUnit;
+
+    @ApiModelProperty(value = "主诉")
+    @JsonProperty("recount")
+    private String recount;
+
+    @ApiModelProperty(value = "肝肾健康信息")
+    @JsonProperty("liver_kidney_normal")
+    private String liverKidneyNormal;
+
+    @ApiModelProperty(value = "妊娠哺乳信息")
+    @JsonProperty("pregnancy")
+    private String pregnancy;
+
+    @ApiModelProperty(value = "过敏史信息")
+    @JsonProperty("allergyhistory")
+    private String allergyhistory;
+
+    @ApiModelProperty(value = "家族病史信息")
+    @JsonProperty("family_history")
+    private String familyHistory;
+
+    @ApiModelProperty(value = "疾病史信息")
+    @JsonProperty("nowmedical")
+    private String nowmedical;
+
+    @ApiModelProperty(value = "诊断描述")
+    @JsonProperty("diagnose")
+    private List<String> diagnose;
+
+    @ApiModelProperty(value = "医保诊断编码信息")
+    @JsonProperty("diagnose_with_med_code")
+    private List<String> diagnoseWithMedCode;
+
+    @ApiModelProperty(value = "购药信息")
+    @JsonProperty("drug")
+    private List<String> drug;
+}
+

+ 46 - 0
fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionDeliveryItem.java

@@ -0,0 +1,46 @@
+package com.fs.hisStore.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉-处方流转信息")
+@Accessors(chain = true)
+public class PrescriptionDeliveryItem {
+
+    @ApiModelProperty(value = "处理场景标识 00-电子处方;10-流转三方;20-药房配药;30-核销出库;40-患者收药")
+    @JsonProperty("scence")
+    private String scence;
+
+    @ApiModelProperty(value = "处理人角色标识 1-开方医生;2-审方药师;3-复核药师;4-出药操作员;5-流转操作员")
+    @JsonProperty("role_tag")
+    private String roleTag;
+
+    @ApiModelProperty(value = "处方状态")
+    @JsonProperty("status")
+    private String status;
+
+    @ApiModelProperty(value = "操作时间")
+    @JsonProperty("execute_time")
+    private Integer executeTime;
+
+    @ApiModelProperty(value = "处理建议")
+    @JsonProperty("result")
+    private String result;
+
+    @ApiModelProperty(value = "操作人姓名")
+    @JsonProperty("real_name")
+    private String realName;
+
+    @ApiModelProperty(value = "操作人科室")
+    @JsonProperty("branch_name")
+    private String branchName;
+
+    @ApiModelProperty(value = "其他信息")
+    @JsonProperty("extend")
+    private List<Object> extend;
+}

+ 66 - 0
fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionInfo.java

@@ -0,0 +1,66 @@
+package com.fs.hisStore.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.util.List;
+
+@Data
+@ApiModel(description = "红杉-处方信息")
+@Accessors(chain = true)
+public class PrescriptionInfo {
+
+    @ApiModelProperty(value = "开单医生")
+    @JsonProperty("billing_doctor_name")
+    private String billingDoctorName;
+
+    @ApiModelProperty(value = "审核医生|药师")
+    @JsonProperty("reviewer_admin_name")
+    private String reviewerAdminName;
+
+    @ApiModelProperty(value = "复核医生|药师")
+    @JsonProperty("audit_admin_name")
+    private String auditAdminName;
+
+    @ApiModelProperty(value = "处方订单号")
+    @JsonProperty("order_no")
+    private String orderNo;
+
+    @ApiModelProperty(value = "处方状态")
+    @JsonProperty("prescription_status")
+    private String prescriptionStatus;
+
+    @ApiModelProperty(value = "处方流转状态名")
+    @JsonProperty("prescription_status_name")
+    private String prescriptionStatusName;
+
+    @ApiModelProperty(value = "电子处方地址")
+    @JsonProperty("prescription_url")
+    private String prescriptionUrl;
+
+    @ApiModelProperty(value = "下单时间")
+    @JsonProperty("create_time")
+    private Integer createTime;
+
+    @ApiModelProperty(value = "下单时间")
+    @JsonProperty("create_time_str")
+    private String createTimeStr;
+
+    @ApiModelProperty(value = "三方订单号")
+    @JsonProperty("third_order_no")
+    private String thirdOrderNo;
+
+    @ApiModelProperty(value = "处方来源类型 00-普通门诊;01-门诊慢病;02-特种病;03-门诊统筹")
+    @JsonProperty("prescription_from_type")
+    private String prescriptionFromType;
+
+    @ApiModelProperty(value = "处方去向 00-未分配;01-院内药房;10-院外药房;20-医保中心;30-配送中心;40-三方电商")
+    @JsonProperty("prescription_flow")
+    private String prescriptionFlow;
+
+    @ApiModelProperty(value = "药品详情")
+    @JsonProperty("details")
+    private List<PrescriptionInfoDetails> details;
+}

+ 63 - 0
fs-service/src/main/java/com/fs/hisStore/domain/PrescriptionInfoDetails.java

@@ -0,0 +1,63 @@
+package com.fs.hisStore.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import java.util.List;
+
+@Data
+@ApiModel(description = "处方药品详情")
+@Accessors(chain = true)
+public class PrescriptionInfoDetails {
+
+    @ApiModelProperty(value = "处方号")
+    @JsonProperty("pre_code")
+    private String preCode;
+
+    @ApiModelProperty(value = "药品id")
+    @JsonProperty("id")
+    private Integer id;
+
+    @ApiModelProperty(value = "药品名称")
+    @JsonProperty("name")
+    private String name;
+
+    @ApiModelProperty(value = "规格")
+    @JsonProperty("format")
+    private String format;
+
+    @ApiModelProperty(value = "用药频次")
+    @JsonProperty("frequency")
+    private String frequency;
+
+    @ApiModelProperty(value = "用法")
+    @JsonProperty("usage")
+    private String usage;
+
+    @ApiModelProperty(value = "总量")
+    @JsonProperty("total")
+    private Integer total;
+
+    @ApiModelProperty(value = "单位")
+    @JsonProperty("unit")
+    private String unit;
+
+    @ApiModelProperty(value = "单次剂量(每日剂量)")
+    @JsonProperty("dose")
+    private String dose;
+
+    @ApiModelProperty(value = "用药要求")
+    @JsonProperty("requirements")
+    private String requirements;
+
+    @ApiModelProperty(value = "中草药处方详情")
+    @JsonProperty("log")
+    private List<String> log;
+
+    @ApiModelProperty(value = "药品条码")
+    @JsonProperty("bar_code")
+    private String barCode;
+}
+

+ 5 - 1
fs-service/src/main/java/com/fs/hisStore/enums/SysConfigEnum.java

@@ -85,7 +85,11 @@ public enum SysConfigEnum {
     //红包流量,joinTime.switch.config
     JOIN_TIME_SWITCH_CONFIG("joinTime.switch.config", "红包流量"),
     //签到配置,store.sign
-    SIGN_CONFIG("store.sign", "签到配置");
+    SIGN_CONFIG("store.sign", "签到配置"),
+    //商城支付配置,store.pay
+    STORE_PAY_CONFIG("store.pay", "商城支付配置"),
+    //声纹复刻配置,vc.config
+    VS_CONFIG("vc.config", "声纹复刻配置");
     private final String key;
     private final String name;
 

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsPrescribeParam.java

@@ -3,11 +3,13 @@ package com.fs.hisStore.param;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
+import lombok.experimental.Accessors;
 
 import java.io.Serializable;
 import java.util.Date;
 
 @Data
+@Accessors(chain = true)
 public class FsPrescribeParam implements Serializable
 {
     private Long orderId;

+ 10 - 7
fs-service/src/main/java/com/fs/hisStore/service/IFsPrescribeScrmService.java

@@ -2,6 +2,7 @@ package com.fs.hisStore.service;
 
 import java.util.List;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.common.core.domain.R;
 import com.fs.hisStore.domain.FsPrescribeScrm;
 import com.fs.hisStore.param.FsPrescribeParam;
@@ -11,7 +12,7 @@ import com.fs.hisStore.vo.FsPrescribeVO;
 
 /**
  * 处方Service接口
- * 
+ *
  * @author fs
  * @date 2022-03-15
  */
@@ -19,7 +20,7 @@ public interface IFsPrescribeScrmService
 {
     /**
      * 查询处方
-     * 
+     *
      * @param prescribeId 处方ID
      * @return 处方
      */
@@ -27,7 +28,7 @@ public interface IFsPrescribeScrmService
 
     /**
      * 查询处方列表
-     * 
+     *
      * @param fsPrescribe 处方
      * @return 处方集合
      */
@@ -35,7 +36,7 @@ public interface IFsPrescribeScrmService
 
     /**
      * 新增处方
-     * 
+     *
      * @param fsPrescribe 处方
      * @return 结果
      */
@@ -43,7 +44,7 @@ public interface IFsPrescribeScrmService
 
     /**
      * 修改处方
-     * 
+     *
      * @param fsPrescribe 处方
      * @return 结果
      */
@@ -51,7 +52,7 @@ public interface IFsPrescribeScrmService
 
     /**
      * 批量删除处方
-     * 
+     *
      * @param prescribeIds 需要删除的处方ID
      * @return 结果
      */
@@ -59,7 +60,7 @@ public interface IFsPrescribeScrmService
 
     /**
      * 删除处方信息
-     * 
+     *
      * @param prescribeId 处方ID
      * @return 结果
      */
@@ -74,4 +75,6 @@ public interface IFsPrescribeScrmService
     List<FsPrescribeVO> selectFsPrescribeListVO(FsPrescribeVOParam fsPrescribe);
 
     FsPrescribeScrm selectFsPrescribeByOrderId(Long id);
+
+    R getPrescribeFromHs(String userId,String inquiryOrderId) throws JsonProcessingException;
 }

+ 117 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsPrescribeScrmServiceImpl.java

@@ -1,16 +1,29 @@
 package com.fs.hisStore.service.impl;
 
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.OrderUtils;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.HsCryptoUtil;
 import com.fs.common.utils.StringUtils;
+import com.fs.course.param.InquiryOrderHsLog;
+import com.fs.course.vo.FsInquiryPatientInfoListUVO;
+import com.fs.his.domain.FsPatient;
+import com.fs.his.mapper.FsInquiryOrderMapper;
+import com.fs.his.mapper.FsInquiryOrderReportMapper;
+import com.fs.his.mapper.FsInquiryPatientInfoMapper;
+import com.fs.his.mapper.FsPatientMapper;
 import com.fs.hisStore.bean.Drug;
 import com.fs.hisStore.bean.Prescribe;
 import com.fs.hisStore.domain.*;
@@ -20,7 +33,16 @@ import com.fs.hisStore.param.FsPrescribeVOParam;
 import com.fs.hisStore.param.PrescribeParam;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.FsPrescribeVO;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import com.fs.hisStore.mapper.FsPrescribeScrmMapper;
 
@@ -49,6 +71,27 @@ public class FsPrescribeScrmServiceImpl implements IFsPrescribeScrmService
     IFsStoreOrderItemScrmService orderItemService;
     @Autowired
     IFsStoreProductScrmService productService;
+    @Autowired
+    private FsInquiryPatientInfoMapper fsInquiryPatientInfoMapper;
+    @Autowired
+    private FsPatientMapper fsPatientMapper;
+    @Autowired
+    private FsInquiryOrderMapper inquiryOrderMapper;
+
+    private final String appId = "TpmBATELpdeMbjtjdaVIGbznlJDRjLDw";
+    private final String hsInquiryOrderPrescriptionUrl = "https://dev.om.yfttech.cn/open_platform/hospital/suppliers/v1/book_order/prescription";
+    //应用秘钥
+    private final String appSecret = "ExbfudweIeSCFLtp";
+    // 三方平台RSA私钥(解密响应密钥用)
+    private final String thirdPrivateKey = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUNkd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQW1Fd2dnSmRBZ0VBQW9HQkFMWmxsblI4SE5Qb0N2WFkKVFBCTmtLdFArejc0NVA4ZzlzZUVHVDZUbysxdmp4Sk9jc2VRNWpFNnR4Rm5IQlJsc3psam5sUHdYT25HYStFLwo3Q2phbElIZjdTZ2p0SzlKSCtpckd5eW1yaVN6cEYyMFpKT2wxQXhsams2UUVFdGRRS0VqYWI2NjFGVHN2clRDCjhaRzAzS05NOVVjM2F6SGY2bVRRUXJtRWp1WjlBZ01CQUFFQ2dZRUF0Zjc5dG5OVkRIaWYzeGthQkRsUkhpOHIKWW5WVmdlRHhmTGtwdTAvMEpPbkkxNXBoV3hJUkxwUUlzUnV5WUFQdVpsZ3BWbFlqVDd5R1RuYksvU1RGUW5YVwpGS05pUEtjQ1pXdXhBdkNhRFFCZ0ZHdXZpMjB6bjBBZ2hEU0tycEIrYU9NL1VGdjhFZWx0cE42WDA0Vis1RlY2CitwWGFsdkZjUlhUWEVHMXBTUUVDUVFEbnVFT2VmcUxKb1pDK3lwMlovMTFFRm0yaWxOL2VKOFBpdzRFUm5WdTkKRU5TWm9OdURIUjFSeVFlYWVzeXU3WlFadzV2d0VvZkFlL1MzV2ZhVlFBM0JBa0VBeVlKRVJEcmdLSHF3YzYyOQpkLys4RGNVOEtZei9hQ3ZqVm9kUGRmMVMwbXZpdUlUS2txYVZhdDFCTis2MjZMcFU2c1BsNVVHeWZ5aXlVc3FTCnpmSi92UUpBRDhGUGw2ODBrbEVSN21jSVlEZ2t0MFJ2SCtiUGNlTnlSakRVemNYTlB3V3Q3dVFwQ0xrcURTMkYKL3RMcXA5b3ZmN0QxSVZXaE5VMDRUbDhuak81V0FRSkJBS01wS0M5NjRJL0dMK09xbFJSNTdJSFY1dzNaemVCQwpVUlI2QVZ3UEh5V2tGM0xDaXVmTm5JUm4zR3YyalFIS0JnSUZWcnVYdzNqMHNkY1prVjdTY0owQ1FGVGJJTnRCCi9OcW5laWI3U3d4ZCt5NHZ4UkRHZVRxZUFjNXE5ZU5RNy8vbUFOWlZyUTZKSEJnZ1NjT3h6ZFhFZFZ5UTk5Z1AKYmQ3c2Z2Mlk2WGdhd3FFPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg";
+    @Autowired
+    private FsInquiryOrderReportMapper fsInquiryOrderReportMapper;
+    /**
+     * 向红杉发起问诊申请后返回的bookNo
+     */
+    public static final String CACHE_BOOK_NO_HS = "cache:hs:book:no:%d";
+    @Qualifier("redisTemplate")
+
 
     /**
      * 查询处方
@@ -242,4 +285,78 @@ public class FsPrescribeScrmServiceImpl implements IFsPrescribeScrmService
     public FsPrescribeScrm selectFsPrescribeByOrderId(Long id) {
         return fsPrescribeMapper.selectFsPrescribeByOrderId(id);
     }
+
+    //从红杉获取处方
+    @Override
+    public R getPrescribeFromHs(String userId,String inquiryOrderId) throws JsonProcessingException {
+        FsInquiryPatientInfoListUVO info = fsInquiryPatientInfoMapper.getInfo(Long.valueOf(inquiryOrderId));
+        FsPatient fsPatient = fsPatientMapper.selectFsPatientByPatientId(info.getPatientId());
+        InquiryOrderHsLog inquiryOrderHsLog = inquiryOrderMapper.selectHsLog(inquiryOrderId);
+
+        String s = sendPostRequest(inquiryOrderHsLog.getBookNo());
+        JsonNode jsonNode = new ObjectMapper().readTree(s);
+        JsonNode data = jsonNode.get("data");
+        if (data == null){
+            throw new RuntimeException("未收到响应的结果");
+        }
+        Map<String, Object> decrypt = HsCryptoUtil.decrypt( data, appSecret, thirdPrivateKey);
+        ObjectMapper objectMapper = new ObjectMapper();
+        String jsonString = objectMapper.writeValueAsString(decrypt);
+        HsPrescribScrm hsPrescribScrm = objectMapper.readValue(jsonString, HsPrescribScrm.class);
+        //拼参数新建处方
+        FsPrescribeParam prescribeParam=new FsPrescribeParam();
+        prescribeParam
+//                .setOrderId(Long.parseLong())//todo 逻辑更改弃用
+                .setPrescribeType(1).setInquiryOrderId(Long.valueOf(inquiryOrderId))
+                .setUserId(Long.parseLong(userId)).setPatientId(info.getPatientId())
+                .setPrescribeCode(hsPrescribScrm.getPrescriptionInfo().getOrderNo())
+                .setPatientDescs(hsPrescribScrm.getMedicalRecord().getRecount())
+                .setPatientAge(String.valueOf(hsPrescribScrm.getMedicalRecord().getAge()))
+                .setPatientName(hsPrescribScrm.getMedicalRecord().getRealName())
+                .setWeight(String.valueOf(fsPatient.getWeight()))
+                .setIsAllergic(hsPrescribScrm.getMedicalRecord().getAllergyhistory()!=null)
+//                .setIsHistoryAllergic(hsPrescribScrm.getMedicalRecord().getAllergyhistory()==null?"是":"否")
+                .setHistoryAllergic(hsPrescribScrm.getMedicalRecord().getAllergyhistory())
+                .setIsLiver(true).setIsRenal(true)
+//                .setLiverUnusual("是").setRenalUnusual("是")
+                .setIsLactation("否")//无法准确提取红杉响应的情况
+                .setPatientTel(fsPatient.getMobile()).setPatientGender(String.valueOf(hsPrescribScrm.getMedicalRecord().getSex()))
+                .setDiagnose(hsPrescribScrm.getMedicalRecord().getDiagnose().toString())
+                .setDoctorName(hsPrescribScrm.getPrescriptionInfo().getBillingDoctorName());
+        doPrescribe(Long.parseLong(userId),prescribeParam);
+
+        //todo 未完成,售后,处方逻辑未清楚,需对接组长再细了解
+        return null;
+    }
+    private String sendPostRequest(String request) {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            // 构建URL
+            String urlWithParams = hsInquiryOrderPrescriptionUrl;
+            HttpPost httpPost = new HttpPost(urlWithParams);
+
+            // 设置请求头
+            httpPost.setHeader("appid", appId);
+            httpPost.setHeader("Accept", "application/json");
+            httpPost.setHeader("Content-Type", "application/json");
+            httpPost.setHeader("User-Agent", "Mozilla/5.0");
+            httpPost.setHeader("_method","GET");//按红杉要求,要传方法
+
+            // 设置请求体
+            String requestBody = request;
+            StringEntity entity = new StringEntity(requestBody, StandardCharsets.UTF_8);
+            httpPost.setEntity(entity);
+
+            // 执行请求
+            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+                int statusCode = response.getCode();
+                System.out.println("响应码: " + statusCode);
+                String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+                return responseBody;
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "向红杉发起请求失败: " + e.getMessage();
+        }
+    }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/wxwork/dto/WxwSilkVoceDTO.java

@@ -9,7 +9,7 @@ public class WxwSilkVoceDTO {
     private Data data;
 
     @lombok.Data
-    public class Data {
+    public static class Data {
         private int duration;
         private String url;
     }

+ 2 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java

@@ -238,4 +238,6 @@ public interface WxWorkService {
      * @return  WxWorkResponseDTO
      */
     WxWorkResponseDTO<WxCdnUploadImgLinkResp> cdnUploadImgLink(WxCdnUploadImgLinkDTO param, Long serverId);
+
+    WxwSilkVoceDTO getSilkVoiceDoubao(String content, Long companyUserId);
 }

+ 122 - 64
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java

@@ -1,10 +1,16 @@
 package com.fs.wxwork.service;
 
+import cn.hutool.core.bean.BeanUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.service.impl.TtsServiceImpl;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.param.VcCompanyUser;
 import com.fs.config.ai.AiHostProper;
+import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.qw.domain.QwIpadServer;
 import com.fs.qw.service.IQwIpadServerService;
 import com.fs.wxwork.dto.*;
@@ -31,6 +37,11 @@ public class WxWorkServiceImpl implements WxWorkService {
     IQwIpadServerService qwIpadServerService;
     @Autowired
     AiHostProper aiHostProper;
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+    @Autowired
+    private TtsServiceImpl ttsServiceImpl;
+
     public String getUrl(Long serverId) {
         String url = redisCache.getCacheObject("serverId:" + serverId);
         if (url != null && !url.isEmpty()) {
@@ -38,107 +49,108 @@ public class WxWorkServiceImpl implements WxWorkService {
         }
         System.out.println("serverId:" + serverId);
         QwIpadServer qwIpadServer = qwIpadServerService.selectQwIpadServerById(serverId);
-        if (qwIpadServer == null||qwIpadServer.getUrl()==null) {
+        if (qwIpadServer == null || qwIpadServer.getUrl() == null) {
             throw new CustomException("未获取到服务地址与端口");
         }
-        redisCache.setCacheObject("serverId:" + serverId,qwIpadServer.getUrl(),2, TimeUnit.HOURS);
+        redisCache.setCacheObject("serverId:" + serverId, qwIpadServer.getUrl(), 2, TimeUnit.HOURS);
         return qwIpadServer.getUrl();
     }
 
 
     private static final String BASE_URL = "http://36.138.18.118:8083/wxwork";
+
     @Override
-    public WxWorkResponseDTO<WxWorkInitRespDTO> init(WxWorkInitDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxWorkInitRespDTO> init(WxWorkInitDTO param, Long serverId) {
         String url = getUrl(serverId) + "/init";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkInitRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO SetCallbackUrl(WxWorkSetCallbackUrlDTO param,Long serverId) {
+    public WxWorkResponseDTO SetCallbackUrl(WxWorkSetCallbackUrlDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SetCallbackUrl";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> SendTextMsg(WxWorkSendTextMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> SendTextMsg(WxWorkSendTextMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendTextMsg";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkSendTextMsgRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkSendAppMsgRespDTO SendAppMsg(WxWorkSendAppMsgDTO param,Long serverId) {
+    public WxWorkSendAppMsgRespDTO SendAppMsg(WxWorkSendAppMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendAppMsg";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkSendAppMsgRespDTO>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendLinkMsgRespDTO> SendLinkMsg(WxwSendLinkMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendLinkMsgRespDTO> SendLinkMsg(WxwSendLinkMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendLinkMsg";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendLinkMsgRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxWorkGetQrCodeRespDTO> getQrCode(WxWorkGetQrCodeDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxWorkGetQrCodeRespDTO> getQrCode(WxWorkGetQrCodeDTO param, Long serverId) {
         String url = getUrl(serverId) + "/getQrCode";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkGetQrCodeRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<List<WxwGetExternalContactRespDTO>> getExternalContacts(WxwGetExternalContactsDTO param,Long serverId) {
+    public WxWorkResponseDTO<List<WxwGetExternalContactRespDTO>> getExternalContacts(WxwGetExternalContactsDTO param, Long serverId) {
         String url = getUrl(serverId) + "/GetExternalContacts";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<List<WxwGetExternalContactRespDTO>>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO CheckCode(WxWorkCheckCodeDTO param,Long serverId) {
+    public WxWorkResponseDTO CheckCode(WxWorkCheckCodeDTO param, Long serverId) {
         String url = getUrl(serverId) + "/CheckCode";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSecondaryValidationRespDTO> SecondaryValidation(WxWorkGetQrCodeDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSecondaryValidationRespDTO> SecondaryValidation(WxWorkGetQrCodeDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SecondaryValidation";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSecondaryValidationRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO automaticLogin(WxWorkGetQrCodeDTO param,Long serverId) {
+    public WxWorkResponseDTO automaticLogin(WxWorkGetQrCodeDTO param, Long serverId) {
         String url = getUrl(serverId) + "/automaticLogin";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwLoginOutRespDTO> LoginOut(WxWorkGetQrCodeDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwLoginOutRespDTO> LoginOut(WxWorkGetQrCodeDTO param, Long serverId) {
         String url = getUrl(serverId) + "/LoginOut";
         try {
             return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwLoginOutRespDTO>>() {
             });
-        }catch (Exception e){
-            log.error("LoginOut error",e);
+        } catch (Exception e) {
+            log.error("LoginOut error", e);
             return null;
         }
 
     }
 
     @Override
-    public WxWorkResponseDTO<WxwMarkAsReadRespDTO> MarkAsRead(WxwMarkAsReadDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwMarkAsReadRespDTO> MarkAsRead(WxwMarkAsReadDTO param, Long serverId) {
         String url = getUrl(serverId) + "/MarkAsRead";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwMarkAsReadRespDTO>>() {
         });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendGroupsMsgRespDTO> SendGroupsMsg(WxwSendGroupsMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendGroupsMsgRespDTO> SendGroupsMsg(WxwSendGroupsMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendGroupsMsg";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendGroupsMsgRespDTO>>() {
         });
@@ -152,82 +164,94 @@ public class WxWorkServiceImpl implements WxWorkService {
     }
 
     @Override
-    public WxWorkResponseDTO RevokeMsg(WxwRevokeMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO RevokeMsg(WxwRevokeMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/RevokeMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO> SendCDNImgMsg(WxwSendCDNImgMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO> SendCDNImgMsg(WxwSendCDNImgMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendCDNImgMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO>>() {
+        });
 
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendCDNFileMsgRespDTO> SendCDNFileMsg(WxwSendCDNFileMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendCDNFileMsgRespDTO> SendCDNFileMsg(WxwSendCDNFileMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendCDNFileMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNFileMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNFileMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO> SendCDNVoiceMsg(WxwSendCDNVoiceMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO> SendCDNVoiceMsg(WxwSendCDNVoiceMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendCDNVoiceMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendCDNVideoMsgRespDTO> SendCDNVideoMsg(WxwSendCDNVideoMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendCDNVideoMsgRespDTO> SendCDNVideoMsg(WxwSendCDNVideoMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendCDNVideoMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNVideoMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNVideoMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendVideoNumberRespDTO> sendVideoNumber(WxwSendVideoNumberDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendVideoNumberRespDTO> sendVideoNumber(WxwSendVideoNumberDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendVideoNumber";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendVideoNumberRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendVideoNumberRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendEmotionMessageRespDTO> SendEmotionMessage(WxwSendEmotionMessageDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendEmotionMessageRespDTO> SendEmotionMessage(WxwSendEmotionMessageDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendEmotionMessage";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendEmotionMessageRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendEmotionMessageRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendBusinessCardMsgRespDTO> SendBusinessCardMsg(WxwSendBusinessCardMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendBusinessCardMsgRespDTO> SendBusinessCardMsg(WxwSendBusinessCardMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendBusinessCardMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendBusinessCardMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendBusinessCardMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendLocationMsgRespDTO> SendLocationMsg(WxwSendLocationMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendLocationMsgRespDTO> SendLocationMsg(WxwSendLocationMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendLocationMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendLocationMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendLocationMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendTextAtMsgTwoRespDTO> sendTextAtMsgTwo(WxwSendTextAtMsgTwoDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendTextAtMsgTwoRespDTO> sendTextAtMsgTwo(WxwSendTextAtMsgTwoDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendTextAtMsgTwo";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendTextAtMsgTwoRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendTextAtMsgTwoRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSyncAllDataRespDTO> syncAllData(WxwSyncAllDataDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSyncAllDataRespDTO> syncAllData(WxwSyncAllDataDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SyncAllData";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSyncAllDataRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSyncAllDataRespDTO>>() {
+        });
     }
 
     @Override
-    public WxWorkResponseDTO<WxwSendCDNBigVideoMsgRespDTO> SendCDNBigVideoMsg(WxwSendCDNBigVideoMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxwSendCDNBigVideoMsgRespDTO> SendCDNBigVideoMsg(WxwSendCDNBigVideoMsgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendCDNBigVideoMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNBigVideoMsgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendCDNBigVideoMsgRespDTO>>() {
+        });
     }
 
     @Override
-    public String getCorpId(String uuid, Long corpId,Long serverId) {
+    public String getCorpId(String uuid, Long corpId, Long serverId) {
         String sCorpId = redisCache.getCacheObject("ipad:corpId:" + corpId);
-        if (sCorpId != null&& !sCorpId.isEmpty()) {
+        if (sCorpId != null && !sCorpId.isEmpty()) {
 
             return sCorpId;
         }
@@ -236,7 +260,8 @@ public class WxWorkServiceImpl implements WxWorkService {
         param.setUuid(uuid);
         param.setCorpid(corpId);
 
-        WxWorkResponseDTO<WxWorkCorpRespDTO> listWxWorkResponseDTO = WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkCorpRespDTO>>() {});
+        WxWorkResponseDTO<WxWorkCorpRespDTO> listWxWorkResponseDTO = WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkCorpRespDTO>>() {
+        });
         WxWorkCorpRespDTO data = listWxWorkResponseDTO.getData();
         List<WxWorkCorpRespDTO.CorpInfo> list = data.getList();
         if (list != null && !list.isEmpty()) {
@@ -246,8 +271,8 @@ public class WxWorkServiceImpl implements WxWorkService {
 
             String scorpId = corpInfo.getScorp_id();
 
-            if (scorpId!=null&&!scorpId.isEmpty()) {
-               redisCache.setCacheObject("ipad:corpId:" + corpId,scorpId);
+            if (scorpId != null && !scorpId.isEmpty()) {
+                redisCache.setCacheObject("ipad:corpId:" + corpId, scorpId);
 
                 return scorpId;
             }
@@ -255,78 +280,111 @@ public class WxWorkServiceImpl implements WxWorkService {
 
         return "";
     }
+
     @Override
-    public WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> Vid2UserId(WxWorkVid2UserIdDTO param,Long serverId) {
+    public WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> Vid2UserId(WxWorkVid2UserIdDTO param, Long serverId) {
         String url = getUrl(serverId) + "/Vid2UserId";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>>>() {
+        });
     }
 
     @Override
     public WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> SpeechToTextEntity(WxwSpeechToTextEntityDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SpeechToTextEntity";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO>>() {
+        });
     }
 
     @Override
     public WxWorkResponseDTO<WxwUploadCdnLinkFileRespDTO> uploadCdnLinkFile(WxwUploadCdnLinkFileDTO param, Long serverId) {
 
         String url = getUrl(serverId) + "/UploadCdnLinkFile";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwUploadCdnLinkFileRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwUploadCdnLinkFileRespDTO>>() {
+        });
     }
 
     @Override
     public WxWorkResponseDTO<WxwUploadCdnLinkImgRespDTO> uploadCdnLinkImg(WxwUploadCdnLinkImgDTO param, Long serverId) {
         String url = getUrl(serverId) + "/CdnUploadImgLink";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwUploadCdnLinkImgRespDTO>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwUploadCdnLinkImgRespDTO>>() {
+        });
     }
 
     @Override
     public WxwSilkVoceDTO getSilkVoice(String param, Long companyUserId) {
-        String  url= aiHostProper.getVoiceApi() + "/app/common/voice?voice="+param+"&id="+companyUserId;
+        String url = aiHostProper.getVoiceApi() + "/app/common/voice?voice=" + param + "&id=" + companyUserId;
         String json = WxWorkHttpUtil.get(url);
         WxwSilkVoceDTO wxwSilkVoceDTO = JSON.parseObject(json, WxwSilkVoceDTO.class);
         return wxwSilkVoceDTO;
     }
 
     @Override
-    public WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> UserId2Vid(WxWorkUserId2VidDTO param,Long serverId) {
+    public WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> UserId2Vid(WxWorkUserId2VidDTO param, Long serverId) {
         String url = getUrl(serverId) + "/UserId2Vid";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>>>() {
+        });
     }
 
     /**
      * 外部联系人图片视频文件下载
+     *
      * @param param    参数
      * @param serverId 服务器ID
-     * @return  WxWorkResponseDTO
+     * @return WxWorkResponseDTO
      */
     @Override
     public WxWorkResponseDTO<String> downloadWeChatFile(WxwDownloadWeChatFileDTO param, Long serverId) {
         String url = getUrl(serverId) + "/DownloadWeChatFile";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<String>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<String>>() {
+        });
     }
 
     /**
      * CDN下载文件重置版本
-     * @param param     参数
-     * @param serverId  服务器ID
-     * @return  WxWorkResponseDTO
+     *
+     * @param param    参数
+     * @param serverId 服务器ID
+     * @return WxWorkResponseDTO
      */
     @Override
     public WxWorkResponseDTO<String> downloadFile(WxDownloadFileDTO param, Long serverId) {
         String url = getUrl(serverId) + "/DownloadFile";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<String>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<String>>() {
+        });
     }
 
     /**
      * CDN上传网络图片
-     * @param param     参数
-     * @param serverId  服务器ID
-     * @return  WxWorkResponseDTO
+     *
+     * @param param    参数
+     * @param serverId 服务器ID
+     * @return WxWorkResponseDTO
      */
     @Override
     public WxWorkResponseDTO<WxCdnUploadImgLinkResp> cdnUploadImgLink(WxCdnUploadImgLinkDTO param, Long serverId) {
         String url = getUrl(serverId) + "/CdnUploadImgLink";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxCdnUploadImgLinkResp>>() {});
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxCdnUploadImgLinkResp>>() {
+        });
+    }
+
+    @Override
+    public WxwSilkVoceDTO getSilkVoiceDoubao(String content, Long companyUserId) {
+        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+        try {
+            if (vcCompanyUser == null)throw new RuntimeException("用户不存在");
+        }catch (Exception e){
+            log.error(String.format("用户id不存在于豆包: %s",companyUserId));
+        }
+
+        AudioVO audioVO = ttsServiceImpl.textToSpeech(new TtsRequest(null,null,vcCompanyUser.getSpeakerId(),content));
+        vcCompanyUser.setLatestTextToSpeechUrl(audioVO.getUrl());
+        companyUserMapper.updateVcCompanyUser(vcCompanyUser);
+        WxwSilkVoceDTO wxwSilkVoceDTO = new WxwSilkVoceDTO() ;
+        WxwSilkVoceDTO.Data data = new WxwSilkVoceDTO.Data();
+        data.setDuration(audioVO.getDuration());
+        data.setUrl(audioVO.getUrl());
+        wxwSilkVoceDTO.setData(data);
+        wxwSilkVoceDTO.setCode(200);
+        return wxwSilkVoceDTO;
     }
 }

+ 16 - 0
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -724,6 +724,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{userId}
         </foreach>
     </update>
+    <update id="updateVcCompanyUser">
+        update vc_company_user
+        set upload_url = #{vcCompanyUser.uploadUrl},
+            times = #{vcCompanyUser.times},
+            upload_time = #{vcCompanyUser.uploadTime},
+            latest_text_to_speech_url = #{vcCompanyUser.latestTextToSpeechUrl}
+        where company_user_id = #{vcCompanyUser.companyUserId}
+
+    </update>
 
     <!-- 根据销售ID查询绑定的fs_user用户列表 -->
     <select id="selectBoundFsUsersByCompanyUserId" resultType="com.fs.hisStore.domain.FsUserScrm">
@@ -738,5 +747,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by create_time desc
             limit 1
     </select>
+    <select id="selectVcCompanyUserByCompanyUserId" resultType="com.fs.company.param.VcCompanyUser">
+        select
+            id,times,speaker_id,upload_url,upload_time,company_user_id,latest_text_to_speech_url
+        from
+            vc_company_user
+        where  company_user_id = #{companyUserId}
+    </select>
 
 </mapper>

+ 22 - 0
fs-service/src/main/resources/mapper/course/FsCourseTrafficLogMapper.xml

@@ -285,6 +285,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="tabType!=null and tabType=='common'">
             group by course_id,`month`
         </if>
+<!--        <choose>-->
+<!--            &lt;!&ndash; 按公司+日期分组 &ndash;&gt;-->
+<!--            <when test="tabType == 'company'">-->
+<!--                GROUP BY company_id, `month`-->
+<!--            </when>-->
+<!--            &lt;!&ndash; 按项目+日期分组 &ndash;&gt;-->
+<!--            <when test="tabType == 'project'">-->
+<!--                GROUP BY project, `month`-->
+<!--            </when>-->
+<!--            &lt;!&ndash; 按课程+日期分组 &ndash;&gt;-->
+<!--            <when test="tabType == 'course'">-->
+<!--                GROUP BY course_id, `month`-->
+<!--            </when>-->
+<!--            &lt;!&ndash; common模式:按课程+日期分组 &ndash;&gt;-->
+<!--            <when test="tabType == 'common'">-->
+<!--                GROUP BY course_id, `month`-->
+<!--            </when>-->
+<!--            &lt;!&ndash; 默认:所有维度都分组 &ndash;&gt;-->
+<!--            <otherwise>-->
+<!--                GROUP BY company_id, project, course_id, `month`-->
+<!--            </otherwise>-->
+<!--        </choose>-->
     </select>
 
     <select id="selectTrafficByCompany" parameterType="com.fs.course.param.FsCourseTrafficLogParam"

+ 42 - 0
fs-service/src/main/resources/mapper/his/FsInquiryOrderMapper.xml

@@ -177,6 +177,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isUserInformation != null">#{isUserInformation},</if>
          </trim>
     </insert>
+    <insert id="insertHsLog">
+        INSERT INTO fs_inquiry_order_hs_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="responseBody != null">response_json,</if>
+            <if test="inquiryOrderId != null">inquiry_order_id,</if>
+            <if test="patientId != null">patient_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="status != null">type,</if>
+            <if test="responseData != null">decode_json,</if>
+            <if test="bookNo != null">book_no,</if>
+            create_time
+        </trim>
+        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
+            <if test="responseBody != null">#{responseBody},</if>
+            <if test="inquiryOrderId != null">#{inquiryOrderId},</if>
+            <if test="patientId != null">#{patientId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="responseData != null">#{responseData},</if>
+            <if test="bookNo != null">#{bookNo},</if>
+            NOW()
+        </trim>
+    </insert>
 
     <update id="updateFsInquiryOrder" parameterType="FsInquiryOrder">
         update fs_inquiry_order
@@ -227,6 +250,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <update id="closeOrder">
         update fs_inquiry_order set status = -1 where order_id = #{orderId}
     </update>
+    <update id="updateHsLog">
+        update fs_inquiry_order_hs_log set consult_room_json = #{jsonStr},update_time = NOW()
+        WHERE inquiry_order_id = #{orderId}
+    </update>
 
     <delete id="deleteFsInquiryOrderByOrderId" parameterType="Long">
         delete from fs_inquiry_order where order_id = #{orderId}
@@ -301,5 +328,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND fior.patient_json IS NOT NULL
           AND fior.patient_json->>'$.reportImages' != '';
     </select>
+    <select id="selectHsLog" resultType="com.fs.course.param.InquiryOrderHsLog">
+        select id,
+               response_json,
+               inquiry_order_id,
+               patient_id,
+               user_id,
+               type,
+               decode_json,
+               book_no
+        from
+            fs_inquiry_order_hs_log
+        where
+            inquiry_order_id = #{inquiryOrderId}
+        and type = 1
+    </select>
 
 </mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/his/FsStoreProductMapper.xml

@@ -247,4 +247,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         #{item}
     </foreach>
     </select>
+    <select id="selectFsStoreProductByBarCode" resultType="com.fs.his.domain.FsStoreProduct">
+        <include refid="selectFsStoreProductVo"/>
+        where bar_code = #{barCode}
+    </select>
 </mapper>

+ 420 - 99
fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -6,6 +6,12 @@ import cn.hutool.core.io.FileUtil;
 import cn.hutool.extra.qrcode.QrCodeUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.aiSoundReplication.VoiceCloneController;
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import com.fs.aiSoundReplication.service.impl.TtsServiceImpl;
+import com.fs.aiSoundReplication.util.FileToMultipartConverterUtil;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.FsBindCompanyUserParam;
 import com.fs.common.annotation.Log;
@@ -16,6 +22,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.CustomException;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.file.OssException;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
@@ -25,6 +32,7 @@ import com.fs.company.domain.CompanyUserCard;
 import com.fs.company.domain.CompanyUserUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.param.CompanyUserLoginParam;
+import com.fs.company.param.VcCompanyUser;
 import com.fs.company.param.companyUserAddPrintParam;
 import com.fs.company.service.ICompanyUserCardService;
 import com.fs.company.service.ICompanyUserService;
@@ -38,7 +46,9 @@ import com.fs.his.domain.FsPatient;
 import com.fs.his.domain.FsUserInformationCollection;
 import com.fs.his.param.*;
 import com.fs.his.service.*;
+import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.*;
+import com.fs.hisStore.enums.SysConfigEnum;
 import com.fs.sop.domain.QwSopTempVoice;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.system.oss.CloudStorageService;
@@ -48,6 +58,7 @@ import com.github.pagehelper.PageInfo;
 import io.lettuce.core.dynamic.annotation.Param;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
 import org.apache.commons.io.FileUtils;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpPost;
@@ -57,22 +68,31 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockMultipartFile;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.commons.CommonsMultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
 import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 
 @Api("销售接口")
 @RestController
-@RequestMapping(value="/app/companyUser")
-public class CompanyUserController extends  AppBaseController {
+@RequestMapping(value = "/app/companyUser")
+public class CompanyUserController extends AppBaseController {
 
 
     @Autowired
@@ -97,24 +117,31 @@ public class CompanyUserController extends  AppBaseController {
     private IFsQuestionAndAnswerService fsQuestionAndAnswerService;
 
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
+    @Autowired
+    private ConfigUtil configUtil;
+    @Autowired
+    private VoiceCloneController voiceCloneController;
+    @Autowired
+    private TtsServiceImpl ttsServiceImpl;
+
     @PostMapping("/login")
-    public R Login(@RequestBody CompanyUserLoginParam param, HttpServletRequest request){
+    public R Login(@RequestBody CompanyUserLoginParam param, HttpServletRequest request) {
         try {
-            CompanyUser companyUser=companyUserService.selectUserByUserName(param.getUserName());
-            if(companyUser==null||companyUser.getDelFlag().equals("1")){
+            CompanyUser companyUser = companyUserService.selectUserByUserName(param.getUserName());
+            if (companyUser == null || companyUser.getDelFlag().equals("1")) {
                 return R.error("用户不存在");
             }
-            if(!companyUser.getStatus().equals("0")){
+            if (!companyUser.getStatus().equals("0")) {
                 return R.error("用户已禁用");
 
             }
-            String pwd= SecurityUtils.encryptPassword(param.getPassword());
-            if(!SecurityUtils.matchesPassword(param.getPassword(),companyUser.getPassword())){
+            String pwd = SecurityUtils.encryptPassword(param.getPassword());
+            if (!SecurityUtils.matchesPassword(param.getPassword(), companyUser.getPassword())) {
                 return R.error("密码不正确");
             }
-            redisCache.setCacheObject("company-user-token:"+Md5Utils.hash(companyUser.getUserId().toString()),companyUser.getUserId(),100, TimeUnit.DAYS);
-            return R.ok().put("companyUserToken", Md5Utils.hash(companyUser.getUserId().toString())).put("user",companyUser);
-        } catch (Exception e){
+            redisCache.setCacheObject("company-user-token:" + Md5Utils.hash(companyUser.getUserId().toString()), companyUser.getUserId(), 100, TimeUnit.DAYS);
+            return R.ok().put("companyUserToken", Md5Utils.hash(companyUser.getUserId().toString())).put("user", companyUser);
+        } catch (Exception e) {
 
             return R.error("操作异常");
         }
@@ -122,33 +149,33 @@ public class CompanyUserController extends  AppBaseController {
 
     @ApiOperation("获取销售信息")
     @GetMapping("/getUserInfo")
-    public R getUserInfo(HttpServletRequest request){
+    public R getUserInfo(HttpServletRequest request) {
 
-        Long userId=getCompanyUserId();
-        if(userId==null){
-            return R.error(403,"用户失效");
+        Long userId = getCompanyUserId();
+        if (userId == null) {
+            return R.error(403, "用户失效");
         }
-        CompanyUser companyUser=companyUserService.selectCompanyUserById(userId);
-        if(companyUser==null||companyUser.getDelFlag().equals("1")){
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(userId);
+        if (companyUser == null || companyUser.getDelFlag().equals("1")) {
             return R.error("用户不存在");
         }
-        if(!companyUser.getStatus().equals("0")){
+        if (!companyUser.getStatus().equals("0")) {
             return R.error("用户已禁用");
         }
-        return R.ok().put("data",companyUser);
+        return R.ok().put("data", companyUser);
     }
 
     @Login
     @ApiOperation("绑定销售")
     @PostMapping("/bindCompanyUser")
-    public R bindCompanyUser(@Validated @RequestBody FsBindCompanyUserParam param, HttpServletRequest request){
-        CompanyUserUser map=new CompanyUserUser();
+    public R bindCompanyUser(@Validated @RequestBody FsBindCompanyUserParam param, HttpServletRequest request) {
+        CompanyUserUser map = new CompanyUserUser();
         map.setCompanyUserId(param.getCompanyUserId());
         map.setUserId(Long.parseLong(getUserId()));
-        List<CompanyUserUser> list= companyUserUserService.selectCompanyUserUserList(map);
-        if(list==null||list.size()==0){
-            CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
-            if(companyUser!=null&&companyUser.getStatus().equals("0")){
+        List<CompanyUserUser> list = companyUserUserService.selectCompanyUserUserList(map);
+        if (list == null || list.size() == 0) {
+            CompanyUser companyUser = companyUserService.selectCompanyUserById(param.getCompanyUserId());
+            if (companyUser != null && companyUser.getStatus().equals("0")) {
                 map.setCompanyId(companyUser.getCompanyId());
                 companyUserUserService.insertCompanyUserUser(map);
             }
@@ -161,14 +188,15 @@ public class CompanyUserController extends  AppBaseController {
     @ApiOperation("上传声纹")
     @PostMapping("/addVoicePrintUrl")
     public R addVoicePrintUrl(@RequestBody companyUserAddPrintParam param) throws Exception {
-        Long userId=getCompanyUserId();
-        if(userId==null){
-            return R.error(403,"用户失效");
+        Long userId = getCompanyUserId();
+        if (userId == null) {
+            return R.error(403, "用户失效");
         }
+
         CompanyUser companyUser = new CompanyUser();
         companyUser.setUserId(userId);
         companyUser.setVoicePrintUrl(param.getVoicePrintUrl());
-
+        String wavUrl = null;
         //转换音频格式 mp3-wav
         String s = AudioUtils.audioWAVFromUrl(param.getVoicePrintUrl());
 
@@ -177,16 +205,23 @@ public class CompanyUserController extends  AppBaseController {
         File file = new File(s);
         FileInputStream fileInputStream = new FileInputStream(file);
         CloudStorageService storage = OSSFactory.build();
-        String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
-
+        wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
+        /*判断sys的声纹复刻的配置value是不是2*/
+        JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+        if (vcConfig != null && !vcConfig.isEmpty() &&
+//                !vcConfig.equals(new JSONObject()) &&
+                "2".equals(vcConfig.getString("type"))) {
+            uploadVoice(userId, wavUrl);
+        }
         //更新销售员工声纹
         companyUser.setVoicePrintUrl(wavUrl);
         companyUserMapper.updateCompanyUser(companyUser);
 
+
         try {
             CloseableHttpClient httpClient = HttpClients.createDefault();
-            HttpPost httpPost = new HttpPost(aiHostProper.getCommonApi()+"/app/common/addCompanyAudio");
-            String json = "{\"url\":\""+wavUrl+"\",\"id\":\""+userId+"\"}";
+            HttpPost httpPost = new HttpPost(aiHostProper.getCommonApi() + "/app/common/addCompanyAudio");
+            String json = "{\"url\":\"" + wavUrl + "\",\"id\":\"" + userId + "\"}";
             StringEntity entity = new StringEntity(json);
             httpPost.setEntity(entity);
             httpPost.setHeader("Content-type", "application/json");
@@ -195,8 +230,8 @@ public class CompanyUserController extends  AppBaseController {
             if (response.getStatusLine().getStatusCode() == 200) {
                 String responseBody = EntityUtils.toString(response.getEntity());
                 JSONObject jsonObject = JSON.parseObject(responseBody);
-                Integer code = (Integer)jsonObject.get("code");
-                if (code==200){
+                Integer code = (Integer) jsonObject.get("code");
+                if (code == 200) {
                     voiceService.insertQwSopTempVoiceModel(userId);
                     return R.ok();
                 }
@@ -212,37 +247,40 @@ public class CompanyUserController extends  AppBaseController {
         return R.error();
 
     }
+
     @ApiOperation("小程序销售绑定医生")
     @Log(title = "小程序销售绑定医生", businessType = BusinessType.UPDATE)
     @PostMapping("/bindDoctorId")
-    public R binDoctor(@RequestBody CompanyUser companyUser){
+    public R binDoctor(@RequestBody CompanyUser companyUser) {
         return companyUserService.bindDoctor(companyUser);
     }
+
     @ApiOperation("小程序销售解除绑定医生")
     @Log(title = "小程序销售解除绑定医生", businessType = BusinessType.UPDATE)
     @GetMapping("/unBindDoctorId/{userId}")
-    public R unBinDoctor(@PathVariable("userId") Long userId){
+    public R unBinDoctor(@PathVariable("userId") Long userId) {
         return companyUserService.unBindDoctor(userId);
     }
 
     @ApiOperation("获取公司收款码")
     @GetMapping("/getCompanyWxaCodeByPayment")
-    public R getCompanyWxaCodeByPayment(@RequestParam("companyId")Long companyId,@RequestParam("appId")String appId,HttpServletRequest request){
+    public R getCompanyWxaCodeByPayment(@RequestParam("companyId") Long companyId, @RequestParam("appId") String appId, HttpServletRequest request) {
         //获取用户码
-        String WxaCode = redisCache.getCacheObject("company-wxa-code:"+companyId+":"+appId);
-        return R.ok().put("data",WxaCode);
+        String WxaCode = redisCache.getCacheObject("company-wxa-code:" + companyId + ":" + appId);
+        return R.ok().put("data", WxaCode);
     }
 
     /**
      * 生成绑定连接或者绑定码
+     *
      * @return
      */
     @Login
     @ApiOperation("获取绑定链接或者绑定码(唯一绑定)")
     @GetMapping("/getBindInfo")
-    public R getBindInfo(){
+    public R getBindInfo() {
         Long companyUserId = getCompanyUserId();
-        if(companyUserId==null){
+        if (companyUserId == null) {
             return R.error("该销售不存在");
         }
         return companyUserService.getBindInfo(companyUserId);
@@ -251,66 +289,88 @@ public class CompanyUserController extends  AppBaseController {
 
     /**
      * 当只有模板文字text时,生成表中对应条的voice_url和user_voice_url
-     * @param id            qw_sop_temp_voice的id
+     *
+     * @param id qw_sop_temp_voice的id
      * @return
      */
     @GetMapping("/companyUserVoice")
-    public R companyUserVoice(@RequestParam("id") Long id){
+    public R companyUserVoice(@RequestParam("id") Long id) {
         AudioVO audioVO = new AudioVO();
         Long companyUserId = getCompanyUserId();
         List<QwSopTempVoice> sopTempVoices = redisCache.getVoiceAllList(SOP_TEMP_VOICE_KEY + ":" + companyUserId);
-        if(sopTempVoices != null && !sopTempVoices.isEmpty()){
+        if (sopTempVoices != null && !sopTempVoices.isEmpty()) {
             List<Long> collect = sopTempVoices.stream().map(QwSopTempVoice::getId).collect(Collectors.toList());
-            if (collect.contains(id)){
-                return R.ok().put("code",202).put("msg","该语音已进入转换,请完成后再试。");
+            if (collect.contains(id)) {
+                return R.ok().put("code", 202).put("msg", "该语音已进入转换,请完成后再试。");
             }
         }
 
-        if(companyUserId != null){
+        if (companyUserId != null) {
             CompanyUser companyUser = companyUserMapper.selectCompanyUserByCompanyUserId(companyUserId);
-            if(companyUser != null && companyUser.getVoicePrintUrl() == null){
-                return R.ok().put("code",201).put("msg","账号未录制声纹,请录制后再试!");
+            if (companyUser != null && companyUser.getVoicePrintUrl() == null) {
+                return R.ok().put("code", 201).put("msg", "账号未录制声纹,请录制后再试!");
             }
         }
 
         QwSopTempVoice qwSopTempVoice = voiceService.selectQwSopTempVoiceById(id);
-        if(qwSopTempVoice != null && qwSopTempVoice.getCompanyUserId() != null){
-            List<FastgptChatVoiceHomo> homos = fastgptChatVoiceHomoMapper.selectFastgptChatVoiceHomoList(new FastgptChatVoiceHomo());
-            audioVO = AudioUtils.createUserUrlAndUrl(homos,qwSopTempVoice.getCompanyUserId(), qwSopTempVoice.getVoiceTxt().replace(" ",""));
-            if(audioVO != null && audioVO.getWavUrl() != null &&  audioVO.getUrl() != null){
+        if (qwSopTempVoice != null && qwSopTempVoice.getCompanyUserId() != null) {
+            JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+            if (vcConfig != null && !vcConfig.isEmpty() &&
+//                    !vcConfig.equals(new JSONObject()) &&
+                    "2".equals(vcConfig.getString("type"))) {
+                VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+                if (vcCompanyUser == null) throw new RuntimeException("用户不存在");
+                audioVO = ttsServiceImpl.textToSpeech(new TtsRequest(null, null, vcCompanyUser.getSpeakerId(), qwSopTempVoice.getVoiceTxt().replace(" ", "")));
+            } else {
+                List<FastgptChatVoiceHomo> homos = fastgptChatVoiceHomoMapper.selectFastgptChatVoiceHomoList(new FastgptChatVoiceHomo());
+                audioVO = AudioUtils.createUserUrlAndUrl(homos, qwSopTempVoice.getCompanyUserId(), qwSopTempVoice.getVoiceTxt().replace(" ", ""));
+            }
+            if (audioVO != null && audioVO.getWavUrl() != null && audioVO.getUrl() != null) {
                 qwSopTempVoice.setVoiceUrl(audioVO.getUrl());
                 qwSopTempVoice.setUserVoiceUrl(audioVO.getWavUrl());
                 qwSopTempVoice.setDuration(audioVO.getDuration());
                 qwSopTempVoice.setRecordType(1);
                 voiceService.updateQwSopTempVoice(qwSopTempVoice);
             }
+
         }
         return R.ok().put("data", audioVO);
     }
 
     /**
      * 当只有user_voice_url时,生成表中对应条的voice_url
-     * @param userVoiceUrl  wav格式的语音文件
-     * @param id            qw_sop_temp_voice的id
+     *
+     * @param userVoiceUrl wav格式的语音文件
+     * @param id           qw_sop_temp_voice的id
      * @return
      */
     @GetMapping("/companyUserVoiceNew")
-    public R companyUserVoiceNew( @RequestParam("id") Long id,@RequestParam("userVoiceUrl") String userVoiceUrl){
+    public R companyUserVoiceNew(@RequestParam("id") Long id, @RequestParam("userVoiceUrl") String userVoiceUrl) {
 
         AudioVO audioVO = new AudioVO();
         Long companyUserId = getCompanyUserId();
         List<QwSopTempVoice> sopTempVoices = redisCache.getVoiceAllList(SOP_TEMP_VOICE_KEY + ":" + companyUserId);
-        if(sopTempVoices != null && !sopTempVoices.isEmpty()){
+        if (sopTempVoices != null && !sopTempVoices.isEmpty()) {
             List<Long> collect = sopTempVoices.stream().map(QwSopTempVoice::getId).collect(Collectors.toList());
-            if (collect.contains(id)){
-                return R.ok().put("code",202).put("msg","该语音已进入转换,请完成后再试。");
+            if (collect.contains(id)) {
+                return R.ok().put("code", 202).put("msg", "该语音已进入转换,请完成后再试。");
             }
         }
 
         QwSopTempVoice qwSopTempVoice = voiceService.selectQwSopTempVoiceByIdAndUserVoiceUrl(id);
-        if(qwSopTempVoice != null && qwSopTempVoice.getId() != null){
-            audioVO = AudioUtils.createVoiceUrl(qwSopTempVoice.getCompanyUserId(), userVoiceUrl);
-            if(audioVO != null && audioVO.getUrl() != null){
+        if (qwSopTempVoice != null && qwSopTempVoice.getId() != null) {
+            JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+            if (vcConfig != null && !vcConfig.isEmpty() &&
+//                    !vcConfig.equals(new JSONObject()) &&
+                    "2".equals(vcConfig.getString("type"))) {
+                VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+                if (vcCompanyUser == null) throw new RuntimeException("用户不存在");
+                audioVO = ttsServiceImpl.textToSpeech(new TtsRequest(null, null, vcCompanyUser.getSpeakerId(), qwSopTempVoice.getVoiceTxt().replace(" ", "")));
+            } else {
+                audioVO = AudioUtils.createVoiceUrl(qwSopTempVoice.getCompanyUserId(), userVoiceUrl);
+            }
+
+            if (audioVO != null && audioVO.getUrl() != null) {
                 qwSopTempVoice.setVoiceUrl(audioVO.getUrl());
                 qwSopTempVoice.setUserVoiceUrl(userVoiceUrl);
                 qwSopTempVoice.setDuration(audioVO.getDuration());
@@ -322,12 +382,11 @@ public class CompanyUserController extends  AppBaseController {
     }
 
 
-
     @GetMapping("/query/{id}")
-    public R querySopVoiceById(@PathVariable("id") Long id){
+    public R querySopVoiceById(@PathVariable("id") Long id) {
         QwSopTempVoice tempVoice = voiceService.selectQwSopTempVoiceById(id);
         AudioVO audioVO = new AudioVO();
-        if(tempVoice != null){
+        if (tempVoice != null) {
             audioVO.setId(tempVoice.getId());
             audioVO.setVoiceTxt(tempVoice.getVoiceTxt());
             audioVO.setUrl(tempVoice.getVoiceUrl());
@@ -339,7 +398,7 @@ public class CompanyUserController extends  AppBaseController {
     }
 
     @GetMapping("/querySopVoiceList")
-    public TableDataInfo querySopVoiceList(@RequestParam("recordType") Integer recordType){
+    public TableDataInfo querySopVoiceList(@RequestParam("recordType") Integer recordType) {
         startPage();
         QwSopTempVoice sopTempVoice = new QwSopTempVoice();
         sopTempVoice.setRecordType(recordType);
@@ -350,55 +409,55 @@ public class CompanyUserController extends  AppBaseController {
 
     /**
      * 一键转换
+     *
      * @return
      */
     @GetMapping("/createUserAllVoice")
-    public R createUserAllVoice(){
+    public R createUserAllVoice() {
         QwSopTempVoice sopTempVoice = new QwSopTempVoice();
         sopTempVoice.setRecordType(0);
         Long companyUserId = getCompanyUserId();
 
 
-        if(companyUserId != null){
+        if (companyUserId != null) {
             CompanyUser companyUser = companyUserMapper.selectCompanyUserByCompanyUserId(companyUserId);
-            if(companyUser != null && companyUser.getVoicePrintUrl() == null){
-                return R.ok().put("code",201).put("msg","账号未录制声纹,请录制后再试!");
+            if (companyUser != null && companyUser.getVoicePrintUrl() == null) {
+                return R.ok().put("code", 201).put("msg", "账号未录制声纹,请录制后再试!");
             }
         }
 
         sopTempVoice.setCompanyUserId(companyUserId);
         List<QwSopTempVoice> sopTempVoices = voiceService.selectQwSopTempVoiceNewList(sopTempVoice);
-        if(sopTempVoices != null && !sopTempVoices.isEmpty()){
+        if (sopTempVoices != null && !sopTempVoices.isEmpty()) {
             List<Long> newCompanyUserId = redisCache.getVoiceAllList(SOP_TEMP_VOICE_KEY);
-            if(newCompanyUserId != null && newCompanyUserId.contains(companyUserId)){
-                return R.error().put("code",202).put("msg","语音还未转换完成,请完成后再添加!");
-            }else{
-                redisCache.setVoice(SOP_TEMP_VOICE_KEY,companyUserId);
-                sopTempVoices.forEach(m -> m.setVoiceTxt(m.getVoiceTxt().replace(" ","")));
+            if (newCompanyUserId != null && newCompanyUserId.contains(companyUserId)) {
+                return R.error().put("code", 202).put("msg", "语音还未转换完成,请完成后再添加!");
+            } else {
+                redisCache.setVoice(SOP_TEMP_VOICE_KEY, companyUserId);
+                sopTempVoices.forEach(m -> m.setVoiceTxt(m.getVoiceTxt().replace(" ", "")));
                 redisCache.setVoiceList(SOP_TEMP_VOICE_KEY + ":" + companyUserId, sopTempVoices);
-                return R.ok().put("msg","语音已加入队列进行转换,请耐心等待!");
+                return R.ok().put("msg", "语音已加入队列进行转换,请耐心等待!");
             }
         }
         return null;
     }
+
     @Login
     @GetMapping("/getPrescribeList")
-    public R getPrescribeList(FsPrescribeListDCompanyParam param)
-    {
+    public R getPrescribeList(FsPrescribeListDCompanyParam param) {
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setCompanyUserId(getCompanyUserId());
         param.setStatus(1);
-        List<FsPrescribeListDVO> list=fsPrescribeService.selectFsPrescribeListDVOByCompanyUser(param);
-        PageInfo<FsPrescribeListDVO> listPageInfo=new PageInfo<>(list);
-        return R.ok().put("data",listPageInfo);
+        List<FsPrescribeListDVO> list = fsPrescribeService.selectFsPrescribeListDVOByCompanyUser(param);
+        PageInfo<FsPrescribeListDVO> listPageInfo = new PageInfo<>(list);
+        return R.ok().put("data", listPageInfo);
     }
 
     /**
      * 查询用户信息采集列表问答列表
      */
     @GetMapping("/questionAndAnswer/allList")
-    public TableDataInfo getQuestionAndAnswer()
-    {
+    public TableDataInfo getQuestionAndAnswer() {
         List<OptionsVO> list = fsQuestionAndAnswerService.selectAllQuestionOptions();
         return getDataTable(list);
     }
@@ -409,8 +468,7 @@ public class CompanyUserController extends  AppBaseController {
      */
     @GetMapping("/informationCollection/list")
     @Login
-    public TableDataInfo list(FsUserInformationCollection fsUserInformationCollection)
-    {
+    public TableDataInfo list(FsUserInformationCollection fsUserInformationCollection) {
         startPage();
         Long companyUserId = getCompanyUserId();
         fsUserInformationCollection.setCompanyUserId(companyUserId);
@@ -423,8 +481,7 @@ public class CompanyUserController extends  AppBaseController {
      * 获取用户信息采集详细信息
      */
     @GetMapping(value = "/informationCollection/{id}")
-    public R getInformationCollectionInfo(@PathVariable("id") Long id)
-    {
+    public R getInformationCollectionInfo(@PathVariable("id") Long id) {
         return R.ok().put("data", fsUserInformationCollectionService.selectFsUserInformationCollectionVoById(id));
     }
 
@@ -433,25 +490,24 @@ public class CompanyUserController extends  AppBaseController {
      */
     @Login
     @PostMapping("/informationCollection")
-    public R add(@RequestBody FsUserInformationCollectionParam fsUserInformationCollection)
-    {
+    public R add(@RequestBody FsUserInformationCollectionParam fsUserInformationCollection) {
         Long companyUserId = getCompanyUserId();
         //查询用户是否绑定销售
         Long userId = fsUserInformationCollection.getUserId();
-        if (userId == null || userId < 0){
+        if (userId == null || userId < 0) {
             return R.error("请选择绑定用户");
         }
         CompanyUserUser companyUserUser = new CompanyUserUser();
         companyUserUser.setCompanyUserId(companyUserId);
         companyUserUser.setUserId(userId);
         List<CompanyUserUser> companyUserUsers = companyUserUserService.selectCompanyUserUserList(companyUserUser);
-        if (companyUserUsers == null || companyUserUsers.isEmpty()){
+        if (companyUserUsers == null || companyUserUsers.isEmpty()) {
             return R.error("用户未绑定该销售");
         }
 
         fsUserInformationCollection.setCompanyUserId(companyUserId);
         Long id = fsUserInformationCollectionService.insertFsUserInformationCollection(fsUserInformationCollection);
-        return id == null?R.error("新增失败"):R.ok().put("data",id);
+        return id == null ? R.error("新增失败") : R.ok().put("data", id);
     }
 
     /**
@@ -459,8 +515,7 @@ public class CompanyUserController extends  AppBaseController {
      */
     @PutMapping("/informationCollection")
     @Login
-    public R edit(@RequestBody FsUserInformationCollectionParam fsUserInformationCollection)
-    {
+    public R edit(@RequestBody FsUserInformationCollectionParam fsUserInformationCollection) {
         Long companyUserId = getCompanyUserId();
         fsUserInformationCollection.setCompanyUserId(companyUserId);
         fsUserInformationCollectionService.update(fsUserInformationCollection);
@@ -471,16 +526,282 @@ public class CompanyUserController extends  AppBaseController {
      * 删除用户信息采集
      */
     @DeleteMapping("/informationCollection/{ids}")
-    public AjaxResult remove(@PathVariable Long[] ids)
-    {
+    public AjaxResult remove(@PathVariable Long[] ids) {
         return toAjax(fsUserInformationCollectionService.deleteFsUserInformationCollectionByIds(ids));
     }
 
     @GetMapping("/informationCollection/getInfo")
-    public AjaxResult getInformationCollection(FsUserInformationCollection fsUserInformationCollection){
+    public AjaxResult getInformationCollection(FsUserInformationCollection fsUserInformationCollection) {
         return AjaxResult.success(fsUserInformationCollectionService.getInfo(fsUserInformationCollection));
     }
 
+    /**
+     * 上传音频豆包
+     *
+     * @param voicePrintUrl
+     * @return
+     * @throws Exception
+     */
+    @PostMapping("/uploadVoice")
+    public R uploadVoice(
+            Long userId,
+            String voicePrintUrl) throws Exception {
+        if (userId == null) userId = 123L;
+        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(userId);
+        if (vcCompanyUser == null) {
+            return R.error("用户没有声纹槽位,请联系管理员");
+        }
+        if (vcCompanyUser.getTimes() != null && vcCompanyUser.getTimes() >= 5)
+            throw new RuntimeException("用户已上传声纹达到上限");
+        vcCompanyUser.setUploadUrl(voicePrintUrl);
+        File file = downloadFileFromUrl(voicePrintUrl);
+        /*获取文件时长*/
+        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
+        AudioFormat format = audioInputStream.getFormat();
+        long frames = audioInputStream.getFrameLength();
+        // 计算时长(秒)= 总帧数 / 帧率
+        Double duration = (double) frames / format.getFrameRate();
+        audioInputStream.close();
+        MultipartFile convert = FileToMultipartConverterUtil.convert(file);
+        voiceCloneController.uploadVoice(vcCompanyUser.getSpeakerId(), convert, 1, 0);
+        vcCompanyUser.incrementTimes();
+        vcCompanyUser.setUploadTime(duration);
+        companyUserMapper.updateVcCompanyUser(vcCompanyUser);
+        return R.ok();
+    }
+//    private static MultipartFile downloadAsMultipartFile(String fileUrl) throws Exception {
+//        URL url = new URL(fileUrl);
+//        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+//
+//        // 设置请求属性
+//        connection.setRequestMethod("GET");
+//        connection.setConnectTimeout(10000); // 10秒连接超时
+//        connection.setReadTimeout(30000);    // 30秒读取超时
+//        connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+//        connection.setRequestProperty("Accept", "*/*");
+//        connection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
+//
+//        if (connection.getResponseCode() != 200) {
+//            throw new IOException("下载失败,响应码: " + connection.getResponseCode());
+//        }
+//        // 获取内容类型和文件名
+//        String contentType = connection.getContentType();
+//        String fileName = getFileNameFromConnection(connection, fileUrl);
+//        // 验证文件类型
+//        if (!isValidAudioFileType(contentType, fileName)) {
+//            throw new IOException("不支持的音频文件类型: " + contentType);
+//        }
+//        // 读取文件内容
+//        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+//        try (InputStream inputStream = connection.getInputStream()) {
+//            byte[] buffer = new byte[8192];
+//            int bytesRead;
+//            while ((bytesRead = inputStream.read(buffer)) != -1) {
+//                outputStream.write(buffer, 0, bytesRead);
+//            }
+//        }
+//
+//        byte[] fileBytes = outputStream.toByteArray();
+//        // 验证文件大小
+//        if (fileBytes.length == 0) {
+//            throw new IOException("下载的文件为空");
+//        }
+//        // 关闭连接
+//        connection.disconnect();
+//
+//        return new MockMultipartFile(
+//                "audioFile",                   // form-data 字段名
+//                fileName,                 // 原始文件名
+//                contentType,
+//                fileBytes                // 文件内容
+//        );
+//    }
+//    private static boolean isValidAudioFileType(String contentType, String fileName) {
+//        if (contentType != null) {
+//            return contentType.startsWith("audio/");
+//        }
+//        if (fileName != null) {
+//            String lowerFileName = fileName.toLowerCase();
+//            return lowerFileName.endsWith(".mp3") || lowerFileName.endsWith(".wav") ||
+//                    lowerFileName.endsWith(".pcm") || lowerFileName.endsWith(".m4a");
+//        }
+//        return false;
+//    }
+//    private static String getFileNameFromConnection(HttpURLConnection connection, String url) {
+//        // 1. 尝试从 Content-Disposition 头获取
+//        String contentDisposition = connection.getHeaderField("Content-Disposition");
+//        if (contentDisposition != null && contentDisposition.contains("filename=")) {
+//            String[] parts = contentDisposition.split("filename=");
+//            if (parts.length > 1) {
+//                String fileName = parts[1].replace("\"", "").trim();
+//                if (!fileName.isEmpty()) {
+//                    return fileName;
+//                }
+//            }
+//        }
+//
+//        // 2. 从 URL 路径提取
+//        try {
+//            URL parsedUrl = new URL(url);
+//            String path = parsedUrl.getPath();
+//            if (path.contains("/")) {
+//                String name = path.substring(path.lastIndexOf("/") + 1);
+//                if (!name.isEmpty()) {
+//                    return name;
+//                }
+//            }
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//        }
+//
+//        // 3. 默认文件名
+//        return "downloaded_file";
+//    }
+//    /**
+//     * 上传音频豆包
+//     *
+//     * @param voicePrintUrl
+//     * @return
+//     * @throws Exception
+//     */
+//    @PostMapping("/uploadVoiceTest")
+//    public R uploadVoiceTest(
+//            Long userId,
+//            @RequestParam String voicePrintUrl) throws Exception {
+//        if (userId == null) userId = 9565L;
+//        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(userId);
+//        vcCompanyUser.setUploadUrl(voicePrintUrl);
+//        File file = downloadFileFromUrl(voicePrintUrl);
+//        /*获取文件时长*/
+//        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
+//        AudioFormat format = audioInputStream.getFormat();
+//        long frames = audioInputStream.getFrameLength();
+//        // 计算时长(秒)= 总帧数 / 帧率
+//        Double duration = (double) frames / format.getFrameRate();
+//        audioInputStream.close();
+//        MultipartFile convert = FileToMultipartConverterUtil.convert(file);
+//
+//        voiceCloneController.uploadVoice(vcCompanyUser.getSpeakerId(), convert, 1, 0);
+//        vcCompanyUser.incrementTimes();
+//        vcCompanyUser.setUploadTime(duration);
+//        companyUserMapper.updateVcCompanyUser(vcCompanyUser);
+//        return R.ok();
+//    }
+//    /**
+//     * 上传音频豆包
+//     *
+//     * @param
+//     * @return
+//     * @throws Exception
+//     */
+//    @PostMapping("/uploadVoiceTest1")
+//    public R uploadVoiceTest1(
+//            Long userId,@RequestParam MultipartFile audioFile
+//            ) throws Exception {
+//        if (userId == null) userId = 9565L;
+//        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(userId);
+//        // 计算时长(秒)= 总帧数 / 帧率
+//        UploadResponse uploadResponse = voiceCloneController.uploadVoice(vcCompanyUser.getSpeakerId(), audioFile, 1, 0);
+//        return R.ok();
+//    }
 
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileUrl 文件路径
+     * @return 文件扩展名
+     */
+    private static String getFileExtension(String fileUrl) {
+        return fileUrl.substring(fileUrl.lastIndexOf("."));
+    }
+
+    private File downloadFileFromUrl(String fileUrl) throws IOException {
+        InputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        try {
+            // 创建 HTTP 连接
+            URL url = new URL(fileUrl);
+            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+            // 设置Referer请求头
+//            connection.setRequestProperty("Referer", "cos.his.cdwjyyh.com");
+            connection.setRequestMethod("GET");
+            connection.connect();
+
+            // 检查是否成功连接
+            if (connection.getResponseCode() != 200) {
+                throw new ServiceException("无法下载音频文件,HTTP 响应码:" + connection.getResponseCode());
+            }
+
+            // 获取输入流
+            inputStream = connection.getInputStream();
+
+            // 创建临时文件,并指定存放地址
+            String tempFileName = "temp_" + UUID.randomUUID() + "_" + getFileExtension(fileUrl);
+            File destinationDirectory = new File("c:\\hook\\");
+
+            // 参照 transferAudioSilk 方法,同步确保目录创建的线程安全
+            synchronized (AudioUtils.class) {
+                if (!destinationDirectory.exists()) {
+                    destinationDirectory.mkdirs();
+                }
+            }
+
+            // 将文件保存到指定路径
+            File tempFile = new File(destinationDirectory, tempFileName);
+
+            // 写入文件
+            outputStream = new FileOutputStream(tempFile);
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+
+            return tempFile;
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (inputStream != null) inputStream.close();
+                if (outputStream != null) outputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 文本转语音
+     *
+     * @param text
+     * @param httpRequest
+     * @return
+     * @throws Exception
+     */
+    @PostMapping("/synthesizeSimple")
+    public R synthesizeAndDownload(
+            @ApiParam(value = "要合成的文本", required = true)
+            @RequestParam String text,
+            @RequestParam Long companyUserId,
+            HttpServletRequest httpRequest) throws Exception {
+        if (companyUserId == null) companyUserId = 123L;
+//                getCompanyUserId();
+
+        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+        if (vcCompanyUser == null) throw new RuntimeException("用户不存在");
+        AudioVO audioVO = voiceCloneController.synthesizeSimple(text, vcCompanyUser.getSpeakerId(), "mp3", 1);
+        vcCompanyUser.setLatestTextToSpeechUrl(audioVO.getUrl());
+        companyUserMapper.updateVcCompanyUser(vcCompanyUser);
+        return R.ok().put("url", audioVO.getUrl());
+    }
+
+    @GetMapping("/status/{speakerId}")
+    @ApiOperation("查询音色训练状态")
+    public StatusResponse getTrainingStatus(
+            @ApiParam(value = "音色ID", required = true)
+            @PathVariable String speakerId) {
+        return voiceCloneController.getTrainingStatus(speakerId);
+    }
 
 }

+ 65 - 8
fs-user-app/src/main/java/com/fs/app/controller/InquiryPatientInfoController.java

@@ -1,8 +1,12 @@
 package com.fs.app.controller;
 
+import com.fs.aiSoundReplication.VoiceCloneController;
 import com.fs.app.annotation.Login;
+import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
 import com.fs.course.param.FsInquiryPatientInfoUParam;
+import com.fs.course.param.SendInquiryParam;
 import com.fs.course.vo.FsInquiryPatientInfoListUVO;
 import com.fs.his.service.IFsInquiryPatientInfoService;
 import com.github.pagehelper.PageHelper;
@@ -10,17 +14,16 @@ import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 
 @Api("患者问诊单接口")
 @RestController
-@RequestMapping(value="/app/inquiryPatient")
-public class InquiryPatientInfoController extends AppBaseController{
+@RequestMapping(value = "/app/inquiryPatient")
+public class InquiryPatientInfoController extends AppBaseController {
 
     @Autowired
     private IFsInquiryPatientInfoService patientInfoService;
@@ -34,7 +37,7 @@ public class InquiryPatientInfoController extends AppBaseController{
         param.setUserId(Long.parseLong(getUserId()));
         List<FsInquiryPatientInfoListUVO> fsInquiryPatientInfoListUVOS = patientInfoService.selectFsInquiryPatientInfoListUVO(param);
         PageInfo<FsInquiryPatientInfoListUVO> pageInfo = new PageInfo<>(fsInquiryPatientInfoListUVOS);
-        return R.ok().put("data",pageInfo);
+        return R.ok().put("data", pageInfo);
     }
 
 
@@ -42,6 +45,60 @@ public class InquiryPatientInfoController extends AppBaseController{
     @GetMapping("/getInfo")
     public R getInfo(@RequestParam("id") Long id) {
         FsInquiryPatientInfoListUVO info = patientInfoService.getInfo(id);
-        return R.ok().put("data",info);
+        return R.ok().put("data", info);
     }
+
+    //    @Login
+    @ApiOperation("患者发起问诊申请给红衫")
+    @PostMapping("/sendInquiryToHS")
+//    @Log日志 //todo 数据流转已存fs_inquiry_order_hs_log
+    @RepeatSubmit
+    @Transactional(rollbackFor = Exception.class)
+    public R sendInquiryToHS(@RequestBody SendInquiryParam param) throws Exception {
+        if (StringUtils.isNotEmpty(getUserId())) {
+            param.setUserId(getUserId());
+        }
+        return R.ok(patientInfoService.sendInquiryToHS(param));
+    }
+
+    @ApiOperation("红杉问诊申请回调-周期回调(暂时只处理处方请求)")
+    @PostMapping("/receiveInquiryFromHsCallbackNotification")
+    public R receiveInquiryFromHsCallbackNotification(HttpServletRequest param) throws Exception {
+
+        return R.ok(patientInfoService.receiveInquiryFromHsCallbackNotification(param));
+    }
+
+    @ApiOperation("红杉问诊咨询问诊医生获取-需自动调用")
+    @PostMapping("/getDoctorScheduleFromHs")
+    public R getDoctorScheduleFromHs() throws Exception {
+        return R.ok().put("data", patientInfoService.getDoctorScheduleFromHs());
+    }
+
+    @ApiOperation("发起咨询后-获取诊室及跳转")
+    @PostMapping("/redirectToHs")
+    public R redirectToHs(@RequestBody String inquiryOrderId) throws Exception {
+        return R.ok(patientInfoService.redirectToHs(inquiryOrderId));
+    }
+
+    @ApiOperation("取消问诊挂号")
+    @PostMapping("/cancelInquiryToHs")
+    public R cancelInquiryToHs(@RequestBody String inquiryOrderId) throws Exception {
+        return R.ok(patientInfoService.cancelInquiryToHs(inquiryOrderId));
+    }
+
+
+//    @ApiOperation("红杉问诊申请回调-进入诊室失败")
+//    @PostMapping("/recieveInquiryFromHsRedirect")
+////    @Log日志 //todo
+//    public R recieveInquiryFromHsRedirect(HttpServletRequest param) throws JsonProcessingException {
+//        patientInfoService.updateFsInquiryPatientInfoFromHsRedirect(param);
+//        return R.ok();
+//    }
+//    @ApiOperation("红杉问诊申请回调-接收通知-获取处方")
+//    @PostMapping("/recieveInquiryFromHsDispensary")
+////    @Log日志 //todo
+//    public R recieveInquiryFromHsDispensary(HttpServletRequest param) throws JsonProcessingException {
+//        patientInfoService.recieveInquiryFromHsDispensary(param);
+//        return R.ok();
+//    }
 }