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.domain.CcTtsAliyun; import com.ruoyi.aicall.model.CallTaskStatModel; import com.ruoyi.aicall.service.ICcCallPhoneService; import com.ruoyi.aicall.service.ICcTtsAliyunService; import com.ruoyi.cc.service.ICcParamsService; 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.transaction.annotation.Transactional; 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; @Autowired private ICcTtsAliyunService ttsAliyunService; @Autowired private ICcParamsService paramsService; @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 list = ccCallTaskService.selectCcCallTaskList(ccCallTask); TableDataInfo tableDataInfo = getDataTable(list); List records = (List) tableDataInfo.getRows(); Integer allDelDays = Integer.valueOf(paramsService.getParamValueByCode("callTask_allowDel_days", "30")); 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); } // if (StringUtils.isNotEmpty(data.getVoiceCode())) { // CcTtsAliyun ccTtsAliyun = ttsAliyunService.selectCcTtsAliyunByVoiceCode(data.getVoiceCode()); // if (null != ccTtsAliyun) { // data.setVoiceSource(ccTtsAliyun.getVoiceSource()); // data.setProvider(ccTtsAliyun.getProvider()); // } // } data.setAllowDel(0); if (data.getIfcall() == 0 && data.getStopTime() > 0 && (System.currentTimeMillis() - allDelDays * 24*3600*1000L) > data.getStopTime()) { data.setAllowDel(1); } } tableDataInfo.setRows(records); return tableDataInfo; } /** * 导出外呼任务列表 */ @RequiresPermissions("aicall:callTask:export") @Log(title = "外呼任务", businessType = BusinessType.EXPORT) @PostMapping("/export") @ResponseBody public AjaxResult export(CcCallTask ccCallTask) { List list = ccCallTaskService.selectCcCallTaskList(ccCallTask); ExcelUtil util = new ExcelUtil(CcCallTask.class); return util.exportExcel(list, "外呼任务数据"); } /** * 新增外呼任务 */ @GetMapping("/add") public String add(ModelMap mmap) { mmap.put("ccCallTask", new CcCallTask()); 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()); if ("acd".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferData(ccCallTask.getAiTransferGroupId()); } else if ("extension".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferData(ccCallTask.getAiTransferExtNumber()); } else if ("gateway".equals(ccCallTask.getAiTransferType())) { JSONObject aiTransferData = new JSONObject(); aiTransferData.put("gatewayId", ccCallTask.getAiTransferGatewayId()); aiTransferData.put("destNumber", ccCallTask.getAiTransferGatewayDestNumber()); ccCallTask.setAiTransferData(JSONObject.toJSONString(aiTransferData)); } 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())); } // if (StringUtils.isNotEmpty(ccCallTask.getVoiceCode())) { // CcTtsAliyun ccTtsAliyun = ttsAliyunService.selectCcTtsAliyunByVoiceCode(ccCallTask.getVoiceCode()); // if (null != ccTtsAliyun) { // ccCallTask.setVoiceSource(ccTtsAliyun.getVoiceSource()); // ccCallTask.setProvider(ccTtsAliyun.getProvider()); // } // } if ("acd".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferGroupId(ccCallTask.getAiTransferData()); } else if ("extension".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferExtNumber(ccCallTask.getAiTransferData()); } else if ("gateway".equals(ccCallTask.getAiTransferType())) { if (StringUtils.isNotEmpty(ccCallTask.getAiTransferData())) { JSONObject aiTransferData = JSONObject.parseObject(ccCallTask.getAiTransferData()); ccCallTask.setAiTransferGatewayId(aiTransferData.getString("gatewayId")); ccCallTask.setAiTransferGatewayDestNumber(aiTransferData.getString("destNumber")); } } mmap.put("ccCallTask", ccCallTask); return prefix + "/edit"; } /** * 修改保存外呼任务 */ @RequiresPermissions("aicall:callTask:edit") @Log(title = "外呼任务", businessType = BusinessType.UPDATE) @PostMapping("/edit") @ResponseBody public AjaxResult editSave(CcCallTask ccCallTask) { if ("acd".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferData(ccCallTask.getAiTransferGroupId()); } else if ("extension".equals(ccCallTask.getAiTransferType())) { ccCallTask.setAiTransferData(ccCallTask.getAiTransferExtNumber()); } else if ("gateway".equals(ccCallTask.getAiTransferType())) { JSONObject aiTransferData = new JSONObject(); aiTransferData.put("gatewayId", ccCallTask.getAiTransferGatewayId()); aiTransferData.put("destNumber", ccCallTask.getAiTransferGatewayDestNumber()); ccCallTask.setAiTransferData(JSONObject.toJSONString(aiTransferData)); } return toAjax(ccCallTaskService.updateCcCallTask(ccCallTask)); } /** * 删除外呼任务 */ @RequiresPermissions("aicall:callTask:remove") @Log(title = "外呼任务", businessType = BusinessType.DELETE) @PostMapping( "/remove") @ResponseBody @Transactional public AjaxResult remove(String ids) { Long batchId = Long.valueOf(ids); // 备份拨打记录数据 ccCallPhoneService.bakCallPhoneByBatchId(batchId); // 删除拨打记录数据 ccCallPhoneService.delCallPhoneByBatchId(batchId); // 备份任务数据 ccCallTaskService.bakCallTaskByBatchId(batchId); // 删除任务数据 return toAjax(ccCallTaskService.deleteCcCallTaskByBatchId(batchId)); } /** * 启动外呼任务 */ @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); // 如果小于5分钟,则判断是否有未挂机通话 if ((System.currentTimeMillis() - ccCallTask.getStopTime()) <= 5*60*1000L) { // 如果有未挂机通话,则不允许启动 List noHangupCalls = ccCallPhoneService.selectNoHangupCalls(batchId); if (noHangupCalls.size() > 0) { log.info("还有没挂机的通话,资源还未释放,无法启动"); return AjaxResult.error("当前任务资源还未释放完成,无法启动,请2分钟后重试!"); } // 有挂机不超过30秒的通话,则不允许启动 Map params = new HashMap<>(); params.put("callEndTimeStart", DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(System.currentTimeMillis() - 30*1000L))); List ccCallPhoneList = ccCallPhoneService.selectCcCallPhoneList(new CcCallPhone().setBatchId(batchId).setParams(params)); if (ccCallPhoneList.size() > 0) { log.info("最后一通电话挂机不超过30秒,资源还未释放,无法启动"); return AjaxResult.error("当前任务资源还未释放完成,无法启动,请2分钟后重试!"); } } 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); ccCallTask.setStopTime(System.currentTimeMillis()); ccCallTaskService.updateCcCallTask(ccCallTask); return toAjax(1); } // 提供模板文件下载接口 @GetMapping("/downloadTemplate") public ResponseEntity 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); JSONObject ttsContentVariables = JSONObject.parseObject(paramsService.getParamValueByCode("tts_content_variables", "{}")); 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 phoneList = new ArrayList<>(); Map phoneMap = new HashMap<>(); // 首行 Row row0 = sheet.getRow(0); Integer idxCustName = -1; Integer idxPhoneNum = -1; Integer idxTtsText = -1; Map idxMap = new HashMap<>(); 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; } else { for (String var: ttsContentVariables.keySet()) { if (ttsContentVariables.getString(var).equals(value)) { idxMap.put(i, var); } } } } } } // 遍历工作表中的每一行 Integer rowNum = 0; for (int i = 1; i <= rowCount; i++) { Row row = sheet.getRow(i); rowNum ++; JSONObject bizJson = new JSONObject(); // 解析扩展字段 for (Integer idx: idxMap.keySet()) { bizJson.put(idxMap.getOrDefault(idx, ""), getCellValue(row.getCell(idx))); } 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:{}, bizJson:{}", rowNum, phoneNumber, custName, bizJson); 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); 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); if (ccCallTask.getTaskType() == 1) { callPhone.setIntent(""); } else { callPhone.setIntent("-"); } return callPhone; } @GetMapping("/all") @ResponseBody public AjaxResult all() { List list = ccCallTaskService.selectCcCallTaskList(new CcCallTask()); return AjaxResult.success(list); } }