吴树波 2 minggu lalu
melakukan
1914633419
100 mengubah file dengan 4177 tambahan dan 0 penghapusan
  1. 48 0
      .gitignore
  2. 20 0
      LICENSE
  3. 25 0
      README.md
  4. 12 0
      bin/clean.bat
  5. 12 0
      bin/package.bat
  6. 14 0
      bin/run.bat
  7. TEMPAT SAMPAH
      doc/images/file.gif
  8. TEMPAT SAMPAH
      doc/images/keyboard.png
  9. TEMPAT SAMPAH
      doc/images/logo.jpg
  10. TEMPAT SAMPAH
      doc/images/minus.gif
  11. TEMPAT SAMPAH
      doc/images/mute.jpg
  12. TEMPAT SAMPAH
      doc/images/mutein.ico
  13. TEMPAT SAMPAH
      doc/images/network-callcenter.png
  14. TEMPAT SAMPAH
      doc/images/network-tra.png
  15. TEMPAT SAMPAH
      doc/images/no_video.jpg
  16. TEMPAT SAMPAH
      doc/images/phone-bar-docs/phone-bar-config-ip.png
  17. TEMPAT SAMPAH
      doc/images/phone-bar-docs/phone-bar-config-outbound-gw.png
  18. TEMPAT SAMPAH
      doc/images/phone-bar-docs/phone-bar-online.png
  19. TEMPAT SAMPAH
      doc/images/phone-bar.png
  20. TEMPAT SAMPAH
      doc/images/phone.png
  21. TEMPAT SAMPAH
      doc/images/phonebar/busy_enable.png
  22. TEMPAT SAMPAH
      doc/images/phonebar/call.png
  23. TEMPAT SAMPAH
      doc/images/phonebar/conference.png
  24. TEMPAT SAMPAH
      doc/images/phonebar/consultation.png
  25. TEMPAT SAMPAH
      doc/images/phonebar/hangup_enable.png
  26. TEMPAT SAMPAH
      doc/images/phonebar/hold.png
  27. TEMPAT SAMPAH
      doc/images/phonebar/online.png
  28. TEMPAT SAMPAH
      doc/images/phonebar/reset.png
  29. TEMPAT SAMPAH
      doc/images/phonebar/setFree.png
  30. TEMPAT SAMPAH
      doc/images/phonebar/transfer.png
  31. TEMPAT SAMPAH
      doc/images/phonebar/unhold.png
  32. TEMPAT SAMPAH
      doc/images/plus.gif
  33. TEMPAT SAMPAH
      doc/images/process-flow.png
  34. TEMPAT SAMPAH
      doc/images/transparent-25-bg.png
  35. TEMPAT SAMPAH
      doc/images/transparent-50-bg.png
  36. TEMPAT SAMPAH
      doc/images/transparent-black-75-bg.png
  37. TEMPAT SAMPAH
      doc/images/unmute.jpg
  38. TEMPAT SAMPAH
      doc/images/video.jpg
  39. TEMPAT SAMPAH
      doc/manual/images/20250728084822.png
  40. TEMPAT SAMPAH
      doc/manual/images/20250728090628.png
  41. TEMPAT SAMPAH
      doc/manual/images/20250728090818.png
  42. TEMPAT SAMPAH
      doc/manual/images/20250728091000.png
  43. TEMPAT SAMPAH
      doc/manual/images/20250728092159.png
  44. TEMPAT SAMPAH
      doc/manual/images/20250728092309.png
  45. TEMPAT SAMPAH
      doc/manual/images/20250728092457.png
  46. TEMPAT SAMPAH
      doc/manual/images/20250728092937.png
  47. TEMPAT SAMPAH
      doc/manual/images/20250728093730.png
  48. TEMPAT SAMPAH
      doc/manual/images/20250728094246.png
  49. TEMPAT SAMPAH
      doc/manual/images/20250728094559.png
  50. TEMPAT SAMPAH
      doc/manual/images/20250728094734.png
  51. TEMPAT SAMPAH
      doc/manual/images/20250728095553.png
  52. TEMPAT SAMPAH
      doc/manual/images/20250728095854.png
  53. TEMPAT SAMPAH
      doc/manual/images/20250728100738.png
  54. TEMPAT SAMPAH
      doc/manual/images/20250728100858.png
  55. TEMPAT SAMPAH
      doc/manual/images/20250728101135.png
  56. TEMPAT SAMPAH
      doc/manual/images/20250728102029.png
  57. TEMPAT SAMPAH
      doc/manual/images/20250728102338.png
  58. TEMPAT SAMPAH
      doc/manual/images/20250728102532.png
  59. TEMPAT SAMPAH
      doc/manual/images/20250728102959.png
  60. TEMPAT SAMPAH
      doc/manual/images/20250728103109.png
  61. 197 0
      doc/manual/manual.md
  62. TEMPAT SAMPAH
      doc/shot/webgui-1.png
  63. TEMPAT SAMPAH
      doc/shot/webgui-2.png
  64. TEMPAT SAMPAH
      doc/shot/webgui-3.png
  65. 26 0
      doc/版本更新记录.md
  66. TEMPAT SAMPAH
      logo.jpg
  67. 289 0
      pom.xml
  68. 212 0
      ruoyi-admin/pom.xml
  69. 69 0
      ruoyi-admin/src/main/assembly/assembly.xml
  70. 33 0
      ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
  71. 18 0
      ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java
  72. 715 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/ApiController.java
  73. 197 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallPhoneController.java
  74. 410 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallTaskController.java
  75. 170 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcInboundLlmAccountController.java
  76. 248 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcLlmAgentAccountController.java
  77. 138 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcLlmAgentProviderController.java
  78. 138 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcTtsAliyunController.java
  79. 87 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/FaqController.java
  80. 136 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallPhone.java
  81. 129 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallTask.java
  82. 53 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcInboundLlmAccount.java
  83. 60 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcLlmAgentAccount.java
  84. 32 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcLlmAgentProvider.java
  85. 38 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcTtsAliyun.java
  86. 30 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/AbstractLlmCapability.java
  87. 15 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/ILlmCapability.java
  88. 15 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/Coze.java
  89. 67 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/DeepSeekChat.java
  90. 15 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/Dify.java
  91. 15 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/MaxKB.java
  92. 49 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/AccountBaseEntity.java
  93. 22 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/CozeAccount.java
  94. 21 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/LlmAccount.java
  95. 86 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcCallPhoneMapper.java
  96. 70 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcCallTaskMapper.java
  97. 63 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcInboundLlmAccountMapper.java
  98. 61 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcLlmAgentAccountMapper.java
  99. 61 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcLlmAgentProviderMapper.java
  100. 61 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcTtsAliyunMapper.java

+ 48 - 0
.gitignore

@@ -0,0 +1,48 @@
+######################################################################
+# Build Tools
+
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### JRebel ###
+rebel.xml
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+dist/
+logs/
+nbdist/
+.nb-gradle/
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
+/README.en.md

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 RuoYi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 25 - 0
README.md

@@ -0,0 +1,25 @@
+# easycallcenter365
+
+![easycallcenter365](logo.jpg)
+
+基于FreeSWITCH和大模型的智能电话客服系统。
+
+### 功能列表
+
+* 支持对接大模型/ `coze` 智能体 / Dify /MaxKB
+* 支持AI客服说话时被打断/打断关键词设置
+* 提供网页管理系统,支持在线配置
+* 实时流式语音合成/FunAsr开源语音识别/阿里云语音识别
+* 支持AI通话无缝转接人工坐席
+* 支持电话工具条/支持acd话务排队
+* 支持多个AI呼入客服/独立配置呼入号码及大模型底座
+* 支持创建AI语音外呼任务
+
+更新日期: 2025/08/05
+
+### 技术交流 && 商业咨询
+
+bug反馈或者咨询问题请在gitee/github上,新建 Issue,并贴上日志。
+
+![联系方式](wechat.png)
+

+ 12 - 0
bin/clean.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 清理工程target生成路径。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+call mvn clean
+
+pause

+ 12 - 0
bin/package.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 打包Web工程,生成war/jar包文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+call mvn clean package -Dmaven.test.skip=true
+
+pause

+ 14 - 0
bin/run.bat

@@ -0,0 +1,14 @@
+@echo off
+echo.
+echo [信息] 使用Jar命令运行Web工程。
+echo.
+
+cd %~dp0
+cd ../ruoyi-admin/target
+
+set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
+
+java -jar %JAVA_OPTS% ruoyi-admin.jar
+
+cd bin
+pause

TEMPAT SAMPAH
doc/images/file.gif


TEMPAT SAMPAH
doc/images/keyboard.png


TEMPAT SAMPAH
doc/images/logo.jpg


TEMPAT SAMPAH
doc/images/minus.gif


TEMPAT SAMPAH
doc/images/mute.jpg


TEMPAT SAMPAH
doc/images/mutein.ico


TEMPAT SAMPAH
doc/images/network-callcenter.png


TEMPAT SAMPAH
doc/images/network-tra.png


TEMPAT SAMPAH
doc/images/no_video.jpg


TEMPAT SAMPAH
doc/images/phone-bar-docs/phone-bar-config-ip.png


TEMPAT SAMPAH
doc/images/phone-bar-docs/phone-bar-config-outbound-gw.png


TEMPAT SAMPAH
doc/images/phone-bar-docs/phone-bar-online.png


TEMPAT SAMPAH
doc/images/phone-bar.png


TEMPAT SAMPAH
doc/images/phone.png


TEMPAT SAMPAH
doc/images/phonebar/busy_enable.png


TEMPAT SAMPAH
doc/images/phonebar/call.png


TEMPAT SAMPAH
doc/images/phonebar/conference.png


TEMPAT SAMPAH
doc/images/phonebar/consultation.png


TEMPAT SAMPAH
doc/images/phonebar/hangup_enable.png


TEMPAT SAMPAH
doc/images/phonebar/hold.png


TEMPAT SAMPAH
doc/images/phonebar/online.png


TEMPAT SAMPAH
doc/images/phonebar/reset.png


TEMPAT SAMPAH
doc/images/phonebar/setFree.png


TEMPAT SAMPAH
doc/images/phonebar/transfer.png


TEMPAT SAMPAH
doc/images/phonebar/unhold.png


TEMPAT SAMPAH
doc/images/plus.gif


TEMPAT SAMPAH
doc/images/process-flow.png


TEMPAT SAMPAH
doc/images/transparent-25-bg.png


TEMPAT SAMPAH
doc/images/transparent-50-bg.png


TEMPAT SAMPAH
doc/images/transparent-black-75-bg.png


TEMPAT SAMPAH
doc/images/unmute.jpg


TEMPAT SAMPAH
doc/images/video.jpg


TEMPAT SAMPAH
doc/manual/images/20250728084822.png


TEMPAT SAMPAH
doc/manual/images/20250728090628.png


TEMPAT SAMPAH
doc/manual/images/20250728090818.png


TEMPAT SAMPAH
doc/manual/images/20250728091000.png


TEMPAT SAMPAH
doc/manual/images/20250728092159.png


TEMPAT SAMPAH
doc/manual/images/20250728092309.png


TEMPAT SAMPAH
doc/manual/images/20250728092457.png


TEMPAT SAMPAH
doc/manual/images/20250728092937.png


TEMPAT SAMPAH
doc/manual/images/20250728093730.png


TEMPAT SAMPAH
doc/manual/images/20250728094246.png


TEMPAT SAMPAH
doc/manual/images/20250728094559.png


TEMPAT SAMPAH
doc/manual/images/20250728094734.png


TEMPAT SAMPAH
doc/manual/images/20250728095553.png


TEMPAT SAMPAH
doc/manual/images/20250728095854.png


TEMPAT SAMPAH
doc/manual/images/20250728100738.png


TEMPAT SAMPAH
doc/manual/images/20250728100858.png


TEMPAT SAMPAH
doc/manual/images/20250728101135.png


TEMPAT SAMPAH
doc/manual/images/20250728102029.png


TEMPAT SAMPAH
doc/manual/images/20250728102338.png


TEMPAT SAMPAH
doc/manual/images/20250728102532.png


TEMPAT SAMPAH
doc/manual/images/20250728102959.png


TEMPAT SAMPAH
doc/manual/images/20250728103109.png


+ 197 - 0
doc/manual/manual.md

@@ -0,0 +1,197 @@
+## 操作手册
+
+
+
+### 1. 语音识别配置
+
+配置语音识别引擎,目前支持FunASR、讯飞ASR、阿里ASR。
+
+#### 1.1 FunASR配置
+
+根据部署的funasr服务配置相关参数,主要修改服务器地址,其他参数用默认值即可,如果funasr部署在同一台服务且端口没修改,该配置可以不修改
+
+![20250728084822](images/20250728084822.png)
+
+
+
+
+
+###  1.2 阿里ASR配置
+
+根据阿里云账号信息配置相关参数,主要修改Access-Key-Id、App-Key和Access-Key-Secret,其他参数用默认值即可
+
+![20250728090628](images/20250728090628.png)
+
+
+
+
+
+
+
+### 1.3 讯飞ASR配置
+
+根据讯飞开放平台账号信息配置相关参数,主要修改APP-ID、API-Key,其他参数用默认值即可
+
+![20250728090818](images/20250728090818.png)
+
+
+
+
+
+### 1.4 默认ASR设置
+
+选择当前需要启用的ASR配置
+
+![20250728091000](images/20250728091000.png)
+
+
+
+### 2. 语音合成配置
+
+配置语音合成引擎,目前支持阿里云tts配置
+
+### 2.1 阿里云tts配置
+
+根据阿里云账号信息配置相关参数,主要修改access_key_id、app_key和access_key_secret,其他参数用默认值即可
+
+![20250728092159](images/20250728092159.png)
+
+
+
+
+
+### 3. 线路配置
+
+配置语音网关,支持对接模式和注册模式,并且支持根据外呼任务的不同配置不同用途的网关
+
+![20250728092309](images/20250728092309.png)
+
+
+
+![20250728092457](images/20250728092457.png)
+
+
+
+
+
+### 4. 大模型配置
+
+配置大模型或者智能体底座信息,用于AI外呼和AI呼入的机器人应答的数据接入,支持直接对接类deepseek,直接对接类chatgpt、对接coze、对接dify、对接maxkb,并支持开启关键词打断功能并配置打断关键词
+
+![20250728092937](images/20250728092937.png)
+
+
+
+### 4.1 类deepseek
+
+支持阿里云百炼对接的deepseek模型,理论上可以支持所有通过url、apiKey、模型名称三个参数对接大模型接口的开放平台和模型,并根据具体业务场景配置大模型提示词、FAQ内容、转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728093730](images/20250728093730.png)
+
+
+
+
+
+### 4.2 coze
+
+支持对接扣子智能体,根据扣子账号信息配置服务地址、botId、token,其中token支持pat和oauth两种方式,详细信息请参考扣子官网说明,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094246](images/20250728094246.png)
+
+
+
+### 4.3 dify
+
+支持对接dify智能体,根据dify账号配置apiKey、服务地址参数,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094559](images/20250728094559.png)
+
+
+
+### 4.4 maxkb
+
+支持对接maxkb智能体,根据maxkb账号配置apiKey、服务地址,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094734](images/20250728094734.png)
+
+
+
+### 4.5 打断
+
+支持关键词打断配置,开启打断后需要配置打断关键词和忽略打断关键词,打断关键词指的是客户说话命中打断,忽略打断关键词指的是命中不打断(比如一些语气词)
+
+![20250728095553](images/20250728095553.png)
+
+
+
+### 5. 外呼任务管理
+
+支持创建外呼任务,支持AI外呼、人工预测式外呼和语音通知,配置好外呼任务以后,在任务列表点击“导入数据”(导入模板在配置外呼任务的页面可以下载),选择excell文件导入外呼名单(名单尽量不要超过10w条数据),导入完成后点击“启动任务”按钮开始外呼,外呼后可以在该页面查看已拨打量、接通量、接通率等统计数据,可以在AI外呼->AI外呼记录查看具体的通话记录
+
+![20250728095854](images/20250728095854.png)
+
+
+
+### 5.1 AI外呼
+
+AI外呼任务,指的是创建并启动任务以后,拨打客户电话,接通后有配置的大模型底座作为大脑生成应答话术,配置的音色作为嘴巴生成实时语音跟客户沟通。
+
+配置AI外呼任务,需要配置外呼的最大并发,外呼使用的线路(可选项在基础配置->线路配置功能中配置),外呼使用的大模型底座(可选项在基础配置->大模型配置中配置),机器人应答使用的音色(内置的阿里云tts的音色)
+
+![20250728100738](images/20250728100738.png)
+
+
+
+### 5.2 人工预测式外呼
+
+人工预测外呼任务指的是创建外呼任务后,系统根据空闲坐席的数量进行自动外呼,接通后转给空闲坐席(人工坐席)跟客户沟通
+
+![20250728101135](images/20250728101135.png)
+
+
+
+### 5.3 语音通知
+
+语音通知指的是,创建并启动任务后,拨打客户电话,接通后播报一句提醒话术后挂机(话术内容在导入时配置)
+
+![20250728102029](images/20250728102029.png)
+
+
+
+### 6. AI外呼记录查询
+
+查询外呼任务管理创建的任务的外呼拨打生成的记录,支持在线听录音、下载录音和查看文本对话
+
+![20250728102338](images/20250728102338.png)
+
+
+
+
+
+### 7. 呼入配置
+
+
+
+配置呼入(客服业务)相关参数
+
+![20250728102532](images/20250728102532.png)
+
+
+
+呼入需要配置大模型底座(可选项在基础配置->大模型配置中配置),音色,被叫号码(即客服号码),服务方式(ai,acd),转人工业务组
+
+![20250728102959](images/20250728102959.png)
+
+
+
+
+
+
+
+### 8. 呼入记录查询
+
+
+
+查询呼入的通话记录,支持在线听录音、下载录音和查看文本对话
+
+![20250728103109](images/20250728103109.png)

TEMPAT SAMPAH
doc/shot/webgui-1.png


TEMPAT SAMPAH
doc/shot/webgui-2.png


TEMPAT SAMPAH
doc/shot/webgui-3.png


+ 26 - 0
doc/版本更新记录.md

@@ -0,0 +1,26 @@
+### v20250804
+初始版本,功能清单如下
+1. 我的工作台
+2. 基础配置
+    1. 参数管理
+    2. FreeSwitch配置
+    3. 语音识别配置
+    4. 语音合成配置
+    5. 证书配置
+    6. 日志监控
+    7. 分级权限配置
+    8. 线路配置
+    9. 客户列表
+
+3. 呼叫中心
+    1. 呼入配置
+    2. 业务组配置
+    3. 手工外呼查询
+    4. 呼入记录查询
+    5. 坐席监控
+    6. 呼入队列监控
+   
+4. AI外呼
+    1. 大模型配置
+    2. 外呼任务管理
+    3. AI外呼记录

TEMPAT SAMPAH
logo.jpg


+ 289 - 0
pom.xml

@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.ruoyi</groupId>
+    <artifactId>ruoyi</artifactId>
+    <version>4.7.9</version>
+
+    <name>easycallcenter365-gui</name>
+    <description>easycallcenter365</description>
+    
+    <properties>
+        <ruoyi.version>4.7.9</ruoyi.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <java.version>1.8</java.version>
+        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
+        <spring-boot.version>2.5.15</spring-boot.version>
+        <shiro.version>1.13.0</shiro.version>
+        <thymeleaf.extras.shiro.version>2.1.0</thymeleaf.extras.shiro.version>
+        <druid.version>1.2.23</druid.version>
+        <bitwalker.version>1.21</bitwalker.version>
+        <kaptcha.version>2.3.3</kaptcha.version>
+        <swagger.version>3.0.0</swagger.version>
+        <pagehelper.boot.version>1.4.7</pagehelper.boot.version>
+        <fastjson.version>1.2.83</fastjson.version>
+        <oshi.version>6.6.5</oshi.version>
+        <commons.io.version>2.16.1</commons.io.version>
+        <poi.version>4.1.2</poi.version>
+        <velocity.version>2.3</velocity.version>
+        <!-- override dependency version -->
+        <tomcat.version>9.0.96</tomcat.version>
+        <logback.version>1.2.13</logback.version>
+        <spring-framework.version>5.3.39</spring-framework.version>
+    </properties>
+
+    <!-- 依赖声明 -->
+    <dependencyManagement>
+        <dependencies>
+
+            <!-- 覆盖SpringFramework的依赖配置-->
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-framework-bom</artifactId>
+                <version>${spring-framework.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- SpringBoot的依赖配置-->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- 覆盖logback的依赖配置-->
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-core</artifactId>
+                <version>${logback.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-classic</artifactId>
+                <version>${logback.version}</version>
+            </dependency>
+
+            <!-- 覆盖tomcat的依赖配置-->
+            <dependency>
+                <groupId>org.apache.tomcat.embed</groupId>
+                <artifactId>tomcat-embed-core</artifactId>
+                <version>${tomcat.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.tomcat.embed</groupId>
+                <artifactId>tomcat-embed-el</artifactId>
+                <version>${tomcat.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.tomcat.embed</groupId>
+                <artifactId>tomcat-embed-websocket</artifactId>
+                <version>${tomcat.version}</version>
+            </dependency>
+
+            <!-- 阿里数据库连接池 -->
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>druid-spring-boot-starter</artifactId>
+                <version>${druid.version}</version>
+            </dependency>
+            
+            <!-- 验证码 -->
+            <dependency>
+                <groupId>pro.fessional</groupId>
+                <artifactId>kaptcha</artifactId>
+                <version>${kaptcha.version}</version>
+            </dependency>
+
+            <!-- Shiro核心框架 -->
+            <dependency>
+                <groupId>org.apache.shiro</groupId>
+                <artifactId>shiro-core</artifactId>
+                <version>${shiro.version}</version>
+            </dependency>
+
+            <!-- Shiro使用Spring框架 -->
+            <dependency>
+                <groupId>org.apache.shiro</groupId>
+                <artifactId>shiro-spring</artifactId>
+                <version>${shiro.version}</version>
+            </dependency>
+
+            <!-- Shiro使用EhCache缓存框架 -->
+            <dependency>
+                <groupId>org.apache.shiro</groupId>
+                <artifactId>shiro-ehcache</artifactId>
+                <version>${shiro.version}</version>
+            </dependency>
+
+            <!-- thymeleaf模板引擎和shiro框架的整合 -->
+            <dependency>
+                <groupId>com.github.theborakompanioni</groupId>
+                <artifactId>thymeleaf-extras-shiro</artifactId>
+                <version>${thymeleaf.extras.shiro.version}</version>
+            </dependency>
+
+            <!-- 解析客户端操作系统、浏览器等 -->
+            <dependency>
+                <groupId>eu.bitwalker</groupId>
+                <artifactId>UserAgentUtils</artifactId>
+                <version>${bitwalker.version}</version>
+            </dependency>
+
+            <!-- pagehelper 分页插件 -->
+            <dependency>
+                <groupId>com.github.pagehelper</groupId>
+                <artifactId>pagehelper-spring-boot-starter</artifactId>
+                <version>${pagehelper.boot.version}</version>
+            </dependency>
+
+            <!-- 获取系统信息 -->
+            <dependency>
+                <groupId>com.github.oshi</groupId>
+                <artifactId>oshi-core</artifactId>
+                <version>${oshi.version}</version>
+            </dependency>
+
+            <!-- Swagger3依赖 -->
+            <dependency>
+                <groupId>io.springfox</groupId>
+                <artifactId>springfox-boot-starter</artifactId>
+                <version>${swagger.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>io.swagger</groupId>
+                        <artifactId>swagger-models</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- io常用工具类 -->
+            <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <version>${commons.io.version}</version>
+            </dependency>
+
+            <!-- excel工具 -->
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+
+            <!-- velocity代码生成使用模板 -->
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity-engine-core</artifactId>
+                <version>${velocity.version}</version>
+            </dependency>
+
+            <!-- 阿里JSON解析器 -->
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson.version}</version>
+            </dependency>
+
+<!--            &lt;!&ndash; 定时任务&ndash;&gt;-->
+<!--            <dependency>-->
+<!--                <groupId>com.ruoyi</groupId>-->
+<!--                <artifactId>ruoyi-quartz</artifactId>-->
+<!--                <version>${ruoyi.version}</version>-->
+<!--            </dependency>-->
+
+            <!-- 代码生成-->
+            <dependency>
+                <groupId>com.ruoyi</groupId>
+                <artifactId>ruoyi-generator</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
+            <!-- 核心模块-->
+            <dependency>
+                <groupId>com.ruoyi</groupId>
+                <artifactId>ruoyi-framework</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
+            <!-- 系统模块-->
+            <dependency>
+                <groupId>com.ruoyi</groupId>
+                <artifactId>ruoyi-system</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
+            <!-- 通用工具-->
+            <dependency>
+                <groupId>com.ruoyi</groupId>
+                <artifactId>ruoyi-common</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <modules>
+        <module>ruoyi-admin</module>
+        <module>ruoyi-framework</module>
+        <module>ruoyi-system</module>
+<!--        <module>ruoyi-quartz</module>-->
+        <module>ruoyi-generator</module>
+        <module>ruoyi-common</module>
+    </modules>
+    <packaging>pom</packaging>
+
+
+    <dependencies>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>public</id>
+            <name>aliyun nexus</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+    <pluginRepositories>
+        <pluginRepository>
+            <id>public</id>
+            <name>aliyun nexus</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </pluginRepository>
+    </pluginRepositories>
+
+</project>

+ 212 - 0
ruoyi-admin/pom.xml

@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi</artifactId>
+        <groupId>com.ruoyi</groupId>
+        <version>4.7.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>ruoyi-admin</artifactId>
+
+    <description>
+        web服务入口
+    </description>
+
+    <dependencies>
+
+        <!-- SpringBoot集成thymeleaf模板 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+
+        <!-- swagger3-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>1.6.2</version>
+        </dependency>
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-framework</artifactId>
+        </dependency>
+
+<!--        &lt;!&ndash; 定时任务&ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.ruoyi</groupId>-->
+<!--            <artifactId>ruoyi-quartz</artifactId>-->
+<!--        </dependency>-->
+
+        <!-- 代码生成-->
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-generator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>link.thingscloud</groupId>
+            <artifactId>freeswitch-esl</artifactId>
+            <version>1.3</version>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/src/main/resources/lib/freeswitch-esl-1.3.jar</systemPath>
+        </dependency>
+
+        <!-- add netty  -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.33.Final</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-transport-native-epoll</artifactId>
+            <version>4.1.33.Final</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+        <!-- add openssl  -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-tcnative-boringssl-static</artifactId>
+            <version>2.0.20.Final</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+        <!-- JWT -->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.10.3</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.5.15</version>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>   
+                <groupId>org.apache.maven.plugins</groupId>   
+                <artifactId>maven-war-plugin</artifactId>   
+                <version>3.0.0</version>   
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>   
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>3.4.2</version>
+                <configuration>
+                    <descriptors>
+                        <descriptor>src/main/assembly/assembly.xml</descriptor>
+                    </descriptors>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
+                        <phase>package</phase> <!-- bind to the packaging phase -->
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- YUI Compressor (CSS/JS压缩) 
+            <plugin>
+                <groupId>net.alchim31.maven</groupId>
+                <artifactId>yuicompressor-maven-plugin</artifactId>
+                <version>1.5.1</version>
+                <executions>
+                    <execution>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>compress</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <encoding>UTF-8</encoding>
+                    <jswarn>false</jswarn>
+                    <nosuffix>true</nosuffix>
+                    <linebreakpos>50000</linebreakpos>
+                    <sourceDirectory>src/main/resources/static</sourceDirectory>
+                    <force>true</force>
+                    <includes>
+                        <include>**/*.js</include>
+                        <include>**/*.css</include>
+                    </includes>
+                    <excludes>
+                        <exclude>**/*.min.js</exclude>
+                        <exclude>**/*.min.css</exclude>
+                        <exclude>**/fileinput.js</exclude>
+                        <exclude>**/validate/**</exclude>
+                        <exclude>**/bootstrap-table/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin> -->
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/java</directory>
+                <includes>
+                    <include>**/*.xml</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <includes>
+                    <include>**/**</include>
+                </includes>
+                <excludes>
+                    <exclude>**/start-stop-daemon</exclude>
+                </excludes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>false</filtering>
+                <includes>
+                    <include>**/start-stop-daemon</include>
+                </includes>
+            </resource>
+        </resources>
+        <finalName>easycallcenter365-gui</finalName>
+    </build>
+
+</project>

+ 69 - 0
ruoyi-admin/src/main/assembly/assembly.xml

@@ -0,0 +1,69 @@
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.1 https://maven.apache.org/xsd/assembly-2.1.1.xsd">
+    <id>release</id>
+    <formats>
+        <format>tar.gz</format>
+        <format>dir</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>target</directory>
+            <includes>
+                <include>callcenter-manager.jar</include>
+            </includes>
+            <outputDirectory>/lib</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>target/classes/bin</directory>
+            <includes>
+                <include>*.sh</include>
+            </includes>
+            <lineEnding>unix</lineEnding>
+            <fileMode>0755</fileMode>
+            <outputDirectory>/bin</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>target/classes/bin</directory>
+            <includes>
+                <include>start-stop-daemon</include>
+            </includes>
+            <fileMode>0755</fileMode>
+            <outputDirectory>/bin</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>target/classes</directory>
+            <includes>
+                <include>*.yml</include>
+                <include>*.xml</include>
+                <include>*.properties</include>
+            </includes>
+            <outputDirectory>/conf</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+            <outputDirectory>/log</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>../sql</directory>
+            <outputDirectory>/sql</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>../doc</directory>
+            <outputDirectory>/doc</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+            <outputDirectory>/pid</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+            <outputDirectory>/tmp</outputDirectory>
+        </fileSet>
+    </fileSets>
+</assembly>

+ 33 - 0
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java

@@ -0,0 +1,33 @@
+package com.ruoyi;
+
+import com.ruoyi.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动程序
+ * 
+ * @author ruoyi
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableScheduling  // 开启定时任务
+@Slf4j
+public class RuoYiApplication
+{
+    public static void main(String[] args)
+    {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(RuoYiApplication.class, args);
+
+        log.info("╔════════════════════════════════════════════════════╗");
+        log.info("║                                                    ║");
+        log.info("║easycallcenter365 start successful                  ║");
+        log.info("║                                                    ║");
+        log.info("╚════════════════════════════════════════════════════╝");
+    }
+}

+ 18 - 0
ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java

@@ -0,0 +1,18 @@
+package com.ruoyi;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * web容器中进行部署
+ * 
+ * @author ruoyi
+ */
+public class RuoYiServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(RuoYiApplication.class);
+    }
+}

+ 715 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/ApiController.java

@@ -0,0 +1,715 @@
+package com.ruoyi.aicall.controller;
+
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.aicall.domain.CcCallPhone;
+import com.ruoyi.aicall.domain.CcCallTask;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.domain.CcTtsAliyun;
+import com.ruoyi.aicall.model.*;
+import com.ruoyi.aicall.service.ICcCallPhoneService;
+import com.ruoyi.aicall.service.ICcCallTaskService;
+import com.ruoyi.aicall.service.ICcLlmAgentAccountService;
+import com.ruoyi.aicall.service.ICcTtsAliyunService;
+import com.ruoyi.aicall.utils.ClientIpCheck;
+import com.ruoyi.cc.domain.CcBizGroup;
+import com.ruoyi.cc.domain.CcGateways;
+import com.ruoyi.cc.service.ICcBizGroupService;
+import com.ruoyi.cc.service.ICcGatewaysService;
+import com.ruoyi.cc.service.ICcParamsService;
+import com.ruoyi.cc.utils.DateValidatorUtils;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.bean.BeanUtils;
+import com.ruoyi.common.utils.uuid.UuidGenerator;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.*;
+
+@Controller
+@Slf4j
+@RequestMapping("/aicall/api")
+public class ApiController extends BaseController {
+
+    @Autowired
+    private ICcCallTaskService callTaskService;
+    @Autowired
+    private ICcCallPhoneService ccCallPhoneService;
+    @Autowired
+    private ICcParamsService paramsService;
+    @Autowired
+    private ICcGatewaysService ccGatewaysService;
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+    @Autowired
+    private ICcBizGroupService ccBizGroupService;
+    @Autowired
+    private ICcCallTaskService ccCallTaskService;
+    @Autowired
+    private ICcTtsAliyunService ccTtsAliyunService;
+
+    /**
+     * 获取外呼网关列表接口
+     * @param req
+     * @return
+     */
+    @GetMapping("/gateway/list")
+    @ResponseBody
+    public AjaxResult getGatewayList(HttpServletRequest req){
+        // 校验客户端ip是否在白名单内
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单!", "");
+        }
+        // 获取外呼网关列表
+        Map<String, Object> params = new HashMap<>();
+        params.put("purposes", Arrays.asList(2,3));
+        List<CcGateways> list = ccGatewaysService.selectCcGatewaysList(new CcGateways().setParams(params));
+        List<ApiGatewaysModel> result = new ArrayList<>();
+        for (CcGateways data: list) {
+            ApiGatewaysModel model = new ApiGatewaysModel();
+            BeanUtils.copyProperties(data, model);
+            result.add(model);
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取大模型列表接口
+     * @param req
+     * @return
+     */
+    @GetMapping("/llmacount/list")
+    @ResponseBody
+    public AjaxResult getLlmAcountList(HttpServletRequest req){
+        // 校验客户端ip是否在白名单内
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单!", "");
+        }
+        // 获取大模型列表
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(new CcLlmAgentAccount());
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取音色列表
+     * @return
+     */
+    @GetMapping("/voicecode/list")
+    @ResponseBody
+    public AjaxResult getVoiceCodeList(HttpServletRequest req)
+    {
+        // 校验客户端ip是否在白名单内
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单!", "");
+        }
+        // 获取音色列表
+        List<CcTtsAliyun> list = ccTtsAliyunService.selectCcTtsAliyunList(new CcTtsAliyun());
+        JSONArray result = new JSONArray();
+        for (CcTtsAliyun ttsAliyun: list) {
+            JSONObject obj = new JSONObject();
+            obj.put("voiceName", ttsAliyun.getVoiceName());
+            obj.put("voiceCode", ttsAliyun.getVoiceCode());
+            obj.put("voiceSource", "aliyun_tts");
+            result.add(obj);
+        }
+        return AjaxResult.success(result);
+    }
+
+
+    /**
+     * 获取技能组列表
+     * @param req
+     * @return
+     */
+    @GetMapping("/busigroup/list")
+    @ResponseBody
+    public AjaxResult getBusigroupList(HttpServletRequest req){
+        // 校验客户端ip是否在白名单内
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单!", "");
+        }
+        // 获取技能组列表
+        List<CcBizGroup> list = ccBizGroupService.selectCcBizGroupList(new CcBizGroup());
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 通话记录查询接口(支持按时间、坐席、号码、呼入/呼出类型筛选)
+     * @param req
+     * @param queryParams
+     * @return
+     */
+    @PostMapping("/calltask/list")
+    @ResponseBody
+    public TableDataInfo getCallTaskList(HttpServletRequest req, @RequestBody ApiCallTaskQueryParams queryParams)
+    {
+        TableDataInfo tableDataInfo;
+        // 校验请求方ip是否合法
+        if (!ClientIpCheck.checkIp(req)) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.NO_AUTH.value());
+            tableDataInfo.setMsg("未授权,请联系系统管理员添加ip白名单!");
+            return tableDataInfo;
+        }
+        // 处理分页
+        if (null == queryParams.getPageNum()) {
+            queryParams.setPageNum(1);
+        }
+        if (null == queryParams.getPageSize()) {
+            queryParams.setPageSize(20);
+        }
+        // 校验参数
+        if (StringUtils.isNotEmpty(queryParams.getCreateTimeStart())
+                && !DateValidatorUtils.isYmdHms(queryParams.getCreateTimeStart())) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("createTimeStart格式不正确,请使用'yyyy-MM-dd HH:mm:ss'格式!");
+            return tableDataInfo;
+        }
+        if (StringUtils.isNotEmpty(queryParams.getCreateTimeEnd())
+                && !DateValidatorUtils.isYmdHms(queryParams.getCreateTimeEnd())) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.ERROR.value());
+            tableDataInfo.setMsg("createTimeEnd格式不正确,请使用'yyyy-MM-dd HH:mm:ss'格式!");
+            return tableDataInfo;
+        }
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("createTimeStart", queryParams.getCreateTimeStart());
+        params.put("createTimeEnd", queryParams.getCreateTimeEnd());
+
+        // 分页请求数据
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        CcCallTask ccCallTask = new CcCallTask();
+        ccCallTask.setBatchId(queryParams.getBatchId());
+        ccCallTask.setBatchName(queryParams.getBatchName());
+        ccCallTask.setParams(params);
+        List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(ccCallTask);
+        tableDataInfo = getDataTable(list);
+        List<CcCallTask> records = (List<CcCallTask>) tableDataInfo.getRows();
+        for (CcCallTask data: records){
+            CallTaskStatModel statModel = ccCallPhoneService.statByBatchId(data.getBatchId());
+            data.setPhoneCount(statModel.getPhoneCount());
+            data.setCallCount(statModel.getCallCount());
+            data.setNoCallCount(statModel.getPhoneCount() - statModel.getCallCount());
+            data.setConnectCount(statModel.getConnectCount());
+            data.setNoConnectCount(statModel.getCallCount() - statModel.getConnectCount());
+            if (data.getCallCount() > 0) {
+                data.setRealConnectRate(data.getConnectCount()*1.0/data.getCallCount());
+            } else {
+                data.setRealConnectRate(0.0);
+            }
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 通话记录查询接口(支持按时间、坐席、号码、呼入/呼出类型筛选)
+     * @param req
+     * @param queryParams
+     * @return
+     */
+    @PostMapping("/records/list")
+    @ResponseBody
+    public TableDataInfo getRecordsList(HttpServletRequest req, @RequestBody ApiCallRecordQueryParams queryParams)
+    {
+        TableDataInfo tableDataInfo;
+        // 校验请求方ip是否合法
+        if (!ClientIpCheck.checkIp(req)) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.NO_AUTH.value());
+            tableDataInfo.setMsg("未授权,请联系系统管理员添加ip白名单!");
+            return tableDataInfo;
+        }
+        // 分页参数处理
+        if (null == queryParams.getPageNum()) {
+            queryParams.setPageNum(1);
+        }
+        if (null == queryParams.getPageSize()) {
+            queryParams.setPageSize(20);
+        }
+        // 类型(01:呼入, 02:AI外呼, 03:人工外呼)
+        String callType = queryParams.getCallType();
+        if (StringUtils.isBlank(callType)) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("callType不能为空!");
+            return tableDataInfo;
+        }
+        // 校验参数
+        if (StringUtils.isNotEmpty(queryParams.getCalloutTimeStart())
+                && !DateValidatorUtils.isYmdHms(queryParams.getCalloutTimeStart())) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("calloutTimeStart格式不正确,请使用'yyyy-MM-dd HH:mm:ss'格式!");
+            return tableDataInfo;
+        }
+        if (StringUtils.isNotEmpty(queryParams.getCalloutTimeEnd())
+                && !DateValidatorUtils.isYmdHms(queryParams.getCalloutTimeEnd())) {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("calloutTimeStart格式不正确,请使用'yyyy-MM-dd HH:mm:ss'格式!");
+            return tableDataInfo;
+        }
+
+        if ("01".equals(callType)) {
+            // TODO
+        } else if ("02".equals(callType)) {
+            return getAiCallRecords(queryParams);
+        } else if ("03".equals(callType)) {
+            // TODO
+        } else {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("callType参数不合法,呼入请输入01,AI外呼请输入02,手工外呼请输入03!");
+            return tableDataInfo;
+        }
+        return null;
+    }
+
+    /**
+     * 创建外呼任务接口(含任务名称、优先级、并发数、TTS/音频模板等参数)接口
+     * @param req
+     * @param apiCallTaskModel
+     * @return
+     */
+    @PostMapping("/ai/createTask")
+    @ResponseBody
+    public AjaxResult createCallTask(HttpServletRequest req, @RequestBody ApiCallTaskModel apiCallTaskModel) {
+        // 校验ip白名单
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单!", "");
+        }
+        CcCallTask ccCallTask = new CcCallTask();
+        // 校验参数
+        // 任务名称不能为空
+        if (StringUtils.isBlank(apiCallTaskModel.getBatchName())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "batchName不能为空!", "");
+        }
+        // threadNum不能为空
+        if (null == apiCallTaskModel.getThreadNum()) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "threadNum不能为空!", "");
+        }
+        // 校验groupId是否存在
+        if (!checkGroupId(apiCallTaskModel.getGroupId())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "groupId参数不合法,请输入正确的groupId!", "");
+        }
+        // 校验taskType是否合法
+        if (null == apiCallTaskModel.getTaskType()) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "taskType不能为空!", "");
+        } else {
+            if (apiCallTaskModel.getTaskType() != 1
+                    && apiCallTaskModel.getTaskType() != 2) {
+                return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "taskType参数不合法,AI外呼请输入1,通知提醒请输入2!", "");
+            }
+        }
+        // 校验gatewayId是否合法
+        if (!checkGatewayId(apiCallTaskModel.getGatewayId())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "gatewayId参数不合法,请输入正确的gatewayId!", "");
+        }
+        // AI外呼需要校验llmAccountId是否合法
+        if (apiCallTaskModel.getTaskType() == 2) {
+            if (!checkLlmAccountId(apiCallTaskModel.getLlmAccountId())) {
+                return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "llmAccountId参数不合法,请输入正确的llmAccountId!", "");
+            }
+        }
+        // playTimes 如果为空,默认值给1
+        if (apiCallTaskModel.getTaskType() == 3) {
+            if (null == apiCallTaskModel.getPlayTimes()) {
+                apiCallTaskModel.setPlayTimes(1);
+            }
+        }
+        // 校验voiceCode、voiceSource是否合法
+        if (!checkVoiceCode(apiCallTaskModel.getVoiceCode(), apiCallTaskModel.getVoiceSource())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "voiceCode或voiceSource参数不合法,请输入正确的voiceCode和voiceSource!", "");
+        }
+        // 创建任务
+        BeanUtils.copyProperties(apiCallTaskModel, ccCallTask, "batchId");
+        if (null != ccCallTask.getConntectRate() && ccCallTask.getConntectRate() > 0) {
+            ccCallTask.setRate(ccCallTask.getConntectRate()/100.0);
+        }
+        ccCallTask.setCreatetime(System.currentTimeMillis());
+        ccCallTaskService.insertCcCallTask(ccCallTask);
+        apiCallTaskModel.setBatchId(ccCallTask.getBatchId());
+        return AjaxResult.success("success", apiCallTaskModel);
+    }
+
+    /**
+     * 启动任务接口
+     * @param req
+     * @param batchId
+     * @return
+     */
+    @GetMapping("/ai/startTask")
+    @ResponseBody
+    public AjaxResult startTask(HttpServletRequest req, @RequestParam("batchId") Long batchId) {
+        // 校验ip白名单
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单", "");
+        }
+        // 启动任务
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        if (null == ccCallTask) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不合法,请输入正确的batchId", "");
+        }
+        ccCallTask.setIfcall(1);
+        ccCallTask.setExecuting(0L);
+        ccCallTask.setStopTime(0L);
+        ccCallTaskService.updateCcCallTask(ccCallTask);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 停止任务接口
+     * @param req
+     * @param batchId
+     * @return
+     */
+    @GetMapping( "/ai/stopTask")
+    @ResponseBody
+    public AjaxResult stopTask(HttpServletRequest req, @RequestParam("batchId") Long batchId)
+    {
+        // 校验ip白名单
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单", "");
+        }
+        // 停止任务
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        if (null == ccCallTask) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不合法,请输入正确的batchId", "");
+        }
+        ccCallTask.setIfcall(0);
+        ccCallTask.setExecuting(0L);
+        ccCallTask.setStopTime(System.currentTimeMillis());
+        ccCallTaskService.updateCcCallTask(ccCallTask);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 追加名单(不自动启动任务)
+     * @param req
+     * @param aiCallListModel
+     * @return
+     */
+    @PostMapping("/ai/addCallList")
+    @ResponseBody
+    public AjaxResult addAiCallList(HttpServletRequest req, @RequestBody AiCallListModel aiCallListModel) {
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单", "");
+        }
+        Long batchId = aiCallListModel.getBatchId();
+        if (null == batchId) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不能为空", "");
+        }
+        if (CollectionUtils.isEmpty(aiCallListModel.getPhoneList())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数phoneList不能为空", "");
+        }
+        // 获取任务
+        CcCallTask ccCallTask = callTaskService.selectCcCallTaskByBatchId(batchId);
+        if (null == ccCallTask) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不合法,请输入正确的batchId", "");
+        }
+        if (ccCallTask.getTaskType() != 1) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不合法,请输入AI外呼任务(taskType为1)的batchId", "");
+        }
+        // 追加名单
+        Integer successCount = 0;
+        List<CcCallPhone> callPhoneList = new ArrayList<>();
+        for (String phoneNum: aiCallListModel.getPhoneList()) {
+            if (StringUtils.isBlank(phoneNum)) {
+                continue;
+            }
+            JSONObject bizJson = new JSONObject();
+            CcCallPhone callPhone = buildCcCallPhone(ccCallTask.getBatchId(), phoneNum, bizJson);
+            callPhoneList.add(callPhone);
+            successCount ++;
+            if (callPhoneList.size() >= 200) {
+                ccCallPhoneService.batchInsertCcCallPhone(callPhoneList);
+                callPhoneList = new ArrayList<>();
+            }
+        }
+        if (callPhoneList.size() > 0) {
+            ccCallPhoneService.batchInsertCcCallPhone(callPhoneList);
+        }
+
+        log.info("成功追加" + successCount + "个名单");
+        return AjaxResult.success("成功追加" + successCount + "个名单");
+    }
+
+
+    /**
+     * 追加外呼名单(不自动启动任务)
+     * @param req
+     * @param commonCallListModel
+     * @return
+     */
+    @PostMapping("/common/addCallList")
+    @ResponseBody
+    public AjaxResult addCommonCallList(HttpServletRequest req, @RequestBody CommonCallListModel commonCallListModel) {
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单", "");
+        }
+        Long batchId = commonCallListModel.getBatchId();
+        if (null == batchId) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不能为空", "");
+        }
+        if (CollectionUtils.isEmpty(commonCallListModel.getPhoneList())) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数phoneList不能为空", "");
+        }
+        // 获取任务
+        CcCallTask ccCallTask = callTaskService.selectCcCallTaskByBatchId(batchId);
+        if (null == ccCallTask) {
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "参数batchId不合法,请输入正确的batchId", "");
+        }
+        // 通知类的通知内容必填
+        if (ccCallTask.getTaskType() == 2) {
+            for (CommonPhoneModel commonPhoneModel : commonCallListModel.getPhoneList()) {
+                if (StringUtils.isBlank(commonPhoneModel.getNoticeContent())) {
+                    return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "号码" + commonPhoneModel.getPhoneNum() + "的noticeContent不能为空!", "");
+                }
+            }
+        }
+
+        // 追加名单
+        Integer successCount = 0;
+        List<CcCallPhone> callPhoneList = new ArrayList<>();
+        for (CommonPhoneModel commonPhoneModel : commonCallListModel.getPhoneList()) {
+            String phoneNum = commonPhoneModel.getPhoneNum();
+            if (StringUtils.isBlank(phoneNum)) {
+                continue;
+            }
+            CcCallPhone callPhone = buildCcCallPhone(ccCallTask.getBatchId(), phoneNum, commonPhoneModel.getBizJson());
+            callPhone.setTtsText(commonPhoneModel.getNoticeContent());
+            callPhoneList.add(callPhone);
+            successCount ++;
+            if (callPhoneList.size() >= 200) {
+                ccCallPhoneService.batchInsertCcCallPhone(callPhoneList);
+                callPhoneList = new ArrayList<>();
+            }
+        }
+        if (callPhoneList.size() > 0) {
+            ccCallPhoneService.batchInsertCcCallPhone(callPhoneList);
+        }
+        log.info("成功追加" + successCount + "个名单");
+        return AjaxResult.success("成功追加" + successCount + "个名单");
+    }
+
+
+    /**
+     * yangqiang定制的追加通知的名单
+     * @param req
+     * @param noticeCallModel
+     * @return
+     */
+    @PostMapping("/notice/call")
+    @ResponseBody
+    public AjaxResult callNotice(HttpServletRequest req, @RequestBody NoticeCallModel noticeCallModel) {
+        if (!ClientIpCheck.checkIp(req)) {
+            return AjaxResult.error(AjaxResult.Type.NO_AUTH, "未授权,请联系系统管理员添加ip白名单", "");
+        }
+        // 获取任务
+        String batchName = paramsService.getParamValueByCode("testNoticeCallTaskName", "test");
+        String phoneNum = paramsService.getParamValueByCode("testNoticeCallPhoneNum", "13908113506");
+        CcCallTask ccCallTask = callTaskService.selectCcCallTaskByBatchName(batchName, 2);
+
+        // 追加名单
+        JSONObject bizJson = new JSONObject();
+        CcCallPhone callPhone = buildCcCallPhone(ccCallTask.getBatchId(), phoneNum, bizJson);
+        callPhone.setTtsText(noticeCallModel.getNoticeContent());
+        ccCallPhoneService.insertCcCallPhone(callPhone);
+
+        // 启动任务
+        ccCallTask.setIfcall(1);
+        ccCallTask.setExecuting(0L);
+        ccCallTask.setStopTime(0L);
+        callTaskService.updateCcCallTask(ccCallTask);
+
+        return AjaxResult.success();
+    }
+
+    private CcCallPhone buildCcCallPhone(Long batchId, String phoneNum, JSONObject bizJson) {
+        CcCallPhone callPhone = new CcCallPhone();
+        callPhone.setId(UuidGenerator.GetOneUuid());
+        callPhone.setGroupId("1");
+        callPhone.setBatchId(batchId);
+        callPhone.setCreatetime(new Date().getTime());
+        callPhone.setCallstatus(0);
+        callPhone.setCalloutTime(0L);
+        callPhone.setCallcount(0);
+        callPhone.setCallEndTime(0L);
+        callPhone.setTimeLen(0L);
+        callPhone.setValidTimeLen(0L);
+        callPhone.setUuid("");
+        callPhone.setConnectedTime(0L);
+        callPhone.setHangupCause("");
+        callPhone.setAnsweredTime(0L);
+        callPhone.setDialogue("");
+        callPhone.setWavfile("");
+        callPhone.setRecordServerUrl("");
+        callPhone.setDialogueCount(0L);
+        callPhone.setAcdOpnum("");
+        callPhone.setAcdQueueTime(0L);
+        callPhone.setAcdWaitTime(0);
+        callPhone.setTelephone(phoneNum);
+        if (phoneNum.length() > 4) {
+            bizJson.put("tailNum", phoneNum.substring(phoneNum.length()-4));
+        } else {
+            bizJson.put("tailNum", phoneNum);
+        }
+        callPhone.setCustName(bizJson.getString("custName"));
+        return callPhone;
+    }
+
+
+    private TableDataInfo getAiCallRecords(ApiCallRecordQueryParams queryParams) {
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("calloutTimeStart", queryParams.getCalloutTimeStart());
+        params.put("calloutTimeEnd", queryParams.getCalloutTimeEnd());
+        if (null != queryParams.getTimeLenStart()) {
+            params.put("timeLenSecondStart", queryParams.getTimeLenStart().toString());
+        }
+        if (null != queryParams.getTimeLenEnd()) {
+            params.put("timeLenSecondEnd", queryParams.getTimeLenEnd().toString());
+        }
+
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        CcCallPhone ccCallPhone = new CcCallPhone();
+        ccCallPhone.setParams(params);
+        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneList(ccCallPhone);
+        TableDataInfo tableData = getDataTable(list);
+        List<CcCallPhone> records = (List<CcCallPhone>) tableData.getRows();
+        List<ApiCallRecordQueryResult> apiRecords = new ArrayList<>();
+        for (CcCallPhone data: records) {
+            ApiCallRecordQueryResult apiData = new ApiCallRecordQueryResult();
+            if (data.getWavfile().startsWith("/")) {
+                data.setWavfile(data.getWavfile().substring(1));
+            }
+            data.setWavfile("/recordings/files?filename=" + data.getWavfile());
+            apiData.setUuid(data.getUuid());
+            apiData.setTelephone(data.getTelephone());
+            apiData.setCalloutTime(DateUtils.format(new Date(data.getCalloutTime()), "yyyy-MM-dd HH:mm:ss"));
+            if (data.getAnsweredTime() > 0) {
+                apiData.setAnsweredTime(DateUtils.format(new Date(data.getAnsweredTime()), "yyyy-MM-dd HH:mm:ss"));
+            } else {
+                apiData.setAnsweredTime("");
+            }
+            if (data.getCallEndTime() > 0) {
+                apiData.setCallEndTime(DateUtils.format(new Date(data.getCallEndTime()), "yyyy-MM-dd HH:mm:ss"));
+            } else {
+                apiData.setCallEndTime("");
+            }
+            apiData.setHangupCause(data.getHangupCause());
+            if (data.getTimeLen() > 0) {
+                apiData.setWavFileUrl(data.getWavfile());
+            } else {
+                apiData.setWavFileUrl("");
+            }
+            if (StringUtils.isNotEmpty(data.getDialogue())) {
+                JSONArray dialogue = new JSONArray();
+                JSONArray.parseArray(data.getDialogue()).forEach(obj -> {
+                    JSONObject json = (JSONObject) obj;
+                    if ("assistant".equals(json.getString("role"))
+                            || "user".equals(json.getString("role"))) {
+                        JSONObject content = new JSONObject();
+                        content.put("role", json.getString("role"));
+                        content.put("content", json.getString("content"));
+                        dialogue.add(content);
+                    }
+                });
+                apiData.setDialogue(dialogue);
+            } else {
+                apiData.setDialogue(new JSONArray());
+            }
+            apiData.setTimeLen(Long.valueOf(data.getTimeLen()/1000).intValue());
+            apiRecords.add(apiData);
+        }
+        tableData.setRows(records);
+        TableDataInfo tableDataInfo = new TableDataInfo();
+        tableDataInfo.setCode(tableData.getCode());
+        tableDataInfo.setRows(apiRecords);
+        tableDataInfo.setTotal(tableData.getTotal());
+        tableDataInfo.setMsg(tableData.getMsg());
+        return tableDataInfo;
+    }
+
+
+    private boolean checkGroupId(String groupId) {
+        // 可以为空
+        if (StringUtils.isBlank(groupId)) {
+            return true;
+        }
+        // 如果传值了,必须是存在的值
+        CcBizGroup ccBizGroup = ccBizGroupService.selectCcBizGroupByGroupId(groupId);
+        if (null == ccBizGroup) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean checkGatewayId(Long gatewayId) {
+        // 不可以为空
+        if (null == gatewayId) {
+            return false;
+        }
+        // 必须是存在的网关,且用途是外呼或者不限制
+        CcGateways ccGateways = ccGatewaysService.selectCcGatewaysById(gatewayId);
+        if (null == ccGateways) {
+            return false;
+        }
+        // 网关用途 0 dropped; 1 phonebar; 2 outbound tasks; 3. Unlimited
+        if (ccGateways.getPurpose() != 2
+                && ccGateways.getPurpose() != 3) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean checkLlmAccountId(Integer llmAccountId) {
+        // 不能为空
+        if (null == llmAccountId) {
+            return false;
+        }
+        // 必须存在
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(llmAccountId);
+        if (null == ccLlmAgentAccount) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean checkVoiceCode(String voiceCode, String voiceSource) {
+        // 不能为空
+        if (StringUtils.isBlank(voiceCode) || StringUtils.isBlank(voiceSource)) {
+            return false;
+        }
+        // voiceSource仅支持aliyun_tts
+        if (!"aliyun_tts".equals(voiceSource)) {
+            return false;
+        }
+        // voiceCode必须是存在的
+        CcTtsAliyun ccTtsAliyun = ccTtsAliyunService.selectCcTtsAliyunByVoiceCode(voiceCode);
+        if (null == ccTtsAliyun) {
+            return false;
+        }
+        return true;
+    }
+}

+ 197 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallPhoneController.java

@@ -0,0 +1,197 @@
+package com.ruoyi.aicall.controller;
+
+import java.text.ParseException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import com.ruoyi.aicall.domain.CcCallTask;
+import com.ruoyi.aicall.model.CallPhoneExportVo;
+import com.ruoyi.aicall.service.ICcCallTaskService;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcCallPhone;
+import com.ruoyi.aicall.service.ICcCallPhoneService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 外呼号码Controller
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Controller
+@RequestMapping("/aicall/callPhone")
+public class CcCallPhoneController extends BaseController
+{
+    private String prefix = "aicall/callPhone";
+
+    @Autowired
+    private ICcCallPhoneService ccCallPhoneService;
+    @Autowired
+    private ICcCallTaskService ccCallTaskService;
+
+    @RequiresPermissions("aicall:callPhone:view")
+    @GetMapping()
+    public String callPhone()
+    {
+        return prefix + "/callPhone";
+    }
+
+    /**
+     * 查询外呼号码列表
+     */
+    @RequiresPermissions("aicall:callPhone:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcCallPhone ccCallPhone)
+    {
+        startPage();
+        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneList(ccCallPhone);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcCallPhone> records = (List<CcCallPhone>) tableDataInfo.getRows();
+        Map<Long, String> batchNameMap = new HashMap<>();
+        for (CcCallPhone data: records) {
+            if (data.getWavfile().startsWith("/")) {
+                data.setWavfile(data.getWavfile().substring(1));
+            }
+            data.setWavfile("/recordings/files?filename=" + data.getWavfile());
+            String batchName = batchNameMap.getOrDefault(data.getBatchId(), "");
+            if (StringUtils.isBlank(batchName)) {
+                CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(data.getBatchId());
+                if (null != ccCallTask) {
+                    batchName = ccCallTask.getBatchName();
+                } else {
+                    batchName = "-";
+                }
+                batchNameMap.put(data.getBatchId(), batchName);
+            }
+            data.setBatchName(batchName);
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 导出外呼号码列表
+     */
+    @RequiresPermissions("aicall:callPhone:export")
+    @Log(title = "外呼号码", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcCallPhone ccCallPhone)
+    {
+        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneList(ccCallPhone);
+        Date d0 = DateUtils.dateTime( "yyyy-MM-dd", "2025-01-01");
+        Map<Long, String> batchNameMap = new HashMap<>();
+        List<CallPhoneExportVo> dataList = new ArrayList<>();
+        for (CcCallPhone data: list) {
+            String batchName = batchNameMap.getOrDefault(data.getBatchId(), "");
+            if (StringUtils.isBlank(batchName)) {
+                CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(data.getBatchId());
+                if (null != ccCallTask) {
+                    batchName = ccCallTask.getBatchName();
+                } else {
+                    batchName = "-";
+                }
+                batchNameMap.put(data.getBatchId(), batchName);
+            }
+            data.setBatchName(batchName);
+            CallPhoneExportVo vo = new CallPhoneExportVo();
+            BeanUtils.copyProperties(data, vo);
+            vo.setCallstatusName(MessageUtils.message("callPhone.table.callstatus" + data.getCallstatus()));
+            if (data.getCalloutTime() <= d0.getTime()) {
+                vo.setCalloutTimeStr("-");
+            } else {
+                vo.setCalloutTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCalloutTime())));
+            }
+            if (data.getAnsweredTime() <= d0.getTime()) {
+                vo.setAnsweredTimeStr("-");
+            } else {
+                vo.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getAnsweredTime())));
+            }
+            if (data.getCallEndTime() <= d0.getTime()) {
+                vo.setCallEndTimeStr("-");
+            } else {
+                vo.setCallEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCallEndTime())));
+            }
+            Long timeLen = data.getTimeLen()/1000;
+            vo.setTimeLenSec(timeLen/60 + "分" + timeLen%60 + "秒" );
+            dataList.add(vo);
+        }
+        ExcelUtil<CallPhoneExportVo> util = new ExcelUtil<>(CallPhoneExportVo.class);
+        return util.exportExcel(dataList, "外呼结果数据");
+    }
+
+    /**
+     * 新增外呼号码
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存外呼号码
+     */
+    @RequiresPermissions("aicall:callPhone:add")
+    @Log(title = "外呼号码", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcCallPhone ccCallPhone)
+    {
+        return toAjax(ccCallPhoneService.insertCcCallPhone(ccCallPhone));
+    }
+
+    /**
+     * 修改外呼号码
+     */
+    @RequiresPermissions("aicall:callPhone:edit")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") String id, ModelMap mmap)
+    {
+        CcCallPhone ccCallPhone = ccCallPhoneService.selectCcCallPhoneById(id);
+        mmap.put("ccCallPhone", ccCallPhone);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存外呼号码
+     */
+    @RequiresPermissions("aicall:callPhone:edit")
+    @Log(title = "外呼号码", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcCallPhone ccCallPhone)
+    {
+        return toAjax(ccCallPhoneService.updateCcCallPhone(ccCallPhone));
+    }
+
+    /**
+     * 删除外呼号码
+     */
+    @RequiresPermissions("aicall:callPhone:remove")
+    @Log(title = "外呼号码", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccCallPhoneService.deleteCcCallPhoneByIds(ids));
+    }
+}

+ 410 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallTaskController.java

@@ -0,0 +1,410 @@
+package com.ruoyi.aicall.controller;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.util.*;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.aicall.domain.CcCallPhone;
+import com.ruoyi.aicall.domain.CcLlmAgentProvider;
+import com.ruoyi.aicall.model.CallTaskStatModel;
+import com.ruoyi.aicall.service.ICcCallPhoneService;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.ExceptionUtil;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.uuid.UuidGenerator;
+import com.ruoyi.framework.web.domain.server.Sys;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcCallTask;
+import com.ruoyi.aicall.service.ICcCallTaskService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.File;
+
+/**
+ * 外呼任务Controller
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Controller
+@RequestMapping("/aicall/callTask")
+@Slf4j
+public class CcCallTaskController extends BaseController
+{
+    private String prefix = "aicall/callTask";
+
+    @Autowired
+    private ICcCallTaskService ccCallTaskService;
+
+    @Autowired
+    private ICcCallPhoneService ccCallPhoneService;
+
+    @RequiresPermissions("aicall:callTask:view")
+    @GetMapping()
+    public String callTask()
+    {
+        return prefix + "/callTask";
+    }
+
+    /**
+     * 查询外呼任务列表
+     */
+    @RequiresPermissions("aicall:callTask:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcCallTask ccCallTask)
+    {
+        startPage();
+        List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(ccCallTask);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcCallTask> records = (List<CcCallTask>) tableDataInfo.getRows();
+        for (CcCallTask data: records){
+            CallTaskStatModel statModel = ccCallPhoneService.statByBatchId(data.getBatchId());
+            data.setPhoneCount(statModel.getPhoneCount());
+            data.setCallCount(statModel.getCallCount());
+            data.setNoCallCount(statModel.getPhoneCount() - statModel.getCallCount());
+            data.setConnectCount(statModel.getConnectCount());
+            data.setNoConnectCount(statModel.getCallCount() - statModel.getConnectCount());
+            if (data.getCallCount() > 0) {
+                data.setRealConnectRate(data.getConnectCount()*1.0/data.getCallCount());
+            } else {
+                data.setRealConnectRate(0.0);
+            }
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 导出外呼任务列表
+     */
+    @RequiresPermissions("aicall:callTask:export")
+    @Log(title = "外呼任务", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcCallTask ccCallTask)
+    {
+        List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(ccCallTask);
+        ExcelUtil<CcCallTask> util = new ExcelUtil<CcCallTask>(CcCallTask.class);
+        return util.exportExcel(list, "外呼任务数据");
+    }
+
+    /**
+     * 新增外呼任务
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:add")
+    @Log(title = "外呼任务", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcCallTask ccCallTask)
+    {
+        // 外呼速率=1/接通率
+        if (null != ccCallTask.getConntectRate() && ccCallTask.getConntectRate() > 0) {
+            ccCallTask.setRate(ccCallTask.getConntectRate()/100.0);
+        }
+        ccCallTask.setCreatetime(System.currentTimeMillis());
+        return toAjax(ccCallTaskService.insertCcCallTask(ccCallTask));
+    }
+
+    /**
+     * 修改外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:edit")
+    @GetMapping("/edit/{batchId}")
+    public String edit(@PathVariable("batchId") Long batchId, ModelMap mmap)
+    {
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        if (null != ccCallTask.getRate() && ccCallTask.getRate() > 0) {
+            ccCallTask.setConntectRate((Double.valueOf(ccCallTask.getRate()*100.0).intValue()));
+        }
+        mmap.put("ccCallTask", ccCallTask);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:edit")
+    @Log(title = "外呼任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcCallTask ccCallTask)
+    {
+        return toAjax(ccCallTaskService.updateCcCallTask(ccCallTask));
+    }
+
+    /**
+     * 删除外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:remove")
+    @Log(title = "外呼任务", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccCallTaskService.deleteCcCallTaskByBatchIds(ids));
+    }
+
+
+    /**
+     * 启动外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:start")
+    @Log(title = "启动任务", businessType = BusinessType.UPDATE)
+    @PostMapping( "/start/{batchId}")
+    @ResponseBody
+    public AjaxResult start(@PathVariable("batchId") Long batchId)
+    {
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        ccCallTask.setIfcall(1);
+        ccCallTask.setExecuting(0L);
+        ccCallTask.setStopTime(0L);
+        ccCallTaskService.updateCcCallTask(ccCallTask);
+        return toAjax(1);
+    }
+
+
+    /**
+     * 暂停外呼任务
+     */
+    @RequiresPermissions("aicall:callTask:pause")
+    @Log(title = "暂停任务", businessType = BusinessType.UPDATE)
+    @PostMapping( "/pause/{batchId}")
+    @ResponseBody
+    public AjaxResult pause(@PathVariable("batchId") Long batchId)
+    {
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        ccCallTask.setIfcall(0);
+        ccCallTask.setExecuting(0L);
+        ccCallTaskService.updateCcCallTask(ccCallTask);
+        return toAjax(1);
+    }
+
+    // 提供模板文件下载接口
+    @GetMapping("/downloadTemplate")
+    public ResponseEntity<Resource> downloadTemplate(@RequestParam (value = "taskType") Integer taskType) {
+        String templateName = "AICallList.xlsx";
+        if (null == taskType) {
+            taskType = 1;
+        } else if (taskType == 0) {
+            templateName = "PredictiveCallList.xlsx";
+        } else if (taskType == 2) {
+            templateName = "ReminderCallList.xlsx";
+        }
+        // 模板文件路径
+        String filePath = "static/templates/" + templateName; // 静态资源路径
+        ClassPathResource resource = new ClassPathResource(filePath);
+
+        // 检查文件是否存在
+        if (!resource.exists()) {
+            throw new RuntimeException("模板文件不存在!");
+        }
+
+        // 设置响应头
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + templateName);
+
+        // 返回文件
+        return ResponseEntity.ok()
+                .headers(headers)
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(resource);
+    }
+
+    /**
+     * 导入外呼任务数据
+     */
+    @RequiresPermissions("aicall:callTask:import")
+    @Log(title = "外呼任务", businessType = BusinessType.IMPORT)
+    @PostMapping("/importFile")
+    @ResponseBody
+    public AjaxResult importFile(@RequestParam("file") MultipartFile file, @RequestParam("batchId") Long batchId) throws Exception {
+        if (file == null || file.isEmpty()) {
+            return AjaxResult.error("上传文件不能为空!");
+        }
+        Long t0 = System.currentTimeMillis();
+        CcCallTask ccCallTask = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+        try (InputStream inputStream = file.getInputStream()) {
+            // 使用 Apache POI 解析 Excel 文件
+            Workbook workbook = new XSSFWorkbook(inputStream);
+            Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表
+            Integer rowCount = sheet.getLastRowNum();
+            log.info("累计数据行数:{}", rowCount);
+            List<CcCallPhone> phoneList = new ArrayList<>();
+            Map<String, Integer> phoneMap = new HashMap<>();
+            // 首行
+            Row row0 = sheet.getRow(0);
+            Integer idxCustName = -1;
+            Integer idxPhoneNum = -1;
+            Integer idxTtsText = -1;
+            for (int i = 0; i < row0.getPhysicalNumberOfCells(); i++) {
+                Cell cell = row0.getCell(i);
+                if (null != cell) {
+                    String value = getCellValue(cell);
+                    if (StringUtils.isNotEmpty(value)) {
+                        if ("客户姓名".equals(value)) {
+                            idxCustName = i;
+                        } else if ("手机号码".equals(value)) {
+                            idxPhoneNum = i;
+                        } else if ("通知内容".equals(value)) {
+                            idxTtsText = i;
+                        }
+                    }
+                }
+            }
+            // 遍历工作表中的每一行
+            Integer rowNum = 0;
+            for (int i = 1; i <= rowCount; i++) {
+                Row row = sheet.getRow(i);
+                rowNum ++;
+                String phoneNumber = getCellValue(row.getCell(idxPhoneNum)); // 手机号码列
+                String custName = "";
+                if (idxCustName >= 0) {
+                    custName = getCellValue(row.getCell(idxCustName)); // 客户姓名列
+                }
+                String ttsText = "";
+                if (idxTtsText >= 0) {
+                    ttsText = getCellValue(row.getCell(idxTtsText)); // 通知内容列
+                }
+                log.info("解析第{}行获取到的数据为,phoneNumber:{}, custName:{}", rowNum, phoneNumber, custName);
+                if (StringUtils.isNotEmpty(phoneNumber)) {
+                    phoneNumber = phoneNumber.replace(".00", "").replace(".0", "").replace("-", "").replace(" ", "");
+                    if (phoneNumber.matches("\\d+")) {
+                        if (null == phoneMap.get(phoneNumber)) {
+                            CcCallPhone callPhone = buildPhone(ccCallTask);
+                            callPhone.setTelephone(phoneNumber);
+                            callPhone.setCustName(custName);
+                            callPhone.setTtsText(ttsText);
+                            JSONObject bizJson = new JSONObject();
+                            bizJson.put("custName", custName);
+                            if (phoneNumber.length() > 4) {
+                                bizJson.put("tailNum", phoneNumber.substring(phoneNumber.length()-4));
+                            } else {
+                                bizJson.put("tailNum", phoneNumber);
+                            }
+                            callPhone.setBizJson(JSONObject.toJSONString(bizJson));
+                            phoneList.add(callPhone);
+                            phoneMap.put(phoneNumber, rowNum);
+                            // 每200条入一次库
+                            if (phoneList.size() >= 200) {
+                                ccCallPhoneService.batchInsertCcCallPhone(phoneList);
+                                phoneList = new ArrayList<>();
+                            }
+                        } else {
+                            log.info("第{}行数据“{}”与第{}行重复,排除", rowNum, phoneNumber, phoneMap.get(phoneNumber));
+                        }
+                    } else {
+                        log.info("第{}行数据“{}”不是手机号码,排除", rowNum, phoneNumber);
+                    }
+                }
+            }
+
+            // 解析后的手机号码入库
+            if (phoneList.size() > 0) {
+                ccCallPhoneService.batchInsertCcCallPhone(phoneList);
+            }
+
+            return AjaxResult.success("导入成功!共解析 " + phoneMap.keySet().size() + " 个手机号码。累计耗时" + (System.currentTimeMillis() - t0)/1000 + "秒");
+        } catch (Exception e) {
+            e.printStackTrace();
+            return AjaxResult.error("导入失败:" + e.getMessage());
+        }
+    }
+
+    private String getCellValue(Cell cell) {
+        String cellValue = "";
+        if (cell != null) {
+            // 根据单元格类型获取值
+            try {
+                switch (cell.getCellType()) {
+                    case STRING:
+                        cellValue = cell.getStringCellValue();
+                        break;
+                    case NUMERIC:
+                        // 使用 BigDecimal 避免科学计数法并去掉多余的 .00
+                        BigDecimal numericValue = new BigDecimal(cell.getNumericCellValue());
+                        cellValue = numericValue.stripTrailingZeros().toPlainString();
+                        break;
+                    default:
+                        cellValue = "";
+                }
+            } catch (Exception e) {
+                log.error("解析数据异常{}", ExceptionUtil.getExceptionMessage(e));
+            }
+        }
+        return cellValue;
+    }
+
+    private CcCallPhone buildPhone(CcCallTask ccCallTask) {
+
+        CcCallPhone callPhone = new CcCallPhone();
+        callPhone.setId(UuidGenerator.GetOneUuid());
+        callPhone.setGroupId("1");
+        callPhone.setBatchId(ccCallTask.getBatchId());
+        callPhone.setCreatetime(new Date().getTime());
+        callPhone.setCallstatus(0);
+        callPhone.setCalloutTime(0L);
+        callPhone.setCallcount(0);
+        callPhone.setCallEndTime(0L);
+        callPhone.setTimeLen(0L);
+        callPhone.setValidTimeLen(0L);
+        callPhone.setUuid("");
+        callPhone.setConnectedTime(0L);
+        callPhone.setHangupCause("");
+        callPhone.setAnsweredTime(0L);
+        callPhone.setDialogue("");
+        callPhone.setWavfile("");
+        callPhone.setRecordServerUrl("");
+//        callPhone.setBizJson("");
+        callPhone.setDialogueCount(0L);
+        callPhone.setAcdOpnum("");
+        callPhone.setAcdQueueTime(0L);
+        callPhone.setAcdWaitTime(0);
+        return callPhone;
+    }
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(new CcCallTask());
+        return AjaxResult.success(list);
+    }
+
+}

+ 170 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcInboundLlmAccountController.java

@@ -0,0 +1,170 @@
+package com.ruoyi.aicall.controller;
+
+import java.util.List;
+
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.domain.CcTtsAliyun;
+import com.ruoyi.aicall.service.ICcLlmAgentAccountService;
+import com.ruoyi.aicall.service.ICcTtsAliyunService;
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcInboundLlmAccount;
+import com.ruoyi.aicall.service.ICcInboundLlmAccountService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 呼入大模型配置Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-23
+ */
+@Controller
+@RequestMapping("/aicall/inboundllm")
+public class CcInboundLlmAccountController extends BaseController
+{
+    private String prefix = "aicall/inboundllm";
+
+    @Autowired
+    private ICcInboundLlmAccountService ccInboundLlmAccountService;
+    @Autowired
+    private ICcLlmAgentAccountService llmAgentAccountService;
+    @Autowired
+    private ICcTtsAliyunService ttsAliyunService;
+
+    @RequiresPermissions("aicall:inboundllm:view")
+    @GetMapping()
+    public String inboundllm()
+    {
+        return prefix + "/inboundllm";
+    }
+
+    /**
+     * 查询呼入大模型配置列表
+     */
+    @RequiresPermissions("aicall:inboundllm:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcInboundLlmAccount ccInboundLlmAccount)
+    {
+        startPage();
+        List<CcInboundLlmAccount> list = ccInboundLlmAccountService.selectCcInboundLlmAccountList(ccInboundLlmAccount);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcInboundLlmAccount> records = (List<CcInboundLlmAccount>) tableDataInfo.getRows();
+        for (CcInboundLlmAccount data: records) {
+            CcLlmAgentAccount llmAgentAccount = llmAgentAccountService.selectCcLlmAgentAccountById(data.getLlmAccountId());
+            if (null != llmAgentAccount) {
+                data.setLlmAccountName(llmAgentAccount.getName());
+            } else {
+                data.setLlmAccountName("");
+            }
+            CcTtsAliyun ccTtsAliyun = ttsAliyunService.selectCcTtsAliyunByVoiceCode(data.getVoiceCode());
+            if (null != ccTtsAliyun) {
+                data.setVoiceName(ccTtsAliyun.getVoiceName());
+            }
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 导出呼入大模型配置列表
+     */
+    @RequiresPermissions("aicall:inboundllm:export")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcInboundLlmAccount ccInboundLlmAccount)
+    {
+        List<CcInboundLlmAccount> list = ccInboundLlmAccountService.selectCcInboundLlmAccountList(ccInboundLlmAccount);
+        ExcelUtil<CcInboundLlmAccount> util = new ExcelUtil<CcInboundLlmAccount>(CcInboundLlmAccount.class);
+        return util.exportExcel(list, "呼入大模型配置数据");
+    }
+
+    /**
+     * 新增呼入大模型配置
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存呼入大模型配置
+     */
+    @RequiresPermissions("aicall:inboundllm:add")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcInboundLlmAccount ccInboundLlmAccount)
+    {
+        if (StringUtils.isBlank(ccInboundLlmAccount.getVoiceSource())) {
+            ccInboundLlmAccount.setVoiceSource("aliyun_tts");
+        }
+        return toAjax(ccInboundLlmAccountService.insertCcInboundLlmAccount(ccInboundLlmAccount));
+    }
+
+    /**
+     * 修改呼入大模型配置
+     */
+    @RequiresPermissions("aicall:inboundllm:edit")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcInboundLlmAccount ccInboundLlmAccount = ccInboundLlmAccountService.selectCcInboundLlmAccountById(id);
+        mmap.put("ccInboundLlmAccount", ccInboundLlmAccount);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存呼入大模型配置
+     */
+    @RequiresPermissions("aicall:inboundllm:edit")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcInboundLlmAccount ccInboundLlmAccount)
+    {
+        if (StringUtils.isBlank(ccInboundLlmAccount.getVoiceSource())) {
+            ccInboundLlmAccount.setVoiceSource("aliyun_tts");
+        }
+        return toAjax(ccInboundLlmAccountService.updateCcInboundLlmAccount(ccInboundLlmAccount));
+    }
+
+    /**
+     * 删除呼入大模型配置
+     */
+    @RequiresPermissions("aicall:inboundllm:remove")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccInboundLlmAccountService.deleteCcInboundLlmAccountByIds(ids));
+    }
+
+    @ResponseBody
+    @GetMapping("/checkCallee")
+    public AjaxResult checkCallee(@RequestParam(value = "id", required = false) Integer id, @RequestParam("callee") String callee)
+    {
+        List<CcInboundLlmAccount> ccInboundLlmAccounts = ccInboundLlmAccountService.selectCcInboundLlmAccountByCallee(callee);
+        // caller不存在校验通过
+        if (ccInboundLlmAccounts.size() <= 0) {
+            return AjaxResult.success(true);
+        }
+        // 存在但id相同,校验通过
+        if (null != id && ccInboundLlmAccounts.get(0).getId().equals(id)) {
+            return AjaxResult.success(true);
+        }
+        return AjaxResult.success(false);
+    }
+}

+ 248 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcLlmAgentAccountController.java

@@ -0,0 +1,248 @@
+package com.ruoyi.aicall.controller;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.aicall.domain.CcLlmAgentProvider;
+import com.ruoyi.aicall.llm.ILlmCapability;
+import com.ruoyi.aicall.llm.model.AccountBaseEntity;
+import com.ruoyi.cc.service.ICcParamsService;
+import com.ruoyi.cc.utils.LlmAccountParser;
+import com.ruoyi.common.utils.CommonUtils;
+import com.ruoyi.common.utils.ExceptionUtil;
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.service.ICcLlmAgentAccountService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 机器人参数配置Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/account")
+public class CcLlmAgentAccountController extends BaseController
+{
+    private String prefix = "aicall/account";
+
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+    @Autowired
+    private ICcParamsService ccParamsService;
+    private static List<String> hideKeys = Arrays.asList("apiKey", "oauthPrivateKey", "oauthPublicKeyId", "patToken");
+
+
+    @RequiresPermissions("aicall:account:view")
+    @GetMapping()
+    public String account()
+    {
+        return prefix + "/account";
+    }
+
+    /**
+     * 查询机器人参数配置列表
+     */
+    @RequiresPermissions("aicall:account:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        startPage();
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcLlmAgentAccount> records = (List<CcLlmAgentAccount>) tableDataInfo.getRows();
+        for (CcLlmAgentAccount data: records) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 导出机器人参数配置列表
+     */
+    @RequiresPermissions("aicall:account:export")
+    @Log(title = "机器人参数配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        ExcelUtil<CcLlmAgentAccount> util = new ExcelUtil<CcLlmAgentAccount>(CcLlmAgentAccount.class);
+        return util.exportExcel(list, "机器人参数配置数据");
+    }
+
+    /**
+     * 新增机器人参数配置
+     */
+    @GetMapping("/add")
+    public String add(ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = new CcLlmAgentAccount();
+        ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        ccLlmAgentAccount.setInterruptFlag(0);
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存机器人参数配置
+     */
+    @RequiresPermissions("aicall:account:add")
+    @Log(title = "机器人参数配置", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+        return toAjax(ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount));
+    }
+
+    /**
+     * 修改机器人参数配置
+     */
+    @RequiresPermissions("aicall:account:edit")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 复制机器人参数配置
+     */
+    @RequiresPermissions("aicall:account:edit")
+    @GetMapping("/copy/{id}")
+    public String copy(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        ccLlmAgentAccount.setId(-1*ccLlmAgentAccount.getId());
+        ccLlmAgentAccount.setName(ccLlmAgentAccount.getName() + "-副本");
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存机器人参数配置
+     */
+    @RequiresPermissions("aicall:account:edit")
+    @Log(title = "机器人参数配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+
+        Integer orignId = ccLlmAgentAccount.getId();
+        if (orignId < 0) {
+            orignId = orignId * -1;
+        }
+        CcLlmAgentAccount oldCcLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(orignId);
+        JSONObject oldAccountJson = JSONObject.parseObject(oldCcLlmAgentAccount.getAccountJson());
+        JSONObject newAccountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: newAccountJson.keySet()) {
+            // 是需要脱敏的key,且值包含星号,则值不更新
+            if (hideKeys.contains(key) && newAccountJson.getString(key).contains("****")) {
+                newAccountJson.put(key, oldAccountJson.getString(key));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(newAccountJson));
+
+        if (ccLlmAgentAccount.getId() > 0) {
+            return toAjax(ccLlmAgentAccountService.updateCcLlmAgentAccount(ccLlmAgentAccount));
+        } else {
+            ccLlmAgentAccount.setId(null);
+            return toAjax(ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount));
+        }
+    }
+
+    /**
+     * 删除机器人参数配置
+     */
+    @RequiresPermissions("aicall:account:remove")
+    @Log(title = "机器人参数配置", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmAgentAccountService.deleteCcLlmAgentAccountByIds(ids));
+    }
+
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(new CcLlmAgentAccount());
+        for (CcLlmAgentAccount data: list) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        return AjaxResult.success(list);
+    }
+
+}

+ 138 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcLlmAgentProviderController.java

@@ -0,0 +1,138 @@
+package com.ruoyi.aicall.controller;
+
+import java.util.List;
+
+import com.ruoyi.aicall.domain.CcTtsAliyun;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcLlmAgentProvider;
+import com.ruoyi.aicall.service.ICcLlmAgentProviderService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 大模型实现类列表Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/provider")
+public class CcLlmAgentProviderController extends BaseController
+{
+    private String prefix = "aicall/provider";
+
+    @Autowired
+    private ICcLlmAgentProviderService ccLlmAgentProviderService;
+
+    @RequiresPermissions("aicall:provider:view")
+    @GetMapping()
+    public String provider()
+    {
+        return prefix + "/provider";
+    }
+
+    /**
+     * 查询大模型实现类列表列表
+     */
+    @RequiresPermissions("aicall:provider:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        startPage();
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出大模型实现类列表列表
+     */
+    @RequiresPermissions("aicall:provider:export")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        ExcelUtil<CcLlmAgentProvider> util = new ExcelUtil<CcLlmAgentProvider>(CcLlmAgentProvider.class);
+        return util.exportExcel(list, "大模型实现类列表数据");
+    }
+
+    /**
+     * 新增大模型实现类列表
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存大模型实现类列表
+     */
+    @RequiresPermissions("aicall:provider:add")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.insertCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 修改大模型实现类列表
+     */
+    @RequiresPermissions("aicall:provider:edit")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentProvider ccLlmAgentProvider = ccLlmAgentProviderService.selectCcLlmAgentProviderById(id);
+        mmap.put("ccLlmAgentProvider", ccLlmAgentProvider);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存大模型实现类列表
+     */
+    @RequiresPermissions("aicall:provider:edit")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.updateCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 删除大模型实现类列表
+     */
+    @RequiresPermissions("aicall:provider:remove")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmAgentProviderService.deleteCcLlmAgentProviderByIds(ids));
+    }
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(new CcLlmAgentProvider());
+        return AjaxResult.success(list);
+    }
+
+}

+ 138 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcTtsAliyunController.java

@@ -0,0 +1,138 @@
+package com.ruoyi.aicall.controller;
+
+import java.util.List;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.aicall.domain.CcTtsAliyun;
+import com.ruoyi.aicall.service.ICcTtsAliyunService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 阿里云音色列Controller
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Controller
+@RequestMapping("/aicall/ttsAliyun")
+public class CcTtsAliyunController extends BaseController
+{
+    private String prefix = "aicall/ttsAliyun";
+
+    @Autowired
+    private ICcTtsAliyunService ccTtsAliyunService;
+
+    @RequiresPermissions("aicall:ttsAliyun:view")
+    @GetMapping()
+    public String ttsAliyun()
+    {
+        return prefix + "/ttsAliyun";
+    }
+
+    /**
+     * 查询阿里云音色列列表
+     */
+    @RequiresPermissions("aicall:ttsAliyun:list")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcTtsAliyun ccTtsAliyun)
+    {
+        startPage();
+        List<CcTtsAliyun> list = ccTtsAliyunService.selectCcTtsAliyunList(ccTtsAliyun);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出阿里云音色列列表
+     */
+    @RequiresPermissions("aicall:ttsAliyun:export")
+    @Log(title = "阿里云音色列", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcTtsAliyun ccTtsAliyun)
+    {
+        List<CcTtsAliyun> list = ccTtsAliyunService.selectCcTtsAliyunList(ccTtsAliyun);
+        ExcelUtil<CcTtsAliyun> util = new ExcelUtil<CcTtsAliyun>(CcTtsAliyun.class);
+        return util.exportExcel(list, "阿里云音色列数据");
+    }
+
+    /**
+     * 新增阿里云音色列
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存阿里云音色列
+     */
+    @RequiresPermissions("aicall:ttsAliyun:add")
+    @Log(title = "阿里云音色列", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcTtsAliyun ccTtsAliyun)
+    {
+        return toAjax(ccTtsAliyunService.insertCcTtsAliyun(ccTtsAliyun));
+    }
+
+    /**
+     * 修改阿里云音色列
+     */
+    @RequiresPermissions("aicall:ttsAliyun:edit")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcTtsAliyun ccTtsAliyun = ccTtsAliyunService.selectCcTtsAliyunById(id);
+        mmap.put("ccTtsAliyun", ccTtsAliyun);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存阿里云音色列
+     */
+    @RequiresPermissions("aicall:ttsAliyun:edit")
+    @Log(title = "阿里云音色列", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcTtsAliyun ccTtsAliyun)
+    {
+        return toAjax(ccTtsAliyunService.updateCcTtsAliyun(ccTtsAliyun));
+    }
+
+    /**
+     * 删除阿里云音色列
+     */
+    @RequiresPermissions("aicall:ttsAliyun:remove")
+    @Log(title = "阿里云音色列", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccTtsAliyunService.deleteCcTtsAliyunByIds(ids));
+    }
+
+    /**
+     * 查询阿里云音色列列表
+     */
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcTtsAliyun> list = ccTtsAliyunService.selectCcTtsAliyunList(new CcTtsAliyun());
+        return AjaxResult.success(list);
+    }
+}

+ 87 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/FaqController.java

@@ -0,0 +1,87 @@
+package com.ruoyi.aicall.controller;
+
+import com.ruoyi.aicall.model.Faq;
+import com.ruoyi.aicall.service.IFaqService;
+import com.ruoyi.common.core.domain.AjaxResult;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+@Controller
+@RequestMapping("/aicall/faq")
+public class FaqController {
+
+    private String prefix = "aicall/faq";
+
+    @Autowired
+    private IFaqService faqService;
+
+    @RequiresPermissions("aicall:faq:view")
+    @GetMapping()
+    public String faq()
+    {
+        return prefix + "/faq";
+    }
+
+    @GetMapping("/list")
+    @ResponseBody
+    public List<Faq> list() {
+        return faqService.list();
+    }
+
+    @GetMapping("/export")
+    @ResponseBody
+    public void export(HttpServletResponse response) throws IOException {
+        List<Faq> faqList = faqService.list();
+        StringBuilder content = new StringBuilder();
+        for (Faq faq : faqList) {
+            content.append(faq.getQuestion()).append("\n");
+            content.append(faq.getAnswer()).append("\n\n");
+        }
+        response.setContentType("application/octet-stream");
+        response.setHeader("Content-Disposition", "attachment; filename=faq.txt");
+        response.getOutputStream().write(content.toString().getBytes("UTF-8"));
+    }
+
+    @PostMapping("/import")
+    @ResponseBody
+    public void importFaq(@RequestParam("file") MultipartFile file) throws IOException {
+        List<Faq> faqList = new ArrayList<>();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"));
+        String line;
+        StringBuilder question = new StringBuilder();
+        StringBuilder answer = new StringBuilder();
+        while ((line = reader.readLine()) != null) {
+            if (line.trim().isEmpty()) {
+                if (!question.toString().isEmpty() && !answer.toString().isEmpty()) {
+                    faqList.add(new Faq(question.toString(), answer.toString()));
+                    question = new StringBuilder();
+                    answer = new StringBuilder();
+                }
+            } else if (question.toString().isEmpty()) {
+                question.append(line);
+            } else {
+                answer.append(line);
+            }
+        }
+        if (!question.toString().isEmpty() && !answer.toString().isEmpty()) {
+            faqList.add(new Faq(question.toString(), answer.toString()));
+        }
+        faqService.importFaq(faqList);
+    }
+
+    @PostMapping("/batchSave")
+    @ResponseBody
+    public AjaxResult batchSaveFaqs(@RequestBody List<Faq> faqs) {
+        faqService.saveFaqList(faqs);
+        return AjaxResult.success();
+    }
+}

+ 136 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallPhone.java

@@ -0,0 +1,136 @@
+package com.ruoyi.aicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 外呼号码对象 cc_call_phone
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Data
+public class CcCallPhone implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**  */
+    private String id;
+
+    /** 业务组 */
+    @Excel(name = "业务组")
+    private String groupId;
+
+    /** 任务批次id */
+    @Excel(name = "任务批次id")
+    private Long batchId;
+
+    /**  */
+    @Excel(name = "")
+    private String telephone;
+
+    /**  */
+    @Excel(name = "客户姓名")
+    private String custName;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间")
+    private Long createtime;
+
+    /** 0  未拨打; 1   已经进入呼叫队列;  2   正在呼叫(进行中);  3   未接通(状态: 客户正在通话中;   关机 ;  空号;  无人接听;   停机, );  4   已接通(拆分成接通后我方挂断,接通后对方挂断); 5   呼损(未触发、未弹屏、仅弹屏、己介入); 6   成功转接座席或者AI(只针对人机耦合);  7线路故障;   [ 3、4、5  是统计大类, 实际写入到数据表的值是 后面的大数字 ]     31;  客户正在通话中;  32 关机;  33 空号;  34 无人接听; 35 停机。 41 拆分成接通后我方挂断;  42 接通后对方挂断 。 51 未触发(未到达弹屏节点); 52 未弹屏(到达弹屏节点,没有弹屏出来) ;   53 仅弹屏(仅弹屏给座席但座席未介入通话,包括呼损);   54 己介入(弹屏给到座席座席介入通话) */
+    @Excel(name = "0  未拨打; 1   已经进入呼叫队列;  2   正在呼叫", readConverterExp = "进=行中")
+    private Integer callstatus;
+
+    /** 外呼时间 */
+    @Excel(name = "外呼时间")
+    private Long calloutTime;
+
+    /** 呼叫次数 */
+    @Excel(name = "呼叫次数")
+    private Integer callcount;
+
+    /** 呼叫结束时间 */
+    @Excel(name = "呼叫结束时间")
+    private Long callEndTime;
+
+    /** 通话时长; 秒; */
+    @Excel(name = "通话时长; 秒;")
+    private Long timeLen;
+
+    /** 有效通话时长; 秒 */
+    @Excel(name = "有效通话时长; 秒")
+    private Long validTimeLen;
+
+    /** 通话唯一标志 */
+    @Excel(name = "通话唯一标志")
+    private String uuid;
+
+    /** 通话接通时间 */
+    @Excel(name = "通话接通时间")
+    private Long connectedTime;
+
+    /** 挂机原因 */
+    @Excel(name = "挂机原因")
+    private String hangupCause;
+
+    /** 电话应答时间 */
+    @Excel(name = "电话应答时间")
+    private Long answeredTime;
+
+    /** 对话内容 */
+    @Excel(name = "对话内容")
+    private String dialogue;
+
+    /** 全程通话录音文件名 */
+    @Excel(name = "全程通话录音文件名")
+    private String wavfile;
+
+    /** 录音文件路径前缀 */
+    @Excel(name = "录音文件路径前缀")
+    private String recordServerUrl;
+
+    /** 业务json数据 */
+    @Excel(name = "业务json数据")
+    private String bizJson;
+
+    /** 交互轮次(一问一答算一轮交互) */
+    @Excel(name = "交互轮次", readConverterExp = "一=问一答算一轮交互")
+    private Long dialogueCount;
+
+
+    /** 人工坐席工号 */
+    @Excel(name = "人工坐席工号", readConverterExp = "人工坐席工号")
+    private String acdOpnum;
+
+    /** 加入转人工排队的时间;  */
+    @Excel(name = "加入转人工排队的时间; ", readConverterExp = "加入转人工排队的时间; ")
+    private Long acdQueueTime;
+
+    /** 人工排队等待时长,秒 */
+    @Excel(name = "人工排队等待时长,秒", readConverterExp = "人工排队等待时长,秒")
+    private Integer acdWaitTime;
+
+    /** tts text for voice call notification. */
+    @Excel(name = "tts text for voice call notification.", readConverterExp = "tts text for voice call notification.")
+    private String ttsText;
+
+    /** 客户意向 */
+    @Excel(name = "客户意向", readConverterExp = "客户意向")
+    private String intent;
+
+    /** 请求参数 */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params = new HashMap<>();
+
+    /** 批次名称 */
+    @Excel(name = "批次名称", readConverterExp = "批次名称")
+    private String batchName;
+
+}

+ 129 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallTask.java

@@ -0,0 +1,129 @@
+package com.ruoyi.aicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 外呼任务对象 cc_call_task
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Data
+public class CcCallTask implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**  */
+    private Long batchId;
+
+    /** 外呼任务的业务组 */
+    @Excel(name = "外呼任务的业务组")
+    private String groupId;
+
+    /**  */
+    @Excel(name = "")
+    private String batchName;
+
+    /** 是否启动任务, 1 启动, 0 停止 */
+    @Excel(name = "是否启动任务, 1 启动, 0 停止")
+    private Integer ifcall;
+
+    /** 外呼速率 */
+    @Excel(name = "外呼速率")
+    private Double rate;
+
+    /** 当前任务最大可用外线数 */
+    @Excel(name = "当前任务最大可用外线数")
+    private Long threadNum;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间")
+    private Long createtime;
+
+    /** 任务是否正在执行; */
+    @Excel(name = "任务是否正在执行;")
+    private Long executing;
+
+    /** 任务停止时间 */
+    @Excel(name = "任务停止时间")
+    private Long stopTime;
+
+    /** 任务创建者用户id */
+    @Excel(name = "任务创建者用户id")
+    private String userid;
+
+    /** 0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification. */
+    @Excel(name = "0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification.")
+    private Integer taskType;
+
+    /** 使用哪条线路外呼 */
+    @Excel(name = "使用哪条线路外呼")
+    private Long gatewayId;
+
+    /** 音色 */
+    @Excel(name = "音色")
+    private String voiceCode;
+
+    /** 音源 */
+    @Excel(name = "音源")
+    private String voiceSource;
+
+    /** The average ringing duration of the call; seconds */
+    @Excel(name = "The average ringing duration of the call; seconds")
+    private Double avgRingTimeLen;
+
+    /** The average pure call duration per call; seconds */
+    @Excel(name = "The average pure call duration per call; seconds")
+    private Double avgCallTalkTimeLen;
+
+    /** The duration of form filling after the call ends; seconds */
+    @Excel(name = "The duration of form filling after the call ends; seconds")
+    private Double avgCallEndProcessTimeLen;
+
+    /** 外呼节点 */
+    @Excel(name = "外呼节点")
+    private String callNodeNo;
+
+    /** 大模型底座账号的Id */
+    @Excel(name = "大模型底座账号的Id")
+    private Integer llmAccountId;
+
+    /** 播放次数 */
+    @Excel(name = "播放次数")
+    private Integer playTimes;
+
+    /** 请求参数 */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params = new HashMap<>();
+
+    /** 总名单量 */
+    private Integer phoneCount;
+
+    /** 未拨打名单量 */
+    private Integer noCallCount;
+
+    /** 已拨打名单量 */
+    private Integer callCount;
+
+    /** 接通名单量 */
+    private Integer connectCount;
+
+    /** 未接通名单量 */
+    private Integer noConnectCount;
+
+    /** 实际接通率 */
+    private Double realConnectRate;
+
+    /** 预估接通率 (百分数格式)*/
+    @Excel(name = "预估接通率")
+    private Integer conntectRate;
+
+}

+ 53 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcInboundLlmAccount.java

@@ -0,0 +1,53 @@
+package com.ruoyi.aicall.domain;
+
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+
+/**
+ * 呼入大模型配置对象 cc_inbound_llm_account
+ * 
+ * @author ruoyi
+ * @date 2025-06-23
+ */
+@Data
+public class CcInboundLlmAccount implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 大模型底座id */
+    @Excel(name = "大模型底座id")
+    private Integer llmAccountId;
+
+    /** 被叫号码 */
+    @Excel(name = "被叫号码")
+    private String callee;
+
+    /** TTS voice code for the robot */
+    @Excel(name = "TTS voice code for the robot")
+    private String voiceCode;
+
+    /** tts provider */
+    @Excel(name = "tts provider")
+    private String voiceSource;
+
+
+    /************  以下不是表结构字段 ************/
+    /** 大模型底座名称 */
+    private String llmAccountName;
+
+    /** TTS voice for the robot */
+    private String voiceName;
+
+    private String serviceType;
+
+    /** TTS voice for the robot */
+    private Integer groupId;
+
+}

+ 60 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcLlmAgentAccount.java

@@ -0,0 +1,60 @@
+package com.ruoyi.aicall.domain;
+
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+
+/**
+ * 机器人参数配置对象 cc_llm_agent_account
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Data
+public class CcLlmAgentAccount implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 机器人名称 */
+    @Excel(name = "机器人名称")
+    private String name;
+
+    /** 机器人配置信息 */
+    @Excel(name = "机器人配置信息")
+    private String accountJson;
+
+    /** Entity class for storing account config info. */
+    @Excel(name = "Entity class for storing account config info.")
+    private String accountEntity;
+
+    /** 实现类 */
+    @Excel(name = "实现类")
+    private String providerClassName;
+
+    /** 是否打断(1:是,0:否) */
+    @Excel(name = "是否打断(1:是,0:否)")
+    private Integer interruptFlag;
+
+    /** 打断关键词列表 */
+    @Excel(name = "打断关键词列表")
+    private String interruptKeywords;
+
+    /** 打断忽略关键字列表 */
+    @Excel(name = "打断忽略关键字列表")
+    private String interruptIgnoreKeywords;
+
+    /** 客户意向提示词 */
+    @Excel(name = "客户意向提示词")
+    private String intentionTips;
+
+    /** 模型并发数 */
+    @Excel(name = "模型并发数")
+    private Integer concurrentNum;
+
+}

+ 32 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcLlmAgentProvider.java

@@ -0,0 +1,32 @@
+package com.ruoyi.aicall.domain;
+
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+
+/**
+ * 大模型实现类列表对象 cc_llm_agent_provider
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Data
+public class CcLlmAgentProvider implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 实现类 */
+    @Excel(name = "实现类")
+    private String providerClassName;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String note;
+
+}

+ 38 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcTtsAliyun.java

@@ -0,0 +1,38 @@
+package com.ruoyi.aicall.domain;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.io.Serializable;
+
+/**
+ * 阿里云音色列对象 cc_tts_aliyun
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Data
+@Accessors(chain = true)
+public class CcTtsAliyun implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** tts发音人名称 */
+    @Excel(name = "tts发音人名称")
+    private String voiceName;
+
+    /** tts发音人代码 */
+    @Excel(name = "tts发音人代码")
+    private String voiceCode;
+
+    /** 是否启用 */
+    @Excel(name = "是否启用")
+    private Integer voiceEnabled;
+
+}

+ 30 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/AbstractLlmCapability.java

@@ -0,0 +1,30 @@
+package com.ruoyi.aicall.llm;
+
+import com.alibaba.fastjson.JSONArray;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.model.AccountBaseEntity;
+import okhttp3.OkHttpClient;
+
+import java.util.concurrent.TimeUnit;
+
+public abstract class AbstractLlmCapability implements ILlmCapability {
+
+    protected AccountBaseEntity llmAccountInfo;
+
+    protected static final OkHttpClient CLIENT =  new OkHttpClient.Builder()
+            .connectTimeout(90, TimeUnit.SECONDS)
+            .readTimeout(90, TimeUnit.SECONDS)
+            .build();
+
+    @Override
+    public  void setAccount(AccountBaseEntity llmAccount){
+        this.llmAccountInfo = llmAccount;
+    }
+    @Override
+    public AccountBaseEntity getAccount(){
+        return llmAccountInfo;
+    }
+
+    @Override
+    public abstract String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents);
+}

+ 15 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/ILlmCapability.java

@@ -0,0 +1,15 @@
+package com.ruoyi.aicall.llm;
+
+import com.alibaba.fastjson.JSONArray;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.model.AccountBaseEntity;
+
+public interface ILlmCapability {
+
+    String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents);
+
+    void setAccount(AccountBaseEntity llmAccount);
+
+    AccountBaseEntity getAccount();
+
+}

+ 15 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/Coze.java

@@ -0,0 +1,15 @@
+package com.ruoyi.aicall.llm.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.AbstractLlmCapability;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class Coze extends AbstractLlmCapability {
+    @Override
+    public String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents) {
+        String intentionTips = ccLlmAgentAccount.getIntentionTips();
+        return null;
+    }
+}

+ 67 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/DeepSeekChat.java

@@ -0,0 +1,67 @@
+package com.ruoyi.aicall.llm.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.AbstractLlmCapability;
+import com.ruoyi.aicall.llm.model.LlmAccount;
+import com.ruoyi.common.utils.ExceptionUtil;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import okio.BufferedSource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class DeepSeekChat extends AbstractLlmCapability {
+    @Override
+    public String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents) {
+        try {
+            log.info("进入DeepSeekChat处理逻辑");
+            // 构造请求体
+            JSONObject params = new JSONObject();
+            params.put("model", ((LlmAccount)getAccount()).getModelName());
+            params.put("enable_thinking", false);
+            JSONArray messages = new JSONArray();
+            JSONObject message1 = new JSONObject();
+            message1.put("role", "system");
+            message1.put("content", ccLlmAgentAccount.getIntentionTips());
+            messages.add(message1);
+            JSONObject message2 = new JSONObject();
+            message2.put("role", "user");
+            message2.put("content", JSONObject.toJSONString(dialogueContents));
+            messages.add(message2);
+            params.put("messages", messages);
+
+            RequestBody requestBody = FormBody.create(MediaType.parse("application/json; charset=utf-8"), JSONObject.toJSONString(params));
+            Request request = new Request.Builder()
+                    .post(requestBody)
+                    .header("Authorization", "Bearer " + ((LlmAccount)getAccount()).getApiKey())
+                    .header("Content-Type", "application/json")
+                    .url(getAccount().serverUrl).build();
+
+            Response response = null;
+            try {
+                response = CLIENT.newCall(request).execute();
+                String responseBody = response.body().string();
+                log.info("Response: " + responseBody);
+                if (response.isSuccessful() && response.body() != null) {
+                    return JSONObject.parseObject(responseBody).getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content");
+                } else {
+                    log.error("Request failed: " + response.code());
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            return "";
+
+        } catch (Exception e) {
+            log.error("DeepSeekChat处理客户意向异常" + ExceptionUtil.getExceptionMessage(e));
+        }
+        return null;
+    }
+}

+ 15 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/Dify.java

@@ -0,0 +1,15 @@
+package com.ruoyi.aicall.llm.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.AbstractLlmCapability;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class Dify extends AbstractLlmCapability {
+    @Override
+    public String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents) {
+        String intentionTips = ccLlmAgentAccount.getIntentionTips();
+        return null;
+    }
+}

+ 15 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/impl/MaxKB.java

@@ -0,0 +1,15 @@
+package com.ruoyi.aicall.llm.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+import com.ruoyi.aicall.llm.AbstractLlmCapability;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MaxKB extends AbstractLlmCapability {
+    @Override
+    public String getIntentionByTips(CcLlmAgentAccount ccLlmAgentAccount, JSONArray dialogueContents) {
+        String intentionTips = ccLlmAgentAccount.getIntentionTips();
+        return null;
+    }
+}

+ 49 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/AccountBaseEntity.java

@@ -0,0 +1,49 @@
+package com.ruoyi.aicall.llm.model;
+
+ public class AccountBaseEntity {
+
+    public String serverUrl;
+
+    public String provider;
+
+    /**
+     * The voice prompt played when  transferring to agent tips string
+     */
+    public String transferToAgentTips;
+
+    /**
+     * The voice prompt played when call session hangup
+     */
+    public String hangupTips;
+
+    /**
+     * tips for No Voice
+     */
+    public String customerNoVoiceTips;
+
+    /**
+     * The opening remarks of a phone call
+     */
+    public String openingRemarks;
+
+
+    public String voiceSource;
+
+    public String voiceCode;
+
+    /**
+     *   voice interruption supported during the robot's speech:
+     *   1:yes, 0:no
+     */
+    public int interruptFlag;
+
+    /**
+     *  List of keywords that support speech interruption.
+     */
+    public String interruptKeywords;
+
+    /**
+     *  List of keywords excluded from triggering speech interruption.
+     */
+    public String interruptIgnoreKeywords;
+}

+ 22 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/CozeAccount.java

@@ -0,0 +1,22 @@
+package com.ruoyi.aicall.llm.model;
+
+import lombok.Data;
+
+@Data
+public class CozeAccount extends  AccountBaseEntity {
+
+    private String botId;
+
+    /**
+     *  available tokenType enums:
+     *  oauth、pat
+     */
+    private String tokenType;
+
+    private String patToken;
+
+    private String oauthClientId;
+    private String oauthPrivateKey;
+    private String oauthPublicKeyId;
+
+}

+ 21 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/llm/model/LlmAccount.java

@@ -0,0 +1,21 @@
+package com.ruoyi.aicall.llm.model;
+
+import lombok.Data;
+
+@Data
+public class LlmAccount extends AccountBaseEntity {
+
+  private String apiKey;
+
+  private String modelName;
+
+    /**
+     *  Large Language model prompt
+     */
+  private String llmTips;
+
+    /**
+     *  Business knowledge used as context
+     */
+  private String faqContext;
+}

+ 86 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcCallPhoneMapper.java

@@ -0,0 +1,86 @@
+package com.ruoyi.aicall.mapper;
+
+import com.ruoyi.aicall.domain.CcCallPhone;
+import com.ruoyi.aicall.model.CallTaskStatModel;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 外呼号码Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+public interface CcCallPhoneMapper 
+{
+    /**
+     * 查询外呼号码
+     * 
+     * @param id 外呼号码主键
+     * @return 外呼号码
+     */
+    public CcCallPhone selectCcCallPhoneById(String id);
+
+    /**
+     * 查询外呼号码列表
+     * 
+     * @param ccCallPhone 外呼号码
+     * @return 外呼号码集合
+     */
+    public List<CcCallPhone> selectCcCallPhoneList(CcCallPhone ccCallPhone);
+
+    /**
+     * 新增外呼号码
+     * 
+     * @param ccCallPhone 外呼号码
+     * @return 结果
+     */
+    public int insertCcCallPhone(CcCallPhone ccCallPhone);
+
+    /**
+     * 修改外呼号码
+     * 
+     * @param ccCallPhone 外呼号码
+     * @return 结果
+     */
+    public int updateCcCallPhone(CcCallPhone ccCallPhone);
+
+    /**
+     * 删除外呼号码
+     * 
+     * @param id 外呼号码主键
+     * @return 结果
+     */
+    public int deleteCcCallPhoneById(String id);
+
+    /**
+     * 批量删除外呼号码
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcCallPhoneByIds(String[] ids);
+
+    /**
+     * 根据batchId统计外呼数据
+     * @param batchId
+     * @return
+     */
+    CallTaskStatModel statByBatchId(Long batchId);
+
+    /**
+     * 批量入库
+     * @param phoneList
+     */
+    void batchInsertCcCallPhone(List<CcCallPhone> phoneList);
+
+    /**
+     * 获取所有的已挂机未跑批客户意向的通话记录
+     * @return
+     */
+    List<CcCallPhone> getCustIntentionList();
+
+    int updateIntentionByIds(@Param("intention") String intention, @Param("phoneIds") List<String> phoneIds);
+}

+ 70 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcCallTaskMapper.java

@@ -0,0 +1,70 @@
+package com.ruoyi.aicall.mapper;
+
+import com.ruoyi.aicall.domain.CcCallTask;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 外呼任务Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+public interface CcCallTaskMapper 
+{
+    /**
+     * 查询外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 外呼任务
+     */
+    public CcCallTask selectCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 查询外呼任务列表
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 外呼任务集合
+     */
+    public List<CcCallTask> selectCcCallTaskList(CcCallTask ccCallTask);
+
+    /**
+     * 新增外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    public int insertCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 修改外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    public int updateCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 删除外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 结果
+     */
+    public int deleteCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 批量删除外呼任务
+     * 
+     * @param batchIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcCallTaskByBatchIds(String[] batchIds);
+
+    /**
+     * 根据任务名称获取任务
+     * @param batchName
+     * @return
+     */
+    CcCallTask selectCcCallTaskByBatchName(@Param("batchName") String batchName, @Param("taskType") Integer taskType);
+}

+ 63 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcInboundLlmAccountMapper.java

@@ -0,0 +1,63 @@
+package com.ruoyi.aicall.mapper;
+
+import java.util.List;
+import com.ruoyi.aicall.domain.CcInboundLlmAccount;
+
+/**
+ * 呼入大模型配置Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-23
+ */
+public interface CcInboundLlmAccountMapper 
+{
+    /**
+     * 查询呼入大模型配置
+     * 
+     * @param id 呼入大模型配置主键
+     * @return 呼入大模型配置
+     */
+    public CcInboundLlmAccount selectCcInboundLlmAccountById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     * 
+     * @param ccInboundLlmAccount 呼入大模型配置
+     * @return 呼入大模型配置集合
+     */
+    public List<CcInboundLlmAccount> selectCcInboundLlmAccountList(CcInboundLlmAccount ccInboundLlmAccount);
+
+    /**
+     * 新增呼入大模型配置
+     * 
+     * @param ccInboundLlmAccount 呼入大模型配置
+     * @return 结果
+     */
+    public int insertCcInboundLlmAccount(CcInboundLlmAccount ccInboundLlmAccount);
+
+    /**
+     * 修改呼入大模型配置
+     * 
+     * @param ccInboundLlmAccount 呼入大模型配置
+     * @return 结果
+     */
+    public int updateCcInboundLlmAccount(CcInboundLlmAccount ccInboundLlmAccount);
+
+    /**
+     * 删除呼入大模型配置
+     * 
+     * @param id 呼入大模型配置主键
+     * @return 结果
+     */
+    public int deleteCcInboundLlmAccountById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcInboundLlmAccountByIds(String[] ids);
+
+    List<CcInboundLlmAccount> selectCcInboundLlmAccountByCallee(String callee);
+}

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcLlmAgentAccountMapper.java

@@ -0,0 +1,61 @@
+package com.ruoyi.aicall.mapper;
+
+import java.util.List;
+import com.ruoyi.aicall.domain.CcLlmAgentAccount;
+
+/**
+ * 机器人参数配置Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface CcLlmAgentAccountMapper 
+{
+    /**
+     * 查询机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 机器人参数配置
+     */
+    public CcLlmAgentAccount selectCcLlmAgentAccountById(Integer id);
+
+    /**
+     * 查询机器人参数配置列表
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 机器人参数配置集合
+     */
+    public List<CcLlmAgentAccount> selectCcLlmAgentAccountList(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 新增机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    public int insertCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 修改机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    public int updateCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 删除机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 结果
+     */
+    public int deleteCcLlmAgentAccountById(Integer id);
+
+    /**
+     * 批量删除机器人参数配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmAgentAccountByIds(String[] ids);
+}

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcLlmAgentProviderMapper.java

@@ -0,0 +1,61 @@
+package com.ruoyi.aicall.mapper;
+
+import java.util.List;
+import com.ruoyi.aicall.domain.CcLlmAgentProvider;
+
+/**
+ * 大模型实现类列表Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface CcLlmAgentProviderMapper 
+{
+    /**
+     * 查询大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 大模型实现类列表
+     */
+    public CcLlmAgentProvider selectCcLlmAgentProviderById(Integer id);
+
+    /**
+     * 查询大模型实现类列表列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 大模型实现类列表集合
+     */
+    public List<CcLlmAgentProvider> selectCcLlmAgentProviderList(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 新增大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    public int insertCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 修改大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    public int updateCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 删除大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 结果
+     */
+    public int deleteCcLlmAgentProviderById(Integer id);
+
+    /**
+     * 批量删除大模型实现类列表
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmAgentProviderByIds(String[] ids);
+}

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcTtsAliyunMapper.java

@@ -0,0 +1,61 @@
+package com.ruoyi.aicall.mapper;
+
+import java.util.List;
+import com.ruoyi.aicall.domain.CcTtsAliyun;
+
+/**
+ * 阿里云音色列Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+public interface CcTtsAliyunMapper 
+{
+    /**
+     * 查询阿里云音色列
+     * 
+     * @param id 阿里云音色列主键
+     * @return 阿里云音色列
+     */
+    public CcTtsAliyun selectCcTtsAliyunById(Integer id);
+
+    /**
+     * 查询阿里云音色列列表
+     * 
+     * @param ccTtsAliyun 阿里云音色列
+     * @return 阿里云音色列集合
+     */
+    public List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 新增阿里云音色列
+     * 
+     * @param ccTtsAliyun 阿里云音色列
+     * @return 结果
+     */
+    public int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 修改阿里云音色列
+     * 
+     * @param ccTtsAliyun 阿里云音色列
+     * @return 结果
+     */
+    public int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 删除阿里云音色列
+     * 
+     * @param id 阿里云音色列主键
+     * @return 结果
+     */
+    public int deleteCcTtsAliyunById(Integer id);
+
+    /**
+     * 批量删除阿里云音色列
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCcTtsAliyunByIds(String[] ids);
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini