Переглянути джерело

优化gui外呼平台页面查询

peicj 4 днів тому
батько
коміт
42bfa1a152

+ 21 - 11
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/ApiController.java

@@ -250,17 +250,27 @@ public class ApiController extends BaseController {
         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);
+        if (records != null && !records.isEmpty()) {
+            List<Long> batchIds = new ArrayList<>(records.size());
+            for (CcCallTask data : records) {
+                batchIds.add(data.getBatchId());
+            }
+            Map<Long, CallTaskStatModel> statMap = ccCallPhoneService.statByBatchIds(batchIds);
+            for (CcCallTask data: records){
+                CallTaskStatModel statModel = statMap.get(data.getBatchId());
+                if (statModel == null) {
+                    statModel = new CallTaskStatModel();
+                }
+                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);

+ 89 - 8
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallPhoneController.java

@@ -58,6 +58,7 @@ public class CcCallPhoneController extends BaseController
 
     /**
      * 查询外呼号码列表
+     * 两阶段查询:先分页查 id(避开大字段),再按 id 回表列表字段;dialogue/biz_json/tts_text 按需加载
      */
     @RequiresPermissions("aicall:callPhone:list")
     @PostMapping("/list")
@@ -66,16 +67,26 @@ public class CcCallPhoneController extends BaseController
     public TableDataInfo list(CcCallPhone ccCallPhone)
     {
         Long t0 = System.currentTimeMillis();
+        // 任务名称已通过 batchId 选择;若仍传 batchName,先解析为 batchIds,避免大表 JOIN
+        resolveBatchNameToIds(ccCallPhone);
+
         startPage();
-        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneList(ccCallPhone);
-        logger.info("通话记录查询执行sql耗时:{}ms", (System.currentTimeMillis() - t0));
-        TableDataInfo tableDataInfo = getDataTable(list);
-        List<CcCallPhone> records = (List<CcCallPhone>) tableDataInfo.getRows();
+        List<String> ids = ccCallPhoneService.selectCcCallPhoneIds(ccCallPhone);
+        logger.info("通话记录查询id分页耗时:{}ms, size={}", (System.currentTimeMillis() - t0), ids == null ? 0 : ids.size());
+        TableDataInfo tableDataInfo = getDataTable(ids);
+        if (ids == null || ids.isEmpty()) {
+            return tableDataInfo;
+        }
+
+        Long t1 = System.currentTimeMillis();
+        List<CcCallPhone> records = ccCallPhoneService.selectCcCallPhoneLiteByIds(ids);
+        fillBatchNames(records);
         for (CcCallPhone data: records) {
-            if (data.getWavfile().startsWith("/")) {
-                data.setWavfile(data.getWavfile().substring(1));
+            String wavfile = data.getWavfile();
+            if (StringUtils.isNotEmpty(wavfile) && wavfile.startsWith("/")) {
+                wavfile = wavfile.substring(1);
             }
-            data.setWavfile("/recordings/files?filename=" + data.getWavfile());
+            data.setWavfile("/recordings/files?filename=" + (wavfile == null ? "" : wavfile));
             data.setTelephone(CommonUtils.maskPhoneNumber(data.getTelephone()));
             data.setCallerNumber(CommonUtils.maskPhoneNumber(data.getCallerNumber()));
             if (StringUtils.isBlank(data.getBatchName())) {
@@ -105,10 +116,80 @@ public class CcCallPhoneController extends BaseController
             }
         }
         tableDataInfo.setRows(records);
-        logger.info("通话记录查询后端累计耗时:{}ms", (System.currentTimeMillis() - t0));
+        logger.info("通话记录回表组装耗时:{}ms, 后端累计:{}ms", (System.currentTimeMillis() - t1), (System.currentTimeMillis() - t0));
         return tableDataInfo;
     }
 
+    /**
+     * 按需加载通话详情大字段(播放录音时拉取 dialogue 等)
+     */
+    @RequiresPermissions("aicall:callPhone:list")
+    @GetMapping("/detail/{id}")
+    @ResponseBody
+    public AjaxResult detail(@PathVariable("id") String id)
+    {
+        CcCallPhone phone = ccCallPhoneService.selectCcCallPhoneById(id);
+        if (phone == null) {
+            return AjaxResult.error("记录不存在");
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("id", phone.getId());
+        data.put("uuid", phone.getUuid());
+        data.put("dialogue", phone.getDialogue());
+        data.put("bizJson", phone.getBizJson());
+        data.put("ttsText", phone.getTtsText());
+        data.put("wavfile", phone.getWavfile());
+        data.put("recordServerUrl", phone.getRecordServerUrl());
+        return AjaxResult.success(data);
+    }
+
+    private void resolveBatchNameToIds(CcCallPhone ccCallPhone) {
+        if (ccCallPhone.getBatchId() != null || StringUtils.isEmpty(ccCallPhone.getBatchName())) {
+            return;
+        }
+        List<CcCallTask> tasks = ccCallTaskService.selectCcCallTaskList(
+                new CcCallTask().setBatchName(ccCallPhone.getBatchName()));
+        List<Long> batchIds = new ArrayList<>();
+        if (tasks != null) {
+            for (CcCallTask task : tasks) {
+                if (task.getBatchId() != null) {
+                    batchIds.add(task.getBatchId());
+                }
+            }
+        }
+        Map<String, Object> params = ccCallPhone.getParams();
+        if (params == null) {
+            params = new HashMap<>();
+            ccCallPhone.setParams(params);
+        }
+        // 无匹配任务时放入空列表,后续 SQL 用不可能命中的条件短路
+        if (batchIds.isEmpty()) {
+            batchIds.add(-1L);
+        }
+        params.put("batchIds", batchIds);
+        ccCallPhone.setBatchName(null);
+    }
+
+    private void fillBatchNames(List<CcCallPhone> records) {
+        Set<Long> batchIdSet = new HashSet<>();
+        for (CcCallPhone data : records) {
+            if (data.getBatchId() != null) {
+                batchIdSet.add(data.getBatchId());
+            }
+        }
+        if (batchIdSet.isEmpty()) {
+            return;
+        }
+        Map<Long, String> batchNameMap = new HashMap<>();
+        for (Long batchId : batchIdSet) {
+            CcCallTask task = ccCallTaskService.selectCcCallTaskByBatchId(batchId);
+            batchNameMap.put(batchId, task == null || StringUtils.isBlank(task.getBatchName()) ? "-" : task.getBatchName());
+        }
+        for (CcCallPhone data : records) {
+            data.setBatchName(batchNameMap.getOrDefault(data.getBatchId(), "-"));
+        }
+    }
+
     /**
      * 导出外呼号码列表
      */

+ 16 - 9
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/CcCallTaskController.java

@@ -92,9 +92,21 @@ public class CcCallTaskController extends BaseController
         List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(ccCallTask);
         TableDataInfo tableDataInfo = getDataTable(list);
         List<CcCallTask> records = (List<CcCallTask>) tableDataInfo.getRows();
+        if (records == null || records.isEmpty()) {
+            return tableDataInfo;
+        }
+        List<Long> batchIds = new ArrayList<>(records.size());
+        for (CcCallTask data : records) {
+            batchIds.add(data.getBatchId());
+        }
+        Map<Long, CallTaskStatModel> statMap = ccCallPhoneService.statByBatchIds(batchIds);
         Integer allDelDays = Integer.valueOf(paramsService.getParamValueByCode("callTask_allowDel_days", "30"));
+        long allowDelDeadline = System.currentTimeMillis() - allDelDays * 24L * 3600L * 1000L;
         for (CcCallTask data: records){
-            CallTaskStatModel statModel = ccCallPhoneService.statByBatchId(data.getBatchId());
+            CallTaskStatModel statModel = statMap.get(data.getBatchId());
+            if (statModel == null) {
+                statModel = new CallTaskStatModel();
+            }
             data.setPhoneCount(statModel.getPhoneCount());
             data.setCallCount(statModel.getCallCount());
             data.setNoCallCount(statModel.getPhoneCount() - statModel.getCallCount());
@@ -105,17 +117,10 @@ public class CcCallTaskController extends BaseController
             } 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()) {
+                    && allowDelDeadline > data.getStopTime()) {
                 data.setAllowDel(1);
             }
         }
@@ -556,6 +561,8 @@ public class CcCallTaskController extends BaseController
     @ResponseBody
     public AjaxResult all(CcCallTask ccCallTask)
     {
+        // 供通话记录页任务名称下拉模糊搜索,限制返回条数避免拖慢
+        startPage(1, 20);
         List<CcCallTask> list = ccCallTaskService.selectCcCallTaskList(ccCallTask);
         return AjaxResult.success(list);
     }

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

@@ -31,6 +31,16 @@ public interface CcCallPhoneMapper
      */
     public List<CcCallPhone> selectCcCallPhoneList(CcCallPhone ccCallPhone);
 
+    /**
+     * 列表加速:仅查询符合条件的主键(供分页/COUNT)
+     */
+    List<String> selectCcCallPhoneIds(CcCallPhone ccCallPhone);
+
+    /**
+     * 列表加速:按主键批量回表轻量字段(不含 dialogue/biz_json/tts_text)
+     */
+    List<CcCallPhone> selectCcCallPhoneLiteByIds(@Param("ids") List<String> ids);
+
     /**
      * 新增外呼号码
      * 
@@ -70,6 +80,13 @@ public interface CcCallPhoneMapper
      */
     CallTaskStatModel statByBatchId(Long batchId);
 
+    /**
+     * 根据多个batchId批量统计外呼数据
+     * @param batchIds
+     * @return
+     */
+    List<CallTaskStatModel> statByBatchIds(@Param("batchIds") List<Long> batchIds);
+
     /**
      * 批量入库
      * @param phoneList

+ 18 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/service/ICcCallPhoneService.java

@@ -6,6 +6,7 @@ import com.ruoyi.aicall.model.CallPhoneExportVo;
 import com.ruoyi.aicall.model.CallTaskStatModel;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 外呼号码Service接口
@@ -31,6 +32,16 @@ public interface ICcCallPhoneService
      */
     public List<CcCallPhone> selectCcCallPhoneList(CcCallPhone ccCallPhone);
 
+    /**
+     * 列表加速:仅查询符合条件的主键(供分页/COUNT)
+     */
+    List<String> selectCcCallPhoneIds(CcCallPhone ccCallPhone);
+
+    /**
+     * 列表加速:按主键批量回表轻量字段(不含 dialogue/biz_json/tts_text)
+     */
+    List<CcCallPhone> selectCcCallPhoneLiteByIds(List<String> ids);
+
     /**
      * 新增外呼号码
      * 
@@ -70,6 +81,13 @@ public interface ICcCallPhoneService
      */
     public CallTaskStatModel statByBatchId(Long batchId);
 
+    /**
+     * 根据多个batchId批量统计外呼数据
+     * @param batchIds
+     * @return key=batchId
+     */
+    Map<Long, CallTaskStatModel> statByBatchIds(List<Long> batchIds);
+
     /**
      * 批量入库
      * @param phoneList

+ 84 - 45
ruoyi-admin/src/main/java/com/ruoyi/aicall/service/impl/CcCallPhoneServiceImpl.java

@@ -55,58 +55,72 @@ public class CcCallPhoneServiceImpl implements ICcCallPhoneService
     @Override
     public List<CcCallPhone> selectCcCallPhoneList(CcCallPhone ccCallPhone)
     {
+        normalizeListQueryParams(ccCallPhone);
+        return ccCallPhoneMapper.selectCcCallPhoneList(ccCallPhone);
+    }
 
-        Map<String, Object> params = ccCallPhone.getParams();
-        if (null != params.get("calloutTimeStart")
-                && !"".equals(params.get("calloutTimeStart"))) {
-            params.put("calloutTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("calloutTimeStart")).getTime());
-        }
-        if (null != params.get("calloutTimeEnd")
-                && !"".equals(params.get("calloutTimeEnd"))) {
-            params.put("calloutTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("calloutTimeEnd")).getTime());
-        }
-        if (null != params.get("callEndTimeStart")
-                && !"".equals(params.get("callEndTimeStart"))) {
-            params.put("callEndTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("callEndTimeStart")).getTime());
-        }
-        if (null != params.get("callEndTimeEnd")
-                && !"".equals(params.get("callEndTimeEnd"))) {
-            params.put("callEndTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("callEndTimeEnd")).getTime());
-        }
-        if (null != params.get("connectedTimeStart")
-                && !"".equals(params.get("connectedTimeStart"))) {
-            params.put("connectedTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("connectedTimeStart")).getTime());
-        }
-        if (null != params.get("connectedTimeEnd")
-                && !"".equals(params.get("connectedTimeEnd"))) {
-            params.put("connectedTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("connectedTimeEnd")).getTime());
-        }
-        if (null != params.get("timeLenStart")
-                && !"".equals(params.get("timeLenStart"))) {
-            params.put("timeLenStart", Double.valueOf((String)params.get("timeLenStart")) * 60 * 1000L);
+    @Override
+    public List<String> selectCcCallPhoneIds(CcCallPhone ccCallPhone) {
+        normalizeListQueryParams(ccCallPhone);
+        return ccCallPhoneMapper.selectCcCallPhoneIds(ccCallPhone);
+    }
+
+    @Override
+    public List<CcCallPhone> selectCcCallPhoneLiteByIds(List<String> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return new java.util.ArrayList<>();
         }
-        if (null != params.get("timeLenEnd")
-                && !"".equals(params.get("timeLenEnd"))) {
-            params.put("timeLenEnd", Double.valueOf((String)params.get("timeLenEnd")) * 60 * 1000L);
+        return ccCallPhoneMapper.selectCcCallPhoneLiteByIds(ids);
+    }
+
+    /**
+     * 统一处理列表查询时间参数;已是数值则不再转换,避免重复转换异常
+     */
+    private void normalizeListQueryParams(CcCallPhone ccCallPhone) {
+        Map<String, Object> params = ccCallPhone.getParams();
+        if (params == null) {
+            params = new HashMap<>();
+            ccCallPhone.setParams(params);
+            return;
         }
-        if (null != params.get("timeLenSecondStart")
-                && !"".equals(params.get("timeLenSecondStart"))) {
-            params.put("timeLenSecondStart", Double.valueOf((String)params.get("timeLenSecondStart")) * 60 * 1000L);
+        convertStringDateToMillis(params, "calloutTimeStart");
+        convertStringDateToMillis(params, "calloutTimeEnd");
+        convertStringDateToMillis(params, "callEndTimeStart");
+        convertStringDateToMillis(params, "callEndTimeEnd");
+        convertStringDateToMillis(params, "connectedTimeStart");
+        convertStringDateToMillis(params, "connectedTimeEnd");
+        convertStringDateToMillis(params, "answeredTimeStart");
+        convertStringDateToMillis(params, "answeredTimeEnd");
+        // 页面时长按“分钟”筛选(历史逻辑)
+        convertMinutesToMillis(params, "timeLenStart");
+        convertMinutesToMillis(params, "timeLenEnd");
+        convertMinutesToMillis(params, "timeLenSecondStart");
+        convertMinutesToMillis(params, "timeLenSecondEnd");
+        ccCallPhone.setParams(params);
+    }
+
+    private void convertStringDateToMillis(Map<String, Object> params, String key) {
+        Object value = params.get(key);
+        if (!(value instanceof String) || StringUtils.isEmpty((String) value)) {
+            return;
         }
-        if (null != params.get("timeLenSecondEnd")
-                && !"".equals(params.get("timeLenSecondEnd"))) {
-            params.put("timeLenSecondEnd", Double.valueOf((String)params.get("timeLenSecondEnd")) * 60 * 1000L);
+        String text = ((String) value).trim();
+        if (text.matches("^\\d+$")) {
+            params.put(key, Long.parseLong(text));
+            return;
         }
-        if (null != params.get("answeredTimeStart")
-                && !"".equals(params.get("answeredTimeStart"))) {
-            params.put("answeredTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("answeredTimeStart")).getTime());
+        params.put(key, DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", text).getTime());
+    }
+
+    private void convertMinutesToMillis(Map<String, Object> params, String key) {
+        Object value = params.get(key);
+        if (value == null || "".equals(value)) {
+            return;
         }
-        if (null != params.get("answeredTimeEnd")
-                && !"".equals(params.get("answeredTimeEnd"))) {
-            params.put("answeredTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("answeredTimeEnd")).getTime());
+        if (value instanceof Number) {
+            return;
         }
-        ccCallPhone.setParams(params);
-        return ccCallPhoneMapper.selectCcCallPhoneList(ccCallPhone);
+        params.put(key, Double.valueOf(value.toString()) * 60 * 1000L);
     }
 
     /**
@@ -160,6 +174,31 @@ public class CcCallPhoneServiceImpl implements ICcCallPhoneService
     @Override
     public CallTaskStatModel statByBatchId(Long batchId) {
         CallTaskStatModel callTaskStatModel = ccCallPhoneMapper.statByBatchId(batchId);
+        return normalizeStat(callTaskStatModel);
+    }
+
+    @Override
+    public Map<Long, CallTaskStatModel> statByBatchIds(List<Long> batchIds) {
+        Map<Long, CallTaskStatModel> result = new HashMap<>();
+        if (batchIds == null || batchIds.isEmpty()) {
+            return result;
+        }
+        List<CallTaskStatModel> stats = ccCallPhoneMapper.statByBatchIds(batchIds);
+        if (stats != null) {
+            for (CallTaskStatModel stat : stats) {
+                CallTaskStatModel normalized = normalizeStat(stat);
+                if (normalized.getBatchId() != null) {
+                    result.put(normalized.getBatchId(), normalized);
+                }
+            }
+        }
+        return result;
+    }
+
+    private CallTaskStatModel normalizeStat(CallTaskStatModel callTaskStatModel) {
+        if (callTaskStatModel == null) {
+            callTaskStatModel = new CallTaskStatModel();
+        }
         if (null == callTaskStatModel.getPhoneCount()) {
             callTaskStatModel.setPhoneCount(0);
         }

+ 48 - 10
ruoyi-admin/src/main/java/com/ruoyi/cc/controller/CcInboundCdrController.java

@@ -16,6 +16,7 @@ import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.common.utils.CommonUtils;
+import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.common.utils.ExceptionUtil;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.common.utils.StringUtils;
@@ -31,6 +32,7 @@ import org.springframework.ui.ModelMap;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -100,25 +102,30 @@ public class CcInboundCdrController extends BaseController
         }
         params.put("manageOpnum", manageOpnum);
         ccInboundCdr.setParams(params);
+        normalizeQueryTimeParams(ccInboundCdr);
 
-        Map<String, String> groupNames = new HashMap<>();
         startPage();
         List<CcInboundCdr> list = ccInboundCdrService.selectCcInboundCdrList(ccInboundCdr);
         TableDataInfo tableDataInfo = getDataTable(list);
         List<CcInboundCdr> records = (List<CcInboundCdr>) tableDataInfo.getRows();
+        if (records == null || records.isEmpty()) {
+            return tableDataInfo;
+        }
+
+        Map<String, String> groupNames = new HashMap<>();
+        for (CcBizGroup group : groupService.selectCcBizGroupList(new CcBizGroup())) {
+            if (group.getGroupId() != null) {
+                groupNames.put(String.valueOf(group.getGroupId()), group.getBizGroupName());
+            }
+        }
         for (CcInboundCdr data: records) {
-            data.setWavFileUrl("/recordings/files?filename=" + data.getWavFile());
+            String wavFile = data.getWavFile();
+            data.setWavFileUrl("/recordings/files?filename=" + (wavFile == null ? "" : wavFile));
             data.setCaller(CommonUtils.maskPhoneNumber(data.getCaller()));
-            String groupName = groupNames.getOrDefault(data.getGroupId(), "");
+            String groupName = groupNames.getOrDefault(data.getGroupId(), "-");
             if (StringUtils.isBlank(groupName)) {
-                CcBizGroup ccBizGroup = groupService.selectCcBizGroupByGroupId(data.getGroupId());
-                if (null != ccBizGroup) {
-                    groupName = ccBizGroup.getBizGroupName();
-                } else {
-                    groupName = "-";
-                }
+                groupName = "-";
             }
-            groupNames.put(data.getGroupId(), groupName);
             data.setGroupName(groupName);
 
             // 挂机原因处理
@@ -149,6 +156,37 @@ public class CcInboundCdrController extends BaseController
         return tableDataInfo;
     }
 
+    /**
+     * 前端传入 yyyy-MM-dd HH:mm:ss,库字段为毫秒时间戳,统一转换以便命中索引
+     */
+    private void normalizeQueryTimeParams(CcInboundCdr ccInboundCdr) {
+        Map<String, Object> params = ccInboundCdr.getParams();
+        if (params == null || params.isEmpty()) {
+            return;
+        }
+        convertDatetimeParamToMillis(params, "inboundTimeStart");
+        convertDatetimeParamToMillis(params, "inboundTimeEnd");
+        convertDatetimeParamToMillis(params, "answeredTimeStart");
+        convertDatetimeParamToMillis(params, "answeredTimeEnd");
+        convertDatetimeParamToMillis(params, "hangupTimeStart");
+        convertDatetimeParamToMillis(params, "hangupTimeEnd");
+    }
+
+    private void convertDatetimeParamToMillis(Map<String, Object> params, String key) {
+        Object value = params.get(key);
+        if (value == null || StringUtils.isEmpty(value.toString())) {
+            return;
+        }
+        String text = value.toString().trim();
+        if (text.matches("^\\d+$")) {
+            return;
+        }
+        Date date = DateUtils.parseDate(text);
+        if (date != null) {
+            params.put(key, date.getTime());
+        }
+    }
+
     /**
      * 导出呼入记录列表
      */

+ 112 - 48
ruoyi-admin/src/main/resources/mapper/aicall/CcCallPhoneMapper.xml

@@ -53,60 +53,111 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                total_cost, billing_status, caller_number, ivr_dtmf_digits, manual_answered_time, manual_answered_time_len from cc_call_phone
     </sql>
 
+    <sql id="ccCallPhoneListWhere">
+        <if test="uuid != null and uuid != '' "> and cp.uuid = #{uuid}</if>
+        <if test="telephone != null  and telephone != ''"> and cp.telephone = #{telephone}</if>
+        <if test="custName != null  and custName != ''"> and cp.cust_name = #{custName}</if>
+        <if test="callstatus != null "> and cp.callstatus = #{callstatus}</if>
+        <if test="params.calloutTimeStart != null and params.calloutTimeStart != ''"><!-- 开始时间检索 -->
+            AND cp.callout_time &gt;= #{params.calloutTimeStart}
+        </if>
+        <if test="params.calloutTimeEnd != null and params.calloutTimeEnd != ''"><!-- 结束时间检索 -->
+            AND cp.callout_time &lt;= #{params.calloutTimeEnd}
+        </if>
+        <if test="params.callEndTimeStart != null and params.callEndTimeStart != ''"><!-- 开始时间检索 -->
+            AND cp.call_end_time &gt;= #{params.callEndTimeStart}
+        </if>
+        <if test="params.callEndTimeEnd != null and params.callEndTimeEnd != ''"><!-- 结束时间检索 -->
+            AND cp.call_end_time &lt;= #{params.callEndTimeEnd}
+        </if>
+        <if test="params.timeLenStart != null and params.timeLenStart != ''"><!-- 开始时间检索 -->
+            AND cp.time_len &gt;= #{params.timeLenStart}
+        </if>
+        <if test="params.timeLenEnd != null and params.timeLenEnd != ''"><!-- 结束时间检索 -->
+            AND cp.time_len &lt;= #{params.timeLenEnd}
+        </if>
+        <if test="params.connectedTimeStart != null and params.connectedTimeStart != ''"><!-- 开始时间检索 -->
+            AND cp.connected_time &gt;= #{params.connectedTimeStart}
+        </if>
+        <if test="params.connectedTimeEnd != null and params.connectedTimeEnd != ''"><!-- 结束时间检索 -->
+            AND cp.connected_time &lt;= #{params.connectedTimeEnd}
+        </if>
+        <if test="params.answeredTimeStart != null and params.answeredTimeStart != ''"><!-- 开始时间检索 -->
+            AND cp.answered_time &gt;= #{params.answeredTimeStart}
+        </if>
+        <if test="params.answeredTimeEnd != null and params.answeredTimeEnd != ''"><!-- 结束时间检索 -->
+            AND cp.answered_time &lt;= #{params.answeredTimeEnd}
+        </if>
+        <if test="batchId != null">
+            AND cp.batch_id = #{batchId}
+        </if>
+        <if test="params.batchIds != null and params.batchIds.size() &gt; 0">
+            AND cp.batch_id IN
+            <foreach collection="params.batchIds" item="bid" open="(" separator="," close=")">
+                #{bid}
+            </foreach>
+        </if>
+        <if test="batchName != null and batchName != ''">
+            AND EXISTS (
+                SELECT 1 FROM cc_call_task ct
+                WHERE ct.batch_id = cp.batch_id
+                  AND ct.batch_name like concat('%', #{batchName}, '%')
+            )
+        </if>
+        <if test="intent != null and intent != ''">
+            AND cp.intent = #{intent}
+        </if>
+        <if test="billingStatus != null ">
+            and cp.billing_status = #{billingStatus}
+        </if>
+        <if test="callerNumber != null and callerNumber != '' ">
+            and cp.caller_number = #{callerNumber}
+        </if>
+        AND cp.callstatus &gt;= 3 AND cp.call_end_time &gt; 0
+    </sql>
+
+    <!-- 列表完整查询(导出/API/计费等仍可用,含大字段) -->
     <select id="selectCcCallPhoneList" parameterType="CcCallPhone" resultMap="CcCallPhoneResult">
-        select cp.*,ct.batch_name as batchName from cc_call_phone cp left join cc_call_task ct on cp.batch_id = ct.batch_id
+        select cp.*, ct.batch_name as batchName
+        from cc_call_phone cp
+        left join cc_call_task ct on cp.batch_id = ct.batch_id
         <where>
-            <if test="uuid != null and uuid != '' "> and cp.uuid = #{uuid}</if>
-            <if test="telephone != null  and telephone != ''"> and cp.telephone = #{telephone}</if>
-            <if test="custName != null  and custName != ''"> and cp.cust_name = #{custName}</if>
-            <if test="callstatus != null "> and cp.callstatus = #{callstatus}</if>
-            <if test="params.calloutTimeStart != null and params.calloutTimeStart != ''"><!-- 开始时间检索 -->
-                AND cp.callout_time &gt;= #{params.calloutTimeStart}
-            </if>
-            <if test="params.calloutTimeEnd != null and params.calloutTimeEnd != ''"><!-- 结束时间检索 -->
-                AND cp.callout_time &lt;= #{params.calloutTimeEnd}
-            </if>
-            <if test="params.callEndTimeStart != null and params.callEndTimeStart != ''"><!-- 开始时间检索 -->
-                AND cp.call_end_time &gt;= #{params.callEndTimeStart}
-            </if>
-            <if test="params.callEndTimeEnd != null and params.callEndTimeEnd != ''"><!-- 结束时间检索 -->
-                AND cp.call_end_time &lt;= #{params.callEndTimeEnd}
-            </if>
-            <if test="params.timeLenStart != null and params.timeLenStart != ''"><!-- 开始时间检索 -->
-                AND cp.time_len &gt;= #{params.timeLenStart}
-            </if>
-            <if test="params.timeLenEnd != null and params.timeLenEnd != ''"><!-- 结束时间检索 -->
-                AND cp.time_len &lt;= #{params.timeLenEnd}
-            </if>
-            <if test="params.connectedTimeStart != null and params.connectedTimeStart != ''"><!-- 开始时间检索 -->
-                AND cp.connected_time &gt;= #{params.connectedTimeStart}
-            </if>
-            <if test="params.connectedTimeEnd != null and params.connectedTimeEnd != ''"><!-- 结束时间检索 -->
-                AND cp.connected_time &lt;= #{connectedTimeEnd}
-            </if>
-            <if test="params.answeredTimeStart != null and params.answeredTimeStart != ''"><!-- 开始时间检索 -->
-                AND cp.answered_time &gt;= #{params.answeredTimeStart}
-            </if>
-            <if test="params.answeredTimeEnd != null and params.answeredTimeEnd != ''"><!-- 结束时间检索 -->
-                AND cp.answered_time &lt;= #{params.answeredTimeEnd}
-            </if>
-            <if test="batchId != null and batchId != ''">
-                AND cp.batch_id = #{batchId}
-            </if>
-            <if test="intent != null and intent != ''">
-                AND cp.intent = #{intent}
-            </if>
-            <if test="billingStatus != null ">
-                and cp.billing_status = #{billingStatus}
-            </if>
-            <if test="callerNumber != null and callerNumber != '' ">
-                and cp.caller_number = #{callerNumber}
-            </if>
-            AND cp.callstatus >= 3 AND cp.call_end_time > 0
+            <include refid="ccCallPhoneListWhere"/>
         </where>
         order by cp.call_end_time desc
     </select>
 
+    <!-- 列表加速第1步:仅查 id,避免分页/COUNT 触碰 TEXT 大字段 -->
+    <select id="selectCcCallPhoneIds" parameterType="CcCallPhone" resultType="java.lang.String">
+        select cp.id
+        from cc_call_phone cp
+        <where>
+            <include refid="ccCallPhoneListWhere"/>
+        </where>
+        order by cp.call_end_time desc
+    </select>
+
+    <!-- 列表加速第2步:按 id 回表列表展示字段(不含 dialogue/biz_json/tts_text) -->
+    <select id="selectCcCallPhoneLiteByIds" resultMap="CcCallPhoneResult">
+        select
+            cp.id, cp.batch_id, cp.telephone, cp.cust_name, cp.createtime, cp.callstatus,
+            cp.callout_time, cp.callcount, cp.call_end_time, cp.time_len, cp.valid_time_len,
+            cp.uuid, cp.connected_time, cp.hangup_cause, cp.answered_time,
+            cp.wavfile, cp.record_server_url, cp.dialogue_count, cp.acd_opnum,
+            cp.acd_queue_time, cp.acd_wait_time, cp.intent, cp.caller_number,
+            cp.ivr_dtmf_digits, cp.manual_answered_time, cp.manual_answered_time_len
+        from cc_call_phone cp
+        where cp.id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        order by field(cp.id,
+        <foreach collection="ids" item="id" separator=",">
+            #{id}
+        </foreach>
+        )
+    </select>
+
     <select id="getCustIntentionList" parameterType="CcCallPhone" resultMap="CcCallPhoneResult">
         <include refid="selectCcCallPhoneVo"/>
           where intent = '' AND callstatus >= 3 AND call_end_time > 0 limit 100
@@ -227,6 +278,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         WHERE batch_id = #{batchId}
     </select>
 
+    <select id="statByBatchIds" resultType="com.ruoyi.aicall.model.CallTaskStatModel">
+        SELECT batch_id AS batchId,
+               COUNT(1) AS phoneCount,
+               SUM(CASE WHEN callout_time > 0 THEN 1 ELSE 0 END) AS callCount,
+               SUM(CASE WHEN answered_time > 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM cc_call_phone
+        WHERE batch_id IN
+        <foreach collection="batchIds" item="batchId" open="(" separator="," close=")">
+            #{batchId}
+        </foreach>
+        GROUP BY batch_id
+    </select>
+
     <insert id="batchInsertCcCallPhone" parameterType="java.util.List">
         INSERT INTO cc_call_phone (
         id, batch_id, telephone, cust_name, createtime, callstatus,

+ 22 - 4
ruoyi-admin/src/main/resources/templates/aicall/callPhone/callPhone.html

@@ -579,11 +579,11 @@
                         const extension = row.wavfile.split('.').pop().toLowerCase();
                         // 判断文件类型
                         if (['wav', 'mp3', 'aac'].includes(extension)) {
-                            actions.push('<a class="btn btn-success btn-xs ' + linstenFlag + '" href="javascript:void(0)" onclick="openModal(\'' + row.recordServerUrl + row.wavfile + '\',\'' + escape(JSON.stringify(row.dialogue)) + '\')"><i class="fa fa-volume-up"></i>' + i18n('btn.record.audio.play') + '</a> ');
-                            actions.push('<a class="btn btn-success btn-xs ' + downloadFlag + '" href="javascript:void(0)" onclick="checkFileExistence(\'' + row.recordServerUrl + row.wavfile + '\',\'' + escape(JSON.stringify(row.dialogue)) + '\')"><i class="fa fa-download"></i>' + i18n('btn.record.audio.download') + '</a>');
+                            actions.push('<a class="btn btn-success btn-xs ' + linstenFlag + '" href="javascript:void(0)" onclick="playRecordById(\'' + row.id + '\',\'' + row.recordServerUrl + row.wavfile + '\')"><i class="fa fa-volume-up"></i>' + i18n('btn.record.audio.play') + '</a> ');
+                            actions.push('<a class="btn btn-success btn-xs ' + downloadFlag + '" href="javascript:void(0)" onclick="checkFileExistence(\'' + row.recordServerUrl + row.wavfile + '\')"><i class="fa fa-download"></i>' + i18n('btn.record.audio.download') + '</a>');
                         } else if (['mp4', 'avi', 'mov'].includes(extension)) {
-                            actions.push('<a class="btn btn-success btn-xs ' + linstenFlag + '" href="javascript:void(0)" onclick="openModal(\'' + row.recordServerUrl + row.wavfile + '\',\'' + escape(JSON.stringify(row.dialogue)) + '\')"><i class="fa fa-volume-up"></i>' + i18n('btn.record.video.play') + '</a> ');
-                            actions.push('<a class="btn btn-success btn-xs ' + downloadFlag + '" href="javascript:void(0)" onclick="checkFileExistence(\'' + row.recordServerUrl + row.wavfile + '\',\'' + escape(JSON.stringify(row.dialogue)) + '\')"><i class="fa fa-download"></i>' + i18n('btn.record.video.download') + '</a>');
+                            actions.push('<a class="btn btn-success btn-xs ' + linstenFlag + '" href="javascript:void(0)" onclick="playRecordById(\'' + row.id + '\',\'' + row.recordServerUrl + row.wavfile + '\')"><i class="fa fa-volume-up"></i>' + i18n('btn.record.video.play') + '</a> ');
+                            actions.push('<a class="btn btn-success btn-xs ' + downloadFlag + '" href="javascript:void(0)" onclick="checkFileExistence(\'' + row.recordServerUrl + row.wavfile + '\')"><i class="fa fa-download"></i>' + i18n('btn.record.video.download') + '</a>');
                         }
                         return actions.join('');
                     }
@@ -644,6 +644,24 @@
     });
 
     var videoPlayer; // 定义一个全局变量来存储播放器实例
+
+    /** 列表已剥离 dialogue,播放时按需拉取大字段 */
+    function playRecordById(id, url) {
+        $.ajax({
+            url: prefix + "/detail/" + id,
+            type: "get",
+            success: function(result) {
+                var dialogue = (result && (result.code === 0 || result.code === 200) && result.data)
+                    ? (result.data.dialogue || '')
+                    : '';
+                openModal(url, escape(JSON.stringify(dialogue)));
+            },
+            error: function() {
+                openModal(url, '');
+            }
+        });
+    }
+
     function openModal(url, escapedDialogContent){
         const type = getMediaType(url);
         console.log(type);