Jelajahi Sumber

Merge branch 'master' of http://1.14.104.71:10880/wushubo/easycallcenter365-gui-dev

yzx 4 hari lalu
induk
melakukan
dc7d303c11
38 mengubah file dengan 1574 tambahan dan 85 penghapusan
  1. 714 1
      ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/ApiController.java
  2. 55 1
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallPhone.java
  3. 2 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcTtsAliyun.java
  4. 4 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/mapper/CcCallPhoneMapper.java
  5. 10 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/model/ApiCallRecordQueryParams.java
  6. 23 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/model/ApiCallTaskModel.java
  7. 4 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/service/ICcCallPhoneService.java
  8. 53 0
      ruoyi-admin/src/main/java/com/ruoyi/aicall/service/impl/CcCallPhoneServiceImpl.java
  9. 27 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/controller/CcExtNumController.java
  10. 4 1
      ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcCustInfo.java
  11. 10 4
      ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcExtNum.java
  12. 5 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcGateways.java
  13. 5 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcOutboundCdr.java
  14. 42 1
      ruoyi-admin/src/main/java/com/ruoyi/cc/mapper/CcExtNumMapper.java
  15. 24 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/service/ICcExtNumService.java
  16. 2 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/service/ICcOutboundCdrService.java
  17. 205 10
      ruoyi-admin/src/main/java/com/ruoyi/cc/service/impl/CcExtNumServiceImpl.java
  18. 56 0
      ruoyi-admin/src/main/java/com/ruoyi/cc/service/impl/CcOutboundCdrServiceImpl.java
  19. 2 2
      ruoyi-admin/src/main/resources/application-dev.yml
  20. 1 1
      ruoyi-admin/src/main/resources/application.yml
  21. 63 0
      ruoyi-admin/src/main/resources/mapper/aicall/CcCallPhoneMapper.xml
  22. 12 14
      ruoyi-admin/src/main/resources/mapper/aicall/CcCallTaskMapper.xml
  23. 63 1
      ruoyi-admin/src/main/resources/mapper/cc/CcExtNumMapper.xml
  24. 7 1
      ruoyi-admin/src/main/resources/mapper/cc/CcGatewaysMapper.xml
  25. TEMPAT SAMPAH
      ruoyi-admin/src/main/resources/static/img/login-background.jpg
  26. 2 2
      ruoyi-admin/src/main/resources/templates/aicall/callPhone/callPhone.html
  27. 9 6
      ruoyi-admin/src/main/resources/templates/aicall/callTask/add.html
  28. 9 6
      ruoyi-admin/src/main/resources/templates/aicall/callTask/edit.html
  29. 8 2
      ruoyi-admin/src/main/resources/templates/aicall/inboundllm/add.html
  30. 8 2
      ruoyi-admin/src/main/resources/templates/aicall/inboundllm/edit.html
  31. 5 5
      ruoyi-admin/src/main/resources/templates/cc/extnum/add.html
  32. 74 0
      ruoyi-admin/src/main/resources/templates/cc/extnum/batchAdd.html
  33. 8 0
      ruoyi-admin/src/main/resources/templates/cc/extnum/extnum.html
  34. 2 1
      ruoyi-admin/src/main/resources/templates/index.html
  35. 5 5
      ruoyi-admin/src/main/resources/templates/login.html
  36. 41 2
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  37. 0 16
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
  38. 10 1
      ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysPasswordService.java

+ 714 - 1
ruoyi-admin/src/main/java/com/ruoyi/aicall/controller/ApiController.java

@@ -13,6 +13,7 @@ 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.aicall.utils.DESUtil;
 import com.ruoyi.cc.domain.*;
 import com.ruoyi.cc.service.*;
 import com.ruoyi.cc.utils.DateValidatorUtils;
@@ -25,15 +26,20 @@ import com.ruoyi.common.utils.ShiroUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.bean.BeanUtils;
 import com.ruoyi.common.utils.uuid.UuidGenerator;
+import com.ruoyi.framework.shiro.service.SysPasswordService;
+import com.ruoyi.framework.shiro.util.AuthorizationUtils;
+import com.ruoyi.system.service.ISysUserService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.RandomUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.*;
+import java.util.stream.Collectors;
 
 @Controller
 @Slf4j
@@ -64,6 +70,18 @@ public class ApiController extends BaseController {
     private ICcExtNumService ccExtNumService;
     @Autowired
     private ICcParamsService ccParamsService;
+    @Autowired
+    private ISysUserService userService;
+    @Autowired
+    private SysPasswordService passwordService;
+    @Autowired
+    private ICcCustCallRecordService ccCustCallRecordService;
+    @Autowired
+    private ICcCustInfoService ccCustInfoService;
+    @Autowired
+    private ISysDivisionDataService sysDivisionDataService;
+    @Autowired
+    private ICcOutboundCdrService ccOutboundCdrService;
 
     /**
      * 获取外呼网关列表接口
@@ -131,6 +149,7 @@ public class ApiController extends BaseController {
             obj.put("voiceName", ttsAliyun.getVoiceName());
             obj.put("voiceCode", ttsAliyun.getVoiceCode());
             obj.put("voiceSource", ttsAliyun.getVoiceSource());
+            obj.put("ttsModels", ttsAliyun.getTtsModels());
             result.add(obj);
         }
         return AjaxResult.success(result);
@@ -396,10 +415,32 @@ public class ApiController extends BaseController {
         }
         // 创建任务
         BeanUtils.copyProperties(apiCallTaskModel, ccCallTask, "batchId");
+        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));
+        }
         if (null != ccCallTask.getConntectRate() && ccCallTask.getConntectRate() > 0) {
             ccCallTask.setRate(ccCallTask.getConntectRate()/100.0);
         }
         ccCallTask.setCreatetime(System.currentTimeMillis());
+        if (StringUtils.isBlank(ccCallTask.getAsrLanguageCode())) {
+            ccCallTask.setAsrLanguageCode("zh-CN");
+        }
+        if (StringUtils.isBlank(ccCallTask.getTtsLanguageCode())) {
+            ccCallTask.setTtsLanguageCode("zh-CN");
+        }
+        if (StringUtils.isBlank(ccCallTask.getAsrModels())) {
+            ccCallTask.setAsrModels("");
+        }
+        if (StringUtils.isBlank(ccCallTask.getTtsModels())) {
+            ccCallTask.setTtsModels("");
+        }
         ccCallTaskService.insertCcCallTask(ccCallTask);
         apiCallTaskModel.setBatchId(ccCallTask.getBatchId());
         return AjaxResult.success("success", apiCallTaskModel);
@@ -562,7 +603,7 @@ public class ApiController extends BaseController {
             ccCallPhoneService.batchInsertCcCallPhone(callPhoneList);
         }
         log.info("成功追加" + successCount + "个名单");
-        return AjaxResult.success("成功追加" + successCount + "个名单");
+        return AjaxResult.success("成功追加" + successCount + "个名单",callPhoneList);
     }
 
 
@@ -1101,4 +1142,676 @@ public class ApiController extends BaseController {
         result.put("data", data);
         return result;
     }
+
+    //=======================================================新增接口============================================================
+    /**
+     * 删除外呼任务
+     */
+    @PostMapping( "/removeTask")
+    @ResponseBody
+    @Transactional
+    public AjaxResult removeTask(@RequestBody Map<String, Long[]> paramMap)
+    {
+        Long[] batchIds= paramMap.get("batchIds");
+        if(batchIds==null){
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "batchIds不能为空!", "");
+        }
+        for (Long batchId : batchIds) {
+            // 备份拨打记录数据
+            ccCallPhoneService.bakCallPhoneByBatchId(batchId);
+            // 删除拨打记录数据
+            ccCallPhoneService.delCallPhoneByBatchId(batchId);
+            // 备份任务数据
+            ccCallTaskService.bakCallTaskByBatchId(batchId);
+        }
+
+        // 删除任务数据
+        String batchIdsStr = Arrays.stream(batchIds)
+                .map(String::valueOf)
+                .collect(Collectors.joining(","));
+        return toAjax(ccCallTaskService.deleteCcCallTaskByBatchIds(batchIdsStr));
+    }
+    /**
+     * 获取电话工具条的网关列表
+     */
+    @PostMapping("/myPhoneBar/params")
+    @ResponseBody
+    public AjaxResult getMyPhoneBaseParams(@RequestBody Map<String,String> param) {
+
+        String extNum = param.get("extNum");
+        String myGateway = param.get("myGateway");
+        if(extNum == null){
+            return AjaxResult.error("分机号参数缺失");
+        }
+        // 获取分机号
+        CcExtNum ccExtNum = ccExtNumService.selectCcExtNumByExtNum(Long.valueOf(extNum));
+
+        String extnum = ccExtNum.getExtNum().toString();
+        String opnum = ccExtNum.getUserCode();
+        String password = ccExtNum.getExtPass();
+        String groupId = "1";
+        String skillLevel = "9";
+        String projectId = "1";
+        //1.创建token
+        String loginToken = ccExtNumService.createToken(extnum, opnum, groupId, skillLevel, projectId);
+        //2.获取加密密码
+        String encryptStr = DESUtil.encrypt(password + "," + DateUtils.format(DateUtils.addDays(new Date(), 1), "yyyyMMddHHmm"));
+        encryptStr = String.format("var _phoneEncryptPassword='%s';", encryptStr);
+
+        CcGateways ccGatewaysVo = new CcGateways();
+        //判断指定网关还是全部网关
+        if(StringUtils.isNotBlank(myGateway)){
+            List<Long> gatewayIds = Arrays.stream(myGateway.split(","))
+                    .map(Long::parseLong)
+                    .collect(Collectors.toList());
+            ccGatewaysVo.setGatewayIds(gatewayIds);
+        }else{
+            String wgName = param.get("wgName");
+            if(StringUtils.isNotBlank(wgName)){
+                ccGatewaysVo.setGwName(wgName);
+            }else{
+                // 网关用途 0 已废弃; 1 电话条; 2 外呼任务; 3 无限制
+                Map<String, Object> params = new HashMap<>();
+                params.put("purposes", Arrays.asList(1,3));
+                ccGatewaysVo.setParams(params);
+            }
+        }
+        //3.获取工具条网关列表
+        List<CcGateways> gatewaysList = ccGatewaysService.selectCcGatewaysList(ccGatewaysVo);
+        List<JSONObject> gatewayList = new ArrayList<>();
+        for (CcGateways ccGateways: gatewaysList) {
+            JSONObject configGateway = new JSONObject();
+            configGateway.put("uuid", ccGateways.getId().toString());
+            configGateway.put("updateTime", ccGateways.getUpdateTime());
+            configGateway.put("gatewayAddr", ccGateways.getGwAddr());
+            configGateway.put("callerNumber", ccGateways.getCaller());
+            configGateway.put("calleePrefix", ccGateways.getCalleePrefix());
+            configGateway.put("callProfile", ccGateways.getProfileName());
+            configGateway.put("priority", ccGateways.getPriority());
+            configGateway.put("concurrency", ccGateways.getMaxConcurrency());
+            configGateway.put("register", ccGateways.getRegister());
+            configGateway.put("authUsername", ccGateways.getAuthUsername());
+            configGateway.put("audioCodec", ccGateways.getCodec());
+            gatewayList.add(configGateway);
+        }
+        JSONObject callConfig = new JSONObject();
+
+        String scriptServer = ccParamsService.getParamValueByCode("call-center-server-ip-addr", "");
+        String scriptPort = ccParamsService.getParamValueByCode("call-center-websocket-port", "");
+
+        callConfig.put("scriptServer", scriptServer);
+        callConfig.put("scriptPort", scriptPort);
+        callConfig.put("loginToken", loginToken);
+        callConfig.put("encryptPsw", encryptStr);
+        callConfig.put("gatewayList", gatewayList);
+        //登录账号
+        callConfig.put("opNum", opnum);
+        //登录用户名称
+        SysUser sysUser = userService.selectUserByLoginName(opnum);
+        if(sysUser != null){
+            callConfig.put("userName", sysUser.getUserName());
+        }
+
+        return AjaxResult.success(callConfig);
+
+    }
+    /**
+     * 查询未绑定的分机管理列表
+     */
+    @GetMapping("/extnum/selectUnBindCcExtNumList")
+    @ResponseBody
+    public AjaxResult selectUnBindCcExtNumList()
+    {
+        return AjaxResult.success(ccExtNumService.selectUnBindCcExtNumList());
+    }
+    /**
+     * 查询公司未绑定的分机管理列表
+     */
+    @PostMapping("/extnum/selectCompanyUnBindCcExtNumList")
+    @ResponseBody
+    public AjaxResult selectCompanyUnBindCcExtNumList(@RequestBody CcExtNum ccExtNum)
+    {
+        return AjaxResult.success(ccExtNumService.selectCcExtNumList(ccExtNum));
+    }
+    /**
+     * 查询未绑定的分机管理列表分页
+     */
+    @PostMapping("/extnum/selectUnBindCcExtNumListPage")
+    @ResponseBody
+    public TableDataInfo selectUnBindCcExtNumListPage(@RequestBody CcExtNum ccExtNum)
+    {
+        Integer pageNum = ccExtNum.getPageNum() != null ? ccExtNum.getPageNum().intValue() : 1;
+        Integer pageSize = ccExtNum.getPageSize() != null ? ccExtNum.getPageSize().intValue() : 10;
+        startPage(pageNum, pageSize);
+        List<CcExtNum> list = ccExtNumService.selectUnBindCcExtNumListPage(ccExtNum);
+        return getDataTable(list);
+    }
+    /**
+     * 通用查询分机列表分页
+     */
+    @PostMapping("/extnumPage")
+    @ResponseBody
+    public TableDataInfo extnumPage(@RequestBody CcExtNum ccExtNum)
+    {
+        Integer pageNum = ccExtNum.getPageNum() != null ? ccExtNum.getPageNum().intValue() : 1;
+        Integer pageSize = ccExtNum.getPageSize() != null ? ccExtNum.getPageSize().intValue() : 10;
+        startPage(pageNum, pageSize);
+        List<CcExtNum> list = ccExtNumService.selectCcExtNumList(ccExtNum);
+        if(!CollectionUtils.isEmpty(list)){
+            list.forEach(extNum -> {
+                if(StringUtils.isNotBlank(extNum.getUserCode()) && extNum.getUserCode().startsWith("_cp_")){
+                    extNum.setUserCode("");
+                }
+            });
+        }
+        return getDataTable(list);
+    }
+    /**
+     * 新增企业绑定未使用的分机(实际只是公司占位,需销售来绑定)
+     */
+    @PostMapping("/companyBindExtNum")
+    @ResponseBody
+    @Transactional
+    public AjaxResult companyBindExtNum(@RequestBody Map<String,String> param)
+    {
+        String companyName = param.get("companyName");
+        String sipExtNumIds = param.get("sipExtNumIds");
+        if(StringUtils.isBlank(companyName) || StringUtils.isBlank(sipExtNumIds)){
+            return AjaxResult.error("参数companyName或sipExtNumIds缺失");
+        }
+        //公司绑定分机
+        List<Long> extNumList = Arrays.stream(sipExtNumIds.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotBlank)
+                .map(Long::parseLong)
+                .collect(Collectors.toList());
+        CcExtNum extNum = new CcExtNum();
+        extNum.setUserCode(companyName);
+        extNum.setSipExtNumList(extNumList);
+        return toAjax(ccExtNumService.updateCompanyBindExtNum(extNum));
+    }
+
+    /**
+     * 公司批量解绑分机,根据分机号
+     */
+    @PostMapping("/companyBatchUnbindSipExt")
+    @ResponseBody
+    public AjaxResult companyBatchUnbindSipExt(@RequestBody Set<Long> extNums)
+    {
+        if(extNums == null || extNums.isEmpty()){
+            return AjaxResult.error("参数为空");
+        }
+        return toAjax(ccExtNumService.companyBatchUnbindSipExt(extNums));
+    }
+
+
+    /**
+     * 新增用户且绑定未使用的分机返回用户
+     */
+    @PostMapping("/user/addUserOrBindExtNumReturnUser")
+    @ResponseBody
+    @Transactional
+    public AjaxResult addUserOrBindExtNumReturnUser(@RequestBody SysUser user)
+    {
+        if(StringUtils.isBlank(user.getPassword())){
+            throw new RuntimeException("密码不能为空");
+        }
+
+        // 校验分机号是否为空
+        if (null == user.getExtNum()) {
+            throw new RuntimeException("新增用户失败,分机号不能为空");
+        }
+        // 校验分机号是否存在
+        CcExtNum extNum = ccExtNumService.selectCcExtNumByExtNum(user.getExtNum());
+        if (null == extNum) {
+            throw new RuntimeException("新增用户失败,分机号" + user.getExtNum() + "不存在");
+        }
+
+        user.setSalt(ShiroUtils.randomSalt());
+        user.setPassword(passwordService.encryptPassword(user.getLoginName(), user.getPassword(), user.getSalt()));
+        user.setPwdUpdateDate(DateUtils.getNowDate());
+        user.setCreateBy("ylrz");
+        int i;
+        SysUser sysUser = userService.selectUserByLoginName(user.getLoginName());
+        if (sysUser != null)
+        {
+            //优化一下,如果账号存在就更新账号
+            user.setUserId(sysUser.getUserId());
+            i = userService.updateUser(user);
+        }else{
+            i = userService.insertUser(user);
+        }
+
+        if(i>0){
+            //绑定分机
+            extNum.setUserCode(user.getLoginName());
+            int num = ccExtNumService.updateCcExtNum(extNum);
+            if(num>0){
+                user.setExtPass(extNum.getExtPass());
+                return AjaxResult.success(user);
+            } else {
+                throw new RuntimeException("用户信息处理成功,但分机号绑定失败");
+            }
+        }
+        throw new RuntimeException("新增用户和绑分机操作失败");
+    }
+    /**
+     * 修改用户且绑分机
+     */
+    @PostMapping("/user/editUserOrUnBindExtNum")
+    @ResponseBody
+    @Transactional
+    public AjaxResult editUserOrUnBindExtNum(@RequestBody SysUser user)
+    {
+        if (!userService.checkLoginNameUnique(user))
+        {
+            throw new RuntimeException("修改用户'" + user.getLoginName() + "'失败,登录账号已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
+        {
+            throw new RuntimeException("修改用户'" + user.getLoginName() + "'失败,手机号码已存在");
+        }
+        user.setUpdateBy("ylrz");
+        AuthorizationUtils.clearAllCachedAuthorizationInfo();
+        int i = userService.updateUser(user);
+        if(i>0){
+            int cleanNum;
+            if(StringUtils.isNotBlank(user.getUserSource()) && user.getUserSource().equals("0")){
+                cleanNum = ccExtNumService.updateUserCodeByLoginName(user.getCompanyName(),user.getLoginName());
+            }else{
+                //先清除原分机绑定
+                cleanNum = ccExtNumService.cleanCcExtNumByUserCode(user.getLoginName());
+            }
+            if(cleanNum>0){
+                //修改绑定分机
+                CcExtNum extNum = new CcExtNum();
+                extNum.setExtNum(user.getExtNum());
+                extNum.setUserCode(user.getLoginName());
+                int updateNum = ccExtNumService.updateCcExtNumByUserCode(extNum);
+                if(updateNum>0){
+                    CcExtNum num = ccExtNumService.selectCcExtNumByExtNum(user.getExtNum());
+                    if(null != num){
+                        user.setExtPass(num.getExtPass());
+                        return AjaxResult.success(user);
+                    }
+                }
+            }
+        }
+        throw new RuntimeException("修改用户失败");
+    }
+
+    /**
+     * 删除用户且解绑分机
+     */
+    @PostMapping("/deleteUserOrunBindExtNum")
+    @ResponseBody
+    @Transactional
+    public AjaxResult deleteUserOrunBindExtNum(@RequestBody SysUser user)
+    {
+        //先解绑分机
+        CcExtNum extNum = new CcExtNum();
+        extNum.setExtNum(user.getExtNum());
+        extNum.setUserCode(user.getUserCode());
+        int updateNum = ccExtNumService.updateCcExtNumByUserCode(extNum);
+        if(updateNum>0){
+            //删除用户
+            int i = userService.deleteUserById(user.getUserId());
+            if(i>0){
+                return AjaxResult.success();
+            }
+        }
+        throw new RuntimeException("删除用户且解绑分机失败");
+    }
+
+    /**
+     * 获取手动外呼客户沟通信息
+     * @param phoneNum 手机号
+     * @param callType 类型  1呼入 2外呼
+     * @param uuid  通话uuid
+     */
+    @GetMapping("/getCustCommunicationInfo")
+    @ResponseBody
+    public AjaxResult getCustCommunicationInfo(@RequestParam("phoneNum") String phoneNum, @RequestParam("callType") Integer callType, @RequestParam("uuid") String uuid)
+    {
+        Map<String,Object> mmap = new HashMap<>();
+        CcCustInfo ccCustInfo = ccCustInfoService.selectCcCustInfoByPhoneNum(phoneNum);
+        if (null == ccCustInfo) {
+            ccCustInfo = new CcCustInfo();
+            ccCustInfo.setCallRecordList(new ArrayList<>());
+        } else {
+            ccCustInfo.setCallRecordList(ccCustCallRecordService.selectCcCustCallRecordList(new CcCustCallRecord().setCustId(ccCustInfo.getId())));
+        }
+        ccCustInfo.setPhoneNum(phoneNum);
+        mmap.put("ccCustInfo", ccCustInfo);
+        mmap.put("callType", callType);
+        mmap.put("uuid", uuid);
+        // 省下拉框
+        List<SysDivisionData> sysDivisionData = sysDivisionDataService.selectSysDivisionDataList(null);
+        List<SysDivisionData> provinces = sysDivisionData.stream()
+                .filter(d -> d.getDeep() == 0)
+                .collect(Collectors.toList());
+        mmap.put("provinces", provinces);
+        // 市下拉框
+        List<SysDivisionData> citys = sysDivisionData.stream()
+                .filter(d -> d.getDeep() == 1)
+                .collect(Collectors.toList());
+        mmap.put("citys", citys);
+        // 区县下拉框
+        List<SysDivisionData> countys = sysDivisionData.stream()
+                .filter(d -> d.getDeep() == 2)
+                .collect(Collectors.toList());
+        mmap.put("countys", countys);
+        return AjaxResult.success(mmap);
+    }
+
+    /**
+     * 新增保存手动外呼沟通记录
+     */
+    @PostMapping("/add/custcallrecord")
+    @ResponseBody
+    public AjaxResult addAustcallrecord(@RequestBody CcCustInfo ccCustInfo)
+    {
+        ccCustInfoService.updateCcCustInfo(ccCustInfo);
+        CcCustInfo custInfoBak = ccCustInfoService.selectCcCustInfoByPhoneNum(ccCustInfo.getPhoneNum());
+        CcCustCallRecord callRecord = JSONObject.parseObject(ccCustInfo.getCallRecord(), CcCustCallRecord.class);
+        callRecord.setCustId(custInfoBak.getId());
+        //这里改成查询
+
+        callRecord.setUserId(ccCustInfo.getOpNum());
+        callRecord.setUserRealName(ccCustInfo.getUserName());
+        callRecord.setCreateTime(new Date());
+        CcCustCallRecord hisCallRecord = ccCustCallRecordService.selectCcCustCallRecordByUuid(callRecord.getUuid());
+        if (null == hisCallRecord) {
+            return toAjax(ccCustCallRecordService.insertCcCustCallRecord(callRecord));
+        } else {
+            callRecord.setId(hisCallRecord.getId());
+            return toAjax(ccCustCallRecordService.updateCcCustCallRecord(callRecord));
+        }
+    }
+    /**
+     * 查询手动外呼记录列表
+     */
+    @PostMapping("/outboundcdrList")
+    @ResponseBody
+    public TableDataInfo outboundcdrList(@RequestBody CcOutboundCdr ccOutboundCdr)
+    {
+        startPage();
+        List<CcOutboundCdr> list = ccOutboundCdrService.selectCcOutboundCdrList(ccOutboundCdr);
+        for (CcOutboundCdr data: list) {
+            data.setWavFileUrl("/recordings/files?filename=" + data.getRecordFilename());
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 通话记录查询接口(返回完整的数据表格式)
+     */
+    @PostMapping("/call/phone/records")
+    @ResponseBody
+    public TableDataInfo getcallPhoneRecords(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()
+                && null == queryParams.getPageSize()) {
+            queryParams.setPageNum(1);
+            queryParams.setPageSize(200000);
+        }
+        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;
+        }
+
+        // 01:呼入, 02:AI外呼, 03:人工外呼
+        if ("01".equals(callType)) {
+            return getInboundRecords(queryParams);
+        } else if ("02".equals(callType)) {
+            return getAiCallRecordsTable(queryParams);
+        } else if ("03".equals(callType)) {
+            return getOutboundRecordsTable(queryParams);
+        } else {
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.INVALID_PARAM.value());
+            tableDataInfo.setMsg("callType参数不合法,呼入请输入01,AI外呼请输入02,手工外呼请输入03!");
+            return tableDataInfo;
+        }
+    }
+    //ai外呼记录查询
+    private TableDataInfo getAiCallRecordsTable(ApiCallRecordQueryParams queryParams) {
+        Map<String, Object> params = new HashMap<>();
+        if (null != queryParams.getTimeLenStart()) {
+            params.put("timeLenStart", queryParams.getTimeLenStart());
+        }
+        if (null != queryParams.getTimeLenEnd()) {
+            params.put("timeLenEnd", queryParams.getTimeLenEnd());
+        }
+        if (null != queryParams.getCalloutTimeStart()) {
+            params.put("calloutTimeStart", queryParams.getCalloutTimeStart());
+        }
+        if (null != queryParams.getCalloutTimeEnd()) {
+            params.put("calloutTimeEnd", queryParams.getCalloutTimeEnd());
+        }
+        if (null != queryParams.getAnsweredTimeStart()) {
+            params.put("answeredTimeStart", queryParams.getAnsweredTimeStart());
+        }
+        if (null != queryParams.getAnsweredTimeEnd()) {
+            params.put("answeredTimeEnd", queryParams.getAnsweredTimeEnd());
+        }
+        if (null != queryParams.getCallEndTimeStart()) {
+            params.put("callEndTimeStart", queryParams.getCallEndTimeStart());
+        }
+        if (null != queryParams.getCallEndTimeEnd()) {
+            params.put("callEndTimeEnd", queryParams.getCallEndTimeEnd());
+        }
+        CcCallPhone ccCallPhone = new CcCallPhone();
+        if (null != queryParams.getBatchId() && queryParams.getBatchId() > 0) {
+            ccCallPhone.setBatchId(queryParams.getBatchId());
+        }
+        ccCallPhone.setUuid(queryParams.getUuid());
+        ccCallPhone.setTelephone(queryParams.getTelephone());
+        ccCallPhone.setAcdOpnum(queryParams.getExtnum());
+        ccCallPhone.setCallstatus(queryParams.getCallstatus());
+        ccCallPhone.setCallerNumber(queryParams.getCallerNumber());
+        ccCallPhone.setParams(params);
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneYlrzList(ccCallPhone);
+        list.forEach(callPhoneRecord -> {
+            if(StringUtils.isNotBlank(callPhoneRecord.getWavfile())){
+                if (callPhoneRecord.getWavfile().startsWith("/")) {
+                    callPhoneRecord.setWavfile("/recordings/files?filename=" + callPhoneRecord.getWavfile().substring(1));
+                }else{
+                    callPhoneRecord.setWavfile("/recordings/files?filename=" + callPhoneRecord.getWavfile());
+                }
+            }
+            callPhoneRecord.setCallstatusName( CcCallPhone.getCallStatusName(callPhoneRecord.getCallstatus()));
+            callPhoneRecord.setCalloutTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getCalloutTime())));
+            callPhoneRecord.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getAnsweredTime())));
+            callPhoneRecord.setCallEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getCallEndTime())));
+            callPhoneRecord.setTimeLenSec(DateUtils.formatTimeLength(callPhoneRecord.getTimeLen()/1000));
+        });
+        return getDataTable(list);
+    }
+    //人工外呼记录查询
+    private TableDataInfo getOutboundRecordsTable(ApiCallRecordQueryParams queryParams) {
+        Map<String, Object> params = new HashMap<>();
+        if (null != queryParams.getTimeLenStart()) {
+            params.put("timeLenStart", queryParams.getTimeLenStart());
+        }
+        if (null != queryParams.getTimeLenEnd()) {
+            params.put("timeLenEnd", queryParams.getTimeLenEnd());
+        }
+        if (null != queryParams.getCalloutTimeStart()) {
+            params.put("calloutTimeStart", queryParams.getCalloutTimeStart());
+        }
+        if (null != queryParams.getCalloutTimeEnd()) {
+            params.put("calloutTimeEnd", queryParams.getCalloutTimeEnd());
+        }
+        if (null != queryParams.getAnsweredTimeStart()) {
+            params.put("answeredTimeStart", queryParams.getAnsweredTimeStart());
+        }
+        if (null != queryParams.getAnsweredTimeEnd()) {
+            params.put("answeredTimeEnd", queryParams.getAnsweredTimeEnd());
+        }
+        if (null != queryParams.getEndTimeStart()) {
+            params.put("endTimeStart", queryParams.getEndTimeStart());
+        }
+        if (null != queryParams.getEndTimeEnd()) {
+            params.put("endTimeEnd", queryParams.getEndTimeEnd());
+        }
+        CcCallPhone ccCallPhone = new CcCallPhone();
+        if (null != queryParams.getBatchId() && queryParams.getBatchId() > 0) {
+            ccCallPhone.setBatchId(queryParams.getBatchId());
+        }
+
+        CcOutboundCdr outboundCdr = new CcOutboundCdr();
+        outboundCdr.setUuid(queryParams.getUuid());
+        outboundCdr.setCaller(queryParams.getTelephone());
+        outboundCdr.setOpnum(queryParams.getExtnum());
+        outboundCdr.setParams(params);
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        List<CcOutboundCdr> list = outboundCdrService.selectCcOutboundCdrYlrzList(outboundCdr);
+        list.forEach(callPhoneRecord -> {
+            if(StringUtils.isNotBlank(callPhoneRecord.getRecordFilename())){
+                if (callPhoneRecord.getRecordFilename().startsWith("/")) {
+                    callPhoneRecord.setRecordFilename("/recordings/files?filename=" + callPhoneRecord.getRecordFilename().substring(1));
+                }else{
+                    callPhoneRecord.setWavFileUrl("/recordings/files?filename=" + callPhoneRecord.getRecordFilename());
+                }
+                callPhoneRecord.setStartTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getStartTime())));
+                callPhoneRecord.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getAnsweredTime())));
+                callPhoneRecord.setEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(callPhoneRecord.getEndTime())));
+                callPhoneRecord.setTimeLenSec(DateUtils.formatTimeLength(callPhoneRecord.getTimeLen()/1000));
+                callPhoneRecord.setTimeLenValidStr(DateUtils.formatTimeLength(callPhoneRecord.getTimeLenValid()/1000));
+            }
+
+        });
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取外呼网关列表接口
+     * purposes 1手动外呼电话条,2AI外呼,3不限制
+     * @param queryParams 网关参数
+     * @param req
+     */
+    @PostMapping("/gateway/myList")
+    @ResponseBody
+    public TableDataInfo getGatewayMyList(@RequestBody CcGateways queryParams, HttpServletRequest req){
+        // 校验请求方ip是否合法
+        if (!ClientIpCheck.checkIp(req)) {
+            TableDataInfo tableDataInfo;
+            tableDataInfo = new TableDataInfo();
+            tableDataInfo.setTotal(0);
+            tableDataInfo.setCode(AjaxResult.Type.NO_AUTH.value());
+            tableDataInfo.setMsg("未授权,请联系系统管理员添加ip白名单!");
+            return tableDataInfo;
+        }
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        return getDataTable(ccGatewaysService.selectCcGatewaysList(queryParams));
+    }
+    /**
+     * 根据通话id集合查询uuid不为空的自动外呼数据
+     */
+    @PostMapping( "/getCcCallPhoneByIds")
+    @ResponseBody
+    public AjaxResult getCcCallPhoneByIds(@RequestBody List<String> callPhoneIds)
+    {
+        if(callPhoneIds==null){
+            return AjaxResult.error(AjaxResult.Type.INVALID_PARAM, "callPhoneIds不能为空!", "");
+        }
+        List<CcCallPhone> list = ccCallPhoneService.selectCcCallPhoneListByIds(callPhoneIds);
+        if(!CollectionUtils.isEmpty(list)){
+            //处理下大字段不要传输
+            list.forEach(callPhoneRecord -> {
+                callPhoneRecord.setDialogue(null);
+                callPhoneRecord.setBizJson(null);
+                callPhoneRecord.setRecordServerUrl(null);
+                callPhoneRecord.setIvrDtmfDigits( null);
+            });
+        }
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 修改任务
+     * @param ccCallTask
+     * @return
+     */
+    @PostMapping("/editTask")
+    @ResponseBody
+    public AjaxResult editTask(@RequestBody 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));
+    }
+
+    /**
+     * 获取音色分页列表
+     */
+    @PostMapping("/voicecodePage")
+    @ResponseBody
+    public TableDataInfo voicecodePage(@RequestBody CcTtsAliyun queryParams)
+    {
+        if(queryParams==null){
+            queryParams = new CcTtsAliyun();
+        }
+        if(queryParams.getPageNum()==null){
+            queryParams.setPageNum(1);
+        }
+        if(queryParams.getPageSize()== null){
+            queryParams.setPageSize(10);
+        }
+        startPage(queryParams.getPageNum(), queryParams.getPageSize());
+        // 获取音色列表
+        List<CcTtsAliyun> list = ccTtsAliyunService.selectCcTtsAliyunList(queryParams);
+
+        return getDataTable(list);
+    }
 }

+ 55 - 1
ruoyi-admin/src/main/java/com/ruoyi/aicall/domain/CcCallPhone.java

@@ -171,5 +171,59 @@ public class CcCallPhone implements Serializable {
     /** The duration of the manual agent service time. */
     private String manualAnsweredTimeLen;
 
-
+    private String callstatusName;
+    private String calloutTimeStr;
+    private String answeredTimeStr;
+    private String callEndTimeStr;
+    private String timeLenSec;
+
+    /**
+     * 根据呼叫状态码获取状态名称
+     * @param status 状态码
+     * @return 状态名称
+     */
+    public static String getCallStatusName(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        switch (status) {
+            case 0:
+                return "未拨打";
+            case 1:
+                return "已进入呼叫队列";
+            case 2:
+                return "正在拨号";
+            case 3:
+            case 30:
+                return "未接通";
+            case 4:
+                return "已接通";
+            case 5:
+                return "通话中断";
+            case 6:
+                return "成功转人工或 AI";
+            case 7:
+                return "线路故障";
+            case 31:
+                return "客户正在通话中";
+            case 32:
+                return "关机";
+            case 33:
+                return "空号";
+            case 34:
+                return "无人接听";
+            case 35:
+                return "停机";
+            case 36:
+                return "网络忙";
+            case 37:
+                return "语音助手";
+            case 38:
+                return "暂时无法接通";
+            case 39:
+                return "呼叫限制";
+            default:
+                return "未知状态";
+        }
+    }
 }

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

@@ -56,4 +56,6 @@ public class CcTtsAliyun implements Serializable {
 
     /** Models of the voice */
     private String ttsModels;
+    private Integer pageNum;
+    private Integer pageSize;
 }

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

@@ -89,4 +89,8 @@ public interface CcCallPhoneMapper
     void bakCallPhoneByBatchId(Long batchId);
 
     void delCallPhoneByBatchId(Long batchId);
+
+    List<CcCallPhone> selectCcCallPhoneYlrzList(CcCallPhone ccCallPhone);
+
+    List<CcCallPhone> selectCcCallPhoneListByIds(List<String> callPhoneIds);
 }

+ 10 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/model/ApiCallRecordQueryParams.java

@@ -47,4 +47,14 @@ public class ApiCallRecordQueryParams implements Serializable {
     /** 主叫号码 */
     private String callerNumber;
 
+
+    /** 接听时间起止 */
+    private String answeredTimeStart;
+    private String answeredTimeEnd;
+    /** 自动外呼挂机时间起止 */
+    private String callEndTimeStart;
+    private String callEndTimeEnd;
+    /** 手动外呼挂机时间起止 */
+    private String endTimeStart;
+    private String endTimeEnd;
 }

+ 23 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/model/ApiCallTaskModel.java

@@ -40,6 +40,11 @@ public class ApiCallTaskModel implements Serializable {
     /** 音源 */
     private String voiceSource;
 
+    /**
+     * tts模型
+     */
+    private String ttsModels;
+
     /** 播放次数 */
     private Integer playTimes;
 
@@ -55,4 +60,22 @@ public class ApiCallTaskModel implements Serializable {
     /** The duration of form filling after the call ends; seconds */
     private Double avgCallEndProcessTimeLen;
 
+    /** aiTransferType */
+    private String aiTransferType;
+
+    /** aiTransferData */
+    private String aiTransferData;
+
+    /** aiTransferGroupId */
+    private String aiTransferGroupId;
+
+    /** aiTransferGatewayId */
+    private String aiTransferGatewayId;
+
+    /** aiTransferGatewayDestNumber */
+    private String aiTransferGatewayDestNumber;
+
+    /** aiTransferExtNumber */
+    private String aiTransferExtNumber;
+
 }

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

@@ -87,4 +87,8 @@ public interface ICcCallPhoneService
     void bakCallPhoneByBatchId(Long batchId);
 
     void delCallPhoneByBatchId(Long batchId);
+
+    List<CcCallPhone> selectCcCallPhoneYlrzList(CcCallPhone ccCallPhone);
+
+    List<CcCallPhone> selectCcCallPhoneListByIds(List<String> callPhoneIds);
 }

+ 53 - 0
ruoyi-admin/src/main/java/com/ruoyi/aicall/service/impl/CcCallPhoneServiceImpl.java

@@ -1,5 +1,6 @@
 package com.ruoyi.aicall.service.impl;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -224,4 +225,56 @@ public class CcCallPhoneServiceImpl implements ICcCallPhoneService
     public void delCallPhoneByBatchId(Long batchId) {
         ccCallPhoneMapper.delCallPhoneByBatchId(batchId);
     }
+    /**
+     * 查询外呼号码列表
+     *
+     * @param ccCallPhone 外呼号码
+     * @return 外呼号码
+     */
+    @Override
+    public List<CcCallPhone> selectCcCallPhoneYlrzList(CcCallPhone ccCallPhone)
+    {
+        Map<String, Object> params = ccCallPhone.getParams();
+        if (null == params) {
+            params = new HashMap<>();
+        }
+        if (null != params.get("timeLenStart")
+                && !"".equals(params.get("timeLenStart"))) {
+            params.put("timeLenStart", Double.valueOf((String)params.get("timeLenStart")) * 60 * 1000L);
+        }
+        if (null != params.get("timeLenEnd")
+                && !"".equals(params.get("timeLenEnd"))) {
+            params.put("timeLenEnd", Double.valueOf((String)params.get("timeLenEnd")) * 60 * 1000L);
+        }
+        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("answeredTimeStart")
+                && !"".equals(params.get("answeredTimeStart"))) {
+            params.put("answeredTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("answeredTimeStart")).getTime());
+        }
+        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 (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());
+        }
+        ccCallPhone.setParams(params);
+        return ccCallPhoneMapper.selectCcCallPhoneYlrzList(ccCallPhone);
+    }
+    @Override
+    public List<CcCallPhone> selectCcCallPhoneListByIds(List<String> callPhoneIds) {
+        return ccCallPhoneMapper.selectCcCallPhoneListByIds(callPhoneIds);
+    }
 }

+ 27 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/controller/CcExtNumController.java

@@ -155,4 +155,31 @@ public class CcExtNumController extends BaseController
         List<CcExtNum> list = ccExtNumService.selectCcExtNumList(new CcExtNum());
         return AjaxResult.success(list);
     }
+
+    /**
+     * 批量新增分机管理页面
+     */
+    @GetMapping("/batchAdd")
+    public String batchAdd()
+    {
+        return prefix + "/batchAdd";
+    }
+
+    /**
+     * 批量新增保存分机管理
+     */
+    @RequiresPermissions("cc:extnum:batchAdd")
+    @Log(title = "分机管理-批量新增", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAdd")
+    @ResponseBody
+    public AjaxResult batchAddSave(int count, String extPass, String userCode, Long startExtNum) {
+        try {
+            int successCount = ccExtNumService.batchInsertCcExtNum(count, extPass, userCode, startExtNum);
+            return AjaxResult.success("成功生成" + successCount + "个分机号");
+        } catch (RuntimeException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("批量新增失败:" + e.getMessage());
+        }
+    }
 }

+ 4 - 1
ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcCustInfo.java

@@ -80,5 +80,8 @@ public class CcCustInfo implements Serializable {
 
     // 详情带的参数
     private List<CcCustCallRecord> callRecordList;
-
+    //登陆账号
+    private String opNum;
+    //用户名称
+    private String userName;
 }

+ 10 - 4
ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcExtNum.java

@@ -2,12 +2,9 @@ package com.ruoyi.cc.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;
+import java.util.List;
 
 /**
  * 分机号对象 cc_ext_num
@@ -32,4 +29,13 @@ public class CcExtNum implements Serializable {
     /** 所属员工/绑定关系 */
     private String userCode;
 
+    private Long pageNum;
+    private Long pageSize;
+
+    /** 分机起 */
+    private Long startExtNum;
+    /** 分机止 */
+    private Long endExtNum;
+    /** 分机号集合 */
+    private List<Long> sipExtNumList;
 }

+ 5 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcGateways.java

@@ -11,6 +11,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
 import java.io.Serializable;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -79,5 +80,9 @@ public class CcGateways implements Serializable {
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private Map<String, Object> params = new HashMap<>();
 
+    private Integer pageSize;
 
+    private Integer pageNum;
+
+    private List<Long> gatewayIds;
 }

+ 5 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/domain/CcOutboundCdr.java

@@ -71,4 +71,9 @@ public class CcOutboundCdr implements Serializable {
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private Map<String, Object> params;
 
+    private String startTimeStr;
+    private String answeredTimeStr;
+    private String endTimeStr;
+    private String timeLenSec;
+    private String timeLenValidStr;
 }

+ 42 - 1
ruoyi-admin/src/main/java/com/ruoyi/cc/mapper/CcExtNumMapper.java

@@ -1,7 +1,10 @@
 package com.ruoyi.cc.mapper;
 
 import java.util.List;
+import java.util.Set;
+
 import com.ruoyi.cc.domain.CcExtNum;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 分机信息Mapper接口
@@ -61,7 +64,45 @@ public interface CcExtNumMapper
 
     /**
      * 获取未分配的分机
-     * @return
      */
     List<CcExtNum> selectUnBindCcExtNumList();
+
+    int cleanCcExtNumByUserCode(String loginName);
+
+    int updateCcExtNumByUserCode(CcExtNum extNum);
+
+    List<CcExtNum> selectUnBindCcExtNumListPage(CcExtNum ccExtNum);
+
+    int updateCompanyBindExtNum(CcExtNum extNum);
+
+    int companyUnbindExtNum(List<String> userCodeList);
+
+    /**
+     * 批量新增分机信息
+     * 
+     * @param extNumList 分机信息列表
+     * @return 结果
+     */
+    int batchInsertCcExtNum(List<CcExtNum> extNumList);
+
+    /**
+     * 查询大于指定值的所有分机号(用于批量生成分机号时优化性能)
+     * 
+     * @param minExtNum 最小分机号(不包含)
+     * @return 分机号集合
+     */
+    List<Long> selectExtNumsGreaterThan(Long minExtNum);
+
+    /**
+     * 查询当前最大的分机号
+     * 
+     * @return 最大分机号,如果没有数据则返回null
+     */
+    Long selectMaxExtNum();
+
+    int companyBatchUnbindSipExt(@Param("extNums") Set<Long> extNums);
+    /**
+     * 销售归还公司分机
+     */
+    int updateUserCodeByLoginName(@Param("companyName") String companyName,@Param("loginName") String loginName);
 }

+ 24 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/service/ICcExtNumService.java

@@ -1,6 +1,8 @@
 package com.ruoyi.cc.service;
 
 import java.util.List;
+import java.util.Set;
+
 import com.ruoyi.cc.domain.CcExtNum;
 
 /**
@@ -89,4 +91,26 @@ public interface ICcExtNumService
      * @return
      */
     List<CcExtNum> selectUnBindCcExtNumList();
+
+    int cleanCcExtNumByUserCode(String loginName);
+
+    int updateCcExtNumByUserCode(CcExtNum extNum);
+
+    List<CcExtNum> selectUnBindCcExtNumListPage(CcExtNum ccExtNum);
+
+    int updateCompanyBindExtNum(CcExtNum extNum);
+
+    /**
+     * 批量新增分机
+     * @param count 生成数量
+     * @param extPass 分机密码
+     * @param userCode 绑定工号(可选)
+     * @param startExtNum 起始分机号(可选,不填则从上次最大分机号+1开始)
+     * @return 结果
+     */
+    int batchInsertCcExtNum(int count, String extPass, String userCode, Long startExtNum);
+
+    int companyBatchUnbindSipExt(Set<Long> extNums);
+
+    int updateUserCodeByLoginName(String companyName, String loginName);
 }

+ 2 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/service/ICcOutboundCdrService.java

@@ -58,4 +58,6 @@ public interface ICcOutboundCdrService
      * @return 结果
      */
     public int deleteCcOutboundCdrById(String id);
+
+    List<CcOutboundCdr> selectCcOutboundCdrYlrzList(CcOutboundCdr outboundCdr);
 }

+ 205 - 10
ruoyi-admin/src/main/java/com/ruoyi/cc/service/impl/CcExtNumServiceImpl.java

@@ -1,22 +1,18 @@
 package com.ruoyi.cc.service.impl;
 
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import com.auth0.jwt.JWT;
 import com.auth0.jwt.algorithms.Algorithm;
 import com.ruoyi.cc.service.ICcParamsService;
+import com.ruoyi.common.utils.StringUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import com.ruoyi.cc.mapper.CcExtNumMapper;
 import com.ruoyi.cc.domain.CcExtNum;
 import com.ruoyi.cc.service.ICcExtNumService;
 import com.ruoyi.common.core.text.Convert;
-import org.springframework.util.CollectionUtils;
 
 /**
  * 【请填写功能名称】Service业务层处理
@@ -33,6 +29,13 @@ public class CcExtNumServiceImpl implements ICcExtNumService
     @Autowired
     private ICcParamsService ccParamsService;
     private String authTokenSecret;
+    
+    // 缓存最大分机号,初始值为0表示未初始化,需要从数据库查询
+    // 使用volatile保证多线程可见性
+    private volatile long cachedMaxExtNum = 0;
+    
+    // 用于同步获取和更新缓存的锁对象
+    private final Object cacheLock = new Object();
 
     /**
      * 查询【请填写功能名称】
@@ -109,7 +112,7 @@ public class CcExtNumServiceImpl implements ICcExtNumService
     @Override
     public CcExtNum selectCcExtNumByExtNum(Long extNum) {
         List<CcExtNum> list = selectCcExtNumList(new CcExtNum().setExtNum(extNum));
-        if (list.size() > 0) {
+        if (!list.isEmpty()) {
             return list.get(0);
         }
         return null;
@@ -118,7 +121,7 @@ public class CcExtNumServiceImpl implements ICcExtNumService
     @Override
     public CcExtNum selectCcExtNumByUserCode(String userCode) {
         List<CcExtNum> list = selectCcExtNumList(new CcExtNum().setUserCode(userCode));
-        if (list.size() > 0) {
+        if (!list.isEmpty()) {
             return list.get(0);
         }
         return null;
@@ -137,7 +140,7 @@ public class CcExtNumServiceImpl implements ICcExtNumService
             Map<String, Object> map = new HashMap<>();
             Calendar instance = Calendar.getInstance();
             instance.add(Calendar.HOUR, 24);
-            String token = JWT.create()
+            return JWT.create()
                     //添加头部
                     .withHeader(map)
                     //添加payload
@@ -150,7 +153,6 @@ public class CcExtNumServiceImpl implements ICcExtNumService
                     .withExpiresAt(instance.getTime())
                     //设置签名 密钥
                     .sign(Algorithm.HMAC256(authTokenSecret));
-            return token;
 
         } catch (Exception err) {
             log.error("error:" + err.getMessage());
@@ -162,4 +164,197 @@ public class CcExtNumServiceImpl implements ICcExtNumService
     public List<CcExtNum> selectUnBindCcExtNumList() {
         return ccExtNumMapper.selectUnBindCcExtNumList();
     }
+    @Override
+    public int cleanCcExtNumByUserCode(String loginName) {
+        return ccExtNumMapper.cleanCcExtNumByUserCode(loginName);
+    }
+    @Override
+    public int updateCcExtNumByUserCode(CcExtNum extNum) {
+        return ccExtNumMapper.updateCcExtNumByUserCode(extNum);
+    }
+
+    @Override
+    public List<CcExtNum> selectUnBindCcExtNumListPage(CcExtNum ccExtNum) {
+        return ccExtNumMapper.selectUnBindCcExtNumListPage(ccExtNum);
+    }
+
+    @Override
+    public int updateCompanyBindExtNum(CcExtNum extNum) {
+        return ccExtNumMapper.updateCompanyBindExtNum(extNum);
+    }
+
+    @Override
+    public int batchInsertCcExtNum(int count, String extPass, String userCode, Long startExtNum) {
+        // ==================== 1. 参数校验(安全检查)====================
+        if (count <= 0 || count > 10000) {
+            throw new RuntimeException("生成数量必须在1-10000之间");
+        }
+        
+        // 密码可以为空字符串,但不能为null
+        if (extPass == null) {
+            extPass = "";
+        }
+        
+        // 密码长度校验(最多50个字符)
+        if (extPass.length() > 50) {
+            throw new RuntimeException("分机密码长度不能超过50个字符");
+        }
+        
+        // 工号长度校验(最多32个字符)
+        if (StringUtils.isNotEmpty(userCode) && userCode.length() > 32) {
+            throw new RuntimeException("工号长度不能超过32个字符");
+        }
+        
+        // ==================== 2. 工号唯一性校验 ====================
+        if (StringUtils.isNotEmpty(userCode)) {
+            CcExtNum checkUserCode = selectCcExtNumByUserCode(userCode);
+            if (checkUserCode != null) {
+                throw new RuntimeException("该工号已经绑定分机" + checkUserCode.getExtNum() + ",不允许重复绑定!");
+            }
+        }
+        
+        // ==================== 3. 确定起始分机号(使用缓存+数据库双重保障)====================
+        long actualStartExtNum;
+        long currentMaxExtNum;
+        
+        synchronized (cacheLock) {
+            // 如果缓存为0,说明是第一次使用或系统刚重启,需要从数据库查询
+            if (cachedMaxExtNum == 0) {
+                Long dbMaxExtNum = ccExtNumMapper.selectMaxExtNum();
+                currentMaxExtNum = (dbMaxExtNum == null) ? 0L : dbMaxExtNum;
+                cachedMaxExtNum = currentMaxExtNum; // 更新缓存
+                log.info("首次初始化分机号缓存,数据库最大分机号: {}", currentMaxExtNum);
+            } else {
+                currentMaxExtNum = cachedMaxExtNum;
+            }
+            
+            // 确定实际起始分机号
+            if (startExtNum == null || startExtNum <= 0) {
+                actualStartExtNum = currentMaxExtNum + 1;
+                if (actualStartExtNum <= 0) {
+                    actualStartExtNum = 1L; // 确保起始值至少为1
+                }
+            } else {
+                // 校验用户指定的起始分机号不能小于当前最大值
+                if (startExtNum <= currentMaxExtNum) {
+                    throw new RuntimeException("起始分机号(" + startExtNum + ")不能小于等于当前最大分机号(" + currentMaxExtNum + "),可选择不填起始分机号");
+                }
+                actualStartExtNum = startExtNum;
+            }
+        }
+        
+        // ==================== 4. 查询已存在的分机号(用于去重)====================
+        List<Long> existingExtNums = ccExtNumMapper.selectExtNumsGreaterThan(actualStartExtNum - 1);
+        Set<Long> existingExtNumSet = new HashSet<>(existingExtNums);
+        
+        // ==================== 5. 分批生成分机号并插入(防止内存溢出)====================
+        int totalInserted = 0;
+        long currentExtNum = actualStartExtNum;
+        int remainingCount = count;
+        
+        // 分机号最大值限制(防止Long溢出)
+        final long MAX_EXT_NUM = Long.MAX_VALUE - 10000; // 预留安全空间
+        
+        // 外层批次大小:每次生成1000个分机号
+        final int GENERATE_BATCH_SIZE = 1000;
+        // 内层插入批次大小:每次插入500条到数据库
+        final int INSERT_BATCH_SIZE = 500;
+        
+        try {
+            while (remainingCount > 0) {
+                // 计算当前批次要生成的数量
+                int currentBatchSize = Math.min(GENERATE_BATCH_SIZE, remainingCount);
+                List<CcExtNum> currentBatchList = new ArrayList<>(currentBatchSize);
+                
+                // 生成当前批次的分机号
+                int generatedInBatch = 0;
+                long attempts = 0;
+                // 防死循环:每个批次最多尝试 currentBatchSize * 10 次
+                long maxAttemptsPerBatch = (long) currentBatchSize * 10L;
+                
+                while (generatedInBatch < currentBatchSize) {
+                    attempts++;
+                    
+                    // 【关键】安全检查1:防止死循环
+                    if (attempts > maxAttemptsPerBatch) {
+                        String errorMsg = String.format(
+                            "生成分机号失败:在区间[%d, %d]内可用分机号不足(已尝试%d次)。建议:1)删除部分已有分机号 2)指定更大的起始位置",
+                            actualStartExtNum, currentExtNum, attempts
+                        );
+                        log.error(errorMsg);
+                        throw new RuntimeException(errorMsg);
+                    }
+                    
+                    // 【关键】安全检查2:防止Long溢出
+                    if (currentExtNum <= 0 || currentExtNum >= MAX_EXT_NUM) {
+                        String errorMsg = String.format(
+                            "分机号已达到系统上限(currentExtNum=%d, MAX_EXT_NUM=%d),无法继续生成",
+                            currentExtNum, MAX_EXT_NUM
+                        );
+                        log.error(errorMsg);
+                        throw new RuntimeException(errorMsg);
+                    }
+                    
+                    // 如果当前分机号不存在,则添加到当前批次列表
+                    if (!existingExtNumSet.contains(currentExtNum)) {
+                        CcExtNum newExt = new CcExtNum();
+                        newExt.setExtNum(currentExtNum);
+                        newExt.setExtPass(extPass);
+                        newExt.setUserCode(userCode != null ? userCode : "");
+                        currentBatchList.add(newExt);
+                        generatedInBatch++;
+                        
+                        // 将新生成的分机号加入已存在集合,避免同一批次内重复
+                        existingExtNumSet.add(currentExtNum);
+                    }
+                    currentExtNum++;
+                }
+                
+                // ==================== 6. 分批插入数据库 ====================
+                if (!currentBatchList.isEmpty()) {
+                    for (int i = 0; i < currentBatchList.size(); i += INSERT_BATCH_SIZE) {
+                        int endIndex = Math.min(i + INSERT_BATCH_SIZE, currentBatchList.size());
+                        List<CcExtNum> insertBatchList = currentBatchList.subList(i, endIndex);
+                        int inserted = ccExtNumMapper.batchInsertCcExtNum(insertBatchList);
+                        totalInserted += inserted;
+                    }
+                    
+                    // 更新剩余计数
+                    remainingCount -= generatedInBatch;
+                    
+                    // 【关键】更新缓存的最大分机号(在同步块中保证线程安全)
+                    synchronized (cacheLock) {
+                        long newMaxExtNum = currentBatchList.get(currentBatchList.size() - 1).getExtNum();
+                        if (newMaxExtNum > cachedMaxExtNum) {
+                            cachedMaxExtNum = newMaxExtNum;
+                            log.debug("更新分机号缓存为: {}", cachedMaxExtNum);
+                        }
+                    }
+                    
+                    // 记录进度日志
+                    log.info("批量生成分机号进度: {}/{}", totalInserted, count);
+                }
+            }
+            
+            log.info("批量生成分机号完成,共生成 {} 条记录", totalInserted);
+            return totalInserted;
+            
+        } catch (RuntimeException e) {
+            log.error("批量生成分机号失败: {}", e.getMessage(), e);
+            throw e;
+        } catch (Exception e) {
+            log.error("批量生成分机号发生未知错误", e);
+            throw new RuntimeException("批量生成分机号失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public int companyBatchUnbindSipExt(Set<Long> extNums) {
+        return ccExtNumMapper.companyBatchUnbindSipExt(extNums);
+    }
+
+    @Override
+    public int updateUserCodeByLoginName(String companyName, String loginName) {
+        return ccExtNumMapper.updateUserCodeByLoginName(companyName, loginName);
+    }
 }

+ 56 - 0
ruoyi-admin/src/main/java/com/ruoyi/cc/service/impl/CcOutboundCdrServiceImpl.java

@@ -5,9 +5,11 @@ import com.ruoyi.cc.mapper.CcOutboundCdrMapper;
 import com.ruoyi.cc.service.ICcOutboundCdrService;
 import com.ruoyi.common.core.text.Convert;
 import com.ruoyi.common.utils.DateUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.net.URLDecoder;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -104,6 +106,12 @@ public class CcOutboundCdrServiceImpl implements ICcOutboundCdrService
     @Override
     public int insertCcOutboundCdr(CcOutboundCdr ccOutboundCdr)
     {
+        // 对opnum字段进行URL解码,防止乱码
+        if(StringUtils.isNotBlank(ccOutboundCdr.getOpnum())){
+            try {
+                ccOutboundCdr.setOpnum(URLDecoder.decode(ccOutboundCdr.getOpnum(), "UTF-8"));
+            } catch (Exception e) {}
+        }
         return ccOutboundCdrMapper.insertCcOutboundCdr(ccOutboundCdr);
     }
 
@@ -142,4 +150,52 @@ public class CcOutboundCdrServiceImpl implements ICcOutboundCdrService
     {
         return ccOutboundCdrMapper.deleteCcOutboundCdrById(id);
     }
+    /**
+     * 查询外呼记录列表
+     *
+     * @param ccOutboundCdr 外呼记录
+     * @return 外呼记录
+     */
+    @Override
+    public List<CcOutboundCdr> selectCcOutboundCdrYlrzList(CcOutboundCdr ccOutboundCdr)
+    {
+        Map<String, Object> params = ccOutboundCdr.getParams();
+        if (null == params) {
+            params = new HashMap<>();
+        }
+        if (null != params.get("timeLenStart")
+                && !"".equals(params.get("timeLenStart"))) {
+            params.put("timeLenStart", Double.valueOf((String)params.get("timeLenStart")) * 60 * 1000L);
+        }
+        if (null != params.get("timeLenEnd")
+                && !"".equals(params.get("timeLenEnd"))) {
+            params.put("timeLenEnd", Double.valueOf((String)params.get("timeLenEnd")) * 60 * 1000L);
+        }
+        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("answeredTimeStart")
+                && !"".equals(params.get("answeredTimeStart"))) {
+            params.put("answeredTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("answeredTimeStart")).getTime());
+        }
+        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 (null != params.get("endTimeStart")
+                && !"".equals(params.get("endTimeStart"))) {
+            params.put("endTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("endTimeStart")).getTime());
+        }
+        if (null != params.get("endTimeEnd")
+                && !"".equals(params.get("endTimeEnd"))) {
+            params.put("endTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("endTimeEnd")).getTime());
+        }
+        ccOutboundCdr.setParams(params);
+        return ccOutboundCdrMapper.selectCcOutboundCdrList(ccOutboundCdr);
+    }
 }

+ 2 - 2
ruoyi-admin/src/main/resources/application-dev.yml

@@ -7,9 +7,9 @@ spring:
     druid:
       # 主库数据源
       master:
-        url: jdbc:mysql://172.200.115.112:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+        url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
         username: root
-        password: Tydic2020!@#dicfin!@#$%
+        password: easycallcenter365
       slave:
         # 从数据源开关/默认关闭
         enabled: false

+ 1 - 1
ruoyi-admin/src/main/resources/application.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev

+ 63 - 0
ruoyi-admin/src/main/resources/mapper/aicall/CcCallPhoneMapper.xml

@@ -282,4 +282,67 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <delete id="delCallPhoneByBatchId" parameterType="Long">
         delete from cc_call_phone where batch_id = #{batchId}
     </delete>
+
+
+    <select id="selectCcCallPhoneYlrzList" parameterType="CcCallPhone" resultMap="CcCallPhoneResult">
+        select ccp.*, cct.batch_name as batchName from cc_call_phone ccp left join cc_call_task cct on cct.batch_id = ccp.batch_id
+        <where>
+            <if test="uuid != null and uuid != '' "> and ccp.uuid = #{uuid}</if>
+            <if test="batchId != null "> and ccp.batch_id = #{batchId}</if>
+            <if test="telephone != null  and telephone != ''"> and ccp.telephone = #{telephone}</if>
+            <if test="custName != null  and custName != ''"> and ccp.cust_name = #{custName}</if>
+            <if test="callstatus != null "> and ccp.callstatus = #{callstatus}</if>
+            <if test="params.calloutTimeStart != null and params.calloutTimeStart != ''"><!-- 开始时间检索 -->
+                AND ccp.callout_time &gt;= #{params.calloutTimeStart}
+            </if>
+            <if test="params.calloutTimeEnd != null and params.calloutTimeEnd != ''"><!-- 结束时间检索 -->
+                AND ccp.callout_time &lt;= #{params.calloutTimeEnd}
+            </if>
+            <if test="params.callEndTimeStart != null and params.callEndTimeStart != ''"><!-- 开始时间检索 -->
+                AND ccp.call_end_time &gt;= #{params.callEndTimeStart}
+            </if>
+            <if test="params.callEndTimeEnd != null and params.callEndTimeEnd != ''"><!-- 结束时间检索 -->
+                AND ccp.call_end_time &lt;= #{params.callEndTimeEnd}
+            </if>
+            <if test="params.timeLenStart != null and params.timeLenStart != ''"><!-- 开始时间检索 -->
+                AND ccp.time_len &gt;= #{params.timeLenStart}
+            </if>
+            <if test="params.timeLenEnd != null and params.timeLenEnd != ''"><!-- 结束时间检索 -->
+                AND ccp.time_len &lt;= #{params.timeLenEnd}
+            </if>
+            <if test="params.connectedTimeStart != null and params.connectedTimeStart != ''"><!-- 开始时间检索 -->
+                AND ccp.connected_time &gt;= #{params.connectedTimeStart}
+            </if>
+            <if test="params.connectedTimeEnd != null and params.connectedTimeEnd != ''"><!-- 结束时间检索 -->
+                AND ccp.connected_time &lt;= #{connectedTimeEnd}
+            </if>
+            <if test="params.answeredTimeStart != null and params.answeredTimeStart != ''"><!-- 开始时间检索 -->
+                AND ccp.answered_time &gt;= #{params.answeredTimeStart}
+            </if>
+            <if test="params.answeredTimeEnd != null and params.answeredTimeEnd != ''"><!-- 结束时间检索 -->
+                AND ccp.answered_time &lt;= #{params.answeredTimeEnd}
+            </if>
+            <if test="batchId != null and batchId != ''">
+                AND ccp.batch_id = #{batchId}
+            </if>
+            <if test="intent != null and intent != ''">
+                AND ccp.intent = #{intent}
+            </if>
+            <if test="billingStatus != null ">
+                and ccp.billing_status = #{billingStatus}
+            </if>
+            <if test="callerNumber != null and callerNumber != '' ">
+                and ccp.caller_number = #{callerNumber}
+            </if>
+            AND ccp.callstatus >= 3 AND ccp.call_end_time > 0
+        </where>
+        order by ccp.call_end_time desc
+    </select>
+    <select id="selectCcCallPhoneListByIds" parameterType="java.util.List" resultMap="CcCallPhoneResult">
+        <include refid="selectCcCallPhoneVo"/>
+        where uuid is not null and id in
+        <foreach item="id" collection="list" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
 </mapper>

+ 12 - 14
ruoyi-admin/src/main/resources/mapper/aicall/CcCallTaskMapper.xml

@@ -108,11 +108,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data,</if>
             <if test="autoStop != null ">auto_stop,</if>
             <if test="ivrId != null and ivrId != ''">ivr_id,</if>
-            asr_language_code,
-            tts_language_code,
-            asr_models,
-            tts_models,
-
+            <if test="asrLanguageCode != null and asrLanguageCode != ''">asr_language_code,</if>
+            <if test="ttsLanguageCode != null and ttsLanguageCode != ''">tts_language_code,</if>
+            <if test="asrModels != null and asrModels != ''">asr_models,</if>
+            <if test="ttsModels != null and ttsModels != ''">tts_models,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="groupId != null and groupId != ''">#{groupId},</if>
@@ -139,10 +138,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="aiTransferData != null and aiTransferData != ''">#{aiTransferData},</if>
             <if test="autoStop != null ">#{autoStop},</if>
             <if test="ivrId != null and ivrId != ''">#{ivrId},</if>
-            #{asrLanguageCode},
-            #{ttsLanguageCode},
-            #{asrModels},
-            #{ttsModels},
+            <if test="asrLanguageCode != null and asrLanguageCode != ''">#{asrLanguageCode},</if>
+            <if test="ttsLanguageCode != null and ttsLanguageCode != ''">#{ttsLanguageCode},</if>
+            <if test="asrModels != null and asrModels != ''">#{asrModels},</if>
+            <if test="ttsModels != null and ttsModels != ''">#{ttsModels},</if>
 
         </trim>
     </insert>
@@ -174,11 +173,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data = #{aiTransferData},</if>
             <if test="autoStop != null ">auto_stop = #{autoStop},</if>
             <if test="ivrId != null and ivrId != ''">ivr_id = #{ivrId},</if>
-            asr_language_code = #{asrLanguageCode},
-            tts_language_code = #{ttsLanguageCode},
-            asr_models = #{asrModels},
-            tts_models = #{ttsModels},
-
+            <if test="asrLanguageCode != null and asrLanguageCode != ''">asr_language_code = #{asrLanguageCode},</if>
+            <if test="ttsLanguageCode != null and ttsLanguageCode != ''">tts_language_code = #{ttsLanguageCode},</if>
+            <if test="asrModels != null and asrModels != ''">asr_models = #{asrModels},</if>
+            <if test="ttsModels != null and ttsModels != ''">tts_models = #{ttsModels},</if>
         </trim>
         where batch_id = #{batchId}
     </update>

+ 63 - 1
ruoyi-admin/src/main/resources/mapper/cc/CcExtNumMapper.xml

@@ -20,8 +20,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <where>  
             <if test="extNum != null "> and ext_num = #{extNum}</if>
             <if test="extPass != null  and extPass != ''"> and ext_pass = #{extPass}</if>
-            <if test="userCode != null  and userCode != ''"> and user_code = #{userCode}</if>
+<!--            <if test="userCode != null  and userCode != ''"> and user_code = #{userCode}</if>-->
+            <if test="userCode != null  and userCode != ''"> and user_code like concat('%', #{userCode}, '%')</if>
+            <if test="sipExtNumList != null and sipExtNumList.size() > 0">
+                and ext_num in
+                <foreach collection="sipExtNumList" item="item" open="(" separator="," close=")">
+                    #{item}
+                </foreach>
+            </if>
         </where>
+        order by ext_num asc
     </select>
     
     <select id="selectCcExtNumByExtId" parameterType="Long" resultMap="CcExtNumResult">
@@ -68,5 +76,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectCcExtNumVo"/>
         where user_code = ''
     </select>
+    <update id="cleanCcExtNumByUserCode" parameterType="String">
+        update cc_ext_num set user_code = '' where user_code = #{userCode}
+    </update>
+    <update id="updateCcExtNumByUserCode" parameterType="CcExtNum">
+        update cc_ext_num set user_code = #{userCode} where ext_num = #{extNum}
+    </update>
+
+    <select id="selectUnBindCcExtNumListPage" parameterType="CcExtNum" resultMap="CcExtNumResult">
+        <include refid="selectCcExtNumVo"/>
+        where user_code = ''
+        <if test="extNum != null">and ext_num = #{extNum}</if>
+        <if test="extPass != null and extPass != ''">and ext_pass = #{extPass}</if>
+        <if test="startExtNum != null">and ext_num &gt;= #{startExtNum}</if>
+        <if test="endExtNum != null">and ext_num  &lt;= #{endExtNum}</if>
+        order by ext_num asc
+    </select>
+
+    <update id="updateCompanyBindExtNum">
+        update cc_ext_num set user_code = #{userCode} where ext_num in
+        <foreach item="extNum" collection="sipExtNumList" open="(" separator="," close=")">
+            #{extNum}
+        </foreach>
+    </update>
+    <update id="companyUnbindExtNum">
+        update cc_ext_num set user_code = '' where user_code in
+        <foreach item="userCode" collection="list" open="(" separator="," close=")">
+            #{userCode}
+        </foreach>
+    </update>
 
+    <insert id="batchInsertCcExtNum" parameterType="java.util.List">
+        insert into cc_ext_num (ext_num, ext_pass, user_code)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.extNum}, #{item.extPass}, #{item.userCode})
+        </foreach>
+    </insert>
+
+    <select id="selectExtNumsGreaterThan" parameterType="java.lang.Long" resultType="java.lang.Long">
+        select ext_num from cc_ext_num where ext_num &gt; #{minExtNum}
+    </select>
+
+    <select id="selectMaxExtNum" resultType="java.lang.Long">
+        select max(ext_num) from cc_ext_num
+    </select>
+
+    <update id="companyBatchUnbindSipExt">
+        update cc_ext_num set user_code = '' where ext_num in
+        <foreach item="extNum" collection="extNums" open="(" separator="," close=")">
+            #{extNum}
+        </foreach>
+    </update>
+    <update id="updateUserCodeByLoginName">
+        update cc_ext_num set user_code = #{companyName} where user_code = #{loginName}
+    </update>
 </mapper>

+ 7 - 1
ruoyi-admin/src/main/resources/mapper/cc/CcGatewaysMapper.xml

@@ -30,7 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectCcGatewaysList" parameterType="CcGateways" resultMap="CcGatewaysResult">
         <include refid="selectCcGatewaysVo"/>
         <where>
-            <if test="gwName != null  and gwName != ''"> and gw_name = #{gwName}</if>
+            <if test="gwName != null  and gwName != ''"> and gw_name like concat('%', #{gwName}, '%')</if>
             <if test="profileName != null  and profileName != ''"> and profile_name = #{profileName}</if>
             <if test="caller != null  and caller != ''"> and caller = #{caller}</if>
             <if test="calleePrefix != null  and calleePrefix != ''"> and callee_prefix = #{calleePrefix}</if>
@@ -49,6 +49,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                     #{purpose}
                 </foreach>
             </if>
+            <if test="gatewayIds != null and !gatewayIds.isEmpty()">
+                and id in
+                <foreach item="gatewayId" collection="gatewayIds" open="(" separator="," close=")">
+                    #{gatewayId}
+                </foreach>
+            </if>
         </where>
     </select>
     

TEMPAT SAMPAH
ruoyi-admin/src/main/resources/static/img/login-background.jpg


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

@@ -499,8 +499,8 @@
                     }
                 },
                 {
-                    field: 'timeLen',
-                    title: i18n('callPhone.table.timeLen'),
+                    field: 'validTimeLen',
+                    title: i18n('callPhone.table.validTimeLen'),
                     width: 80,
                     formatter: function(value, row, index, field) {
                         // 计算分钟数,向下取整

+ 9 - 6
ruoyi-admin/src/main/resources/templates/aicall/callTask/add.html

@@ -29,8 +29,8 @@
                 <label class="col-sm-3 control-label is-required" th:text="#{callTask.form.taskType}"></label>
                 <div class="col-sm-6">
                     <select name="taskType" id="taskType"  th:field="*{taskType}" class="form-control" required>
-                        <option value="0" th:text="#{callTask.form.taskType0}"></option>
                         <option value="1" th:text="#{callTask.form.taskType1}"></option>
+                        <option value="0" th:text="#{callTask.form.taskType0}"></option>
                         <option value="2" th:text="#{callTask.form.taskType2}"></option>
                         <option value="3" th:text="#{callTask.form.taskType3}"></option>
                     </select>
@@ -292,9 +292,6 @@
                 if (gatewayId) {
                     gatewaySelect.val(gatewayId);
                 }
-
-
-
                 var gatewaySelect = $('select[name="aiTransferGatewayId"]');
                 gatewaySelect.empty();
                 gatewaySelect.append('<option value="">' + i18n('callTask.form.aiTransferGatewayId.empty') + '</option>');
@@ -315,6 +312,7 @@
             success: function(rsp) {
                 var voiceSourceSelect = $('#voiceSource');
                 voiceSourceSelect.empty();
+                voiceSourceSelect.append('<option value="">' + i18n('callTask.form.voiceSource.empty') + '</option>');
                 // 遍历 map 对象,key 作为 value,value 作为显示文本
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     voiceSourceSelect.append($("<option>").attr("value", key).text(value));
@@ -347,6 +345,7 @@
             success: function(rsp) {
                 var providerSelect = $('#asrProvider');
                 providerSelect.empty();
+                providerSelect.append('<option value="">' + i18n('callTask.form.asrProvider.empty') + '</option>');
                 // 遍历 map 对象,key 作为 value,value 作为显示文本
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     providerSelect.append($("<option>").attr("value", key).text(value));
@@ -594,7 +593,7 @@
                 url: ctx + "aicall/ttsAliyun/getByLanguageCode",
                 data: requestData,
                 success: function(rsp) {
-                    voiceSelect.empty();
+                    voiceSelect.empty().append('<option value="">' + i18n('callTask.form.voiceCode.empty') + '</option>');
                     rsp.data.forEach(function(voice) {
                         voiceSelect.append($("<option>")
                             .attr("value", voice.voiceCode)
@@ -646,7 +645,11 @@
             var voiceSource = $('#voiceSource').val();
             var ttsLanguageCode = $('#ttsLanguageCode').val();
             var ttsModels = $(this).val();
-            loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            if (!ttsModels) {
+                $('select[name="voiceCode"]').empty().append('<option value="">' + i18n('callTask.form.voiceCode.empty') + '</option>');
+            } else {
+                loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            }
         });
 
         // ASR厂商变更事件

+ 9 - 6
ruoyi-admin/src/main/resources/templates/aicall/callTask/edit.html

@@ -29,8 +29,8 @@
                 <label class="col-sm-3 control-label is-required" th:text="#{callTask.form.taskType}"></label>
                 <div class="col-sm-6">
                     <select name="taskType" id="taskType"  th:field="*{taskType}" class="form-control" required>
-                        <option value="0" th:text="#{callTask.form.taskType0}"></option>
                         <option value="1" th:text="#{callTask.form.taskType1}"></option>
+                        <option value="0" th:text="#{callTask.form.taskType0}"></option>
                         <option value="2" th:text="#{callTask.form.taskType2}"></option>
                         <option value="3" th:text="#{callTask.form.taskType3}"></option>
                     </select>
@@ -292,9 +292,6 @@
                 if (gatewayId) {
                     gatewaySelect.val(gatewayId);
                 }
-
-
-
                 var gatewaySelect = $('select[name="aiTransferGatewayId"]');
                 gatewaySelect.empty();
                 gatewaySelect.append('<option value="">' + i18n('callTask.form.aiTransferGatewayId.empty') + '</option>');
@@ -315,6 +312,7 @@
             success: function(rsp) {
                 var voiceSourceSelect = $('#voiceSource');
                 voiceSourceSelect.empty();
+                voiceSourceSelect.append('<option value="">' + i18n('callTask.form.voiceSource.empty') + '</option>');
                 // 遍历 map 对象,key 作为 value,value 作为显示文本
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     voiceSourceSelect.append($("<option>").attr("value", key).text(value));
@@ -347,6 +345,7 @@
             success: function(rsp) {
                 var providerSelect = $('#asrProvider');
                 providerSelect.empty();
+                providerSelect.append('<option value="">' + i18n('callTask.form.asrProvider.empty') + '</option>');
                 // 遍历 map 对象,key 作为 value,value 作为显示文本
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     providerSelect.append($("<option>").attr("value", key).text(value));
@@ -594,7 +593,7 @@
                 url: ctx + "aicall/ttsAliyun/getByLanguageCode",
                 data: requestData,
                 success: function(rsp) {
-                    voiceSelect.empty();
+                    voiceSelect.empty().append('<option value="">' + i18n('callTask.form.voiceCode.empty') + '</option>');
                     rsp.data.forEach(function(voice) {
                         voiceSelect.append($("<option>")
                             .attr("value", voice.voiceCode)
@@ -646,7 +645,11 @@
             var voiceSource = $('#voiceSource').val();
             var ttsLanguageCode = $('#ttsLanguageCode').val();
             var ttsModels = $(this).val();
-            loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            if (!ttsModels) {
+                $('select[name="voiceCode"]').empty().append('<option value="">' + i18n('callTask.form.voiceCode.empty') + '</option>');
+            } else {
+                loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            }
         });
 
         // ASR厂商变更事件

+ 8 - 2
ruoyi-admin/src/main/resources/templates/aicall/inboundllm/add.html

@@ -261,6 +261,7 @@
             success: function(rsp) {
                 var providerSelect = $('#asrProvider');
                 providerSelect.empty();
+                providerSelect.append('<option value="">' + i18n('inboundllm.form.asrProvider.empty') + '</option>');
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     providerSelect.append($("<option>").attr("value", key).text(value));
                 });
@@ -289,6 +290,7 @@
             success: function(rsp) {
                 var voiceSourceSelect = $('#voiceSource');
                 voiceSourceSelect.empty();
+                voiceSourceSelect.append('<option value="">' + i18n('inboundllm.form.voiceSource.empty') + '</option>');
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     voiceSourceSelect.append($("<option>").attr("value", key).text(value));
                 });
@@ -407,7 +409,11 @@
             var voiceSource = $('#voiceSource').val();
             var ttsLanguageCode = $('#ttsLanguageCode').val();
             var ttsModels = $(this).val();
-            loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            if (!ttsModels) {
+                $('#voiceCode').empty().append('<option value="">' + i18n('inboundllm.form.voiceCode.empty') + '</option>');
+            } else {
+                loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            }
         });
 
         $(document).on('change', '#asrProvider', function() {
@@ -693,7 +699,7 @@
                 ttsModels: ttsModels
             },
             success: function(rsp) {
-                voiceSelect.empty();
+                voiceSelect.empty().append('<option value="">' + i18n('inboundllm.form.voiceCode.empty') + '</option>');
                 rsp.data.forEach(function(voice) {
                     voiceSelect.append($("<option>")
                         .attr("value", voice.voiceCode)

+ 8 - 2
ruoyi-admin/src/main/resources/templates/aicall/inboundllm/edit.html

@@ -261,6 +261,7 @@
             success: function(rsp) {
                 var providerSelect = $('#asrProvider');
                 providerSelect.empty();
+                providerSelect.append('<option value="">' + i18n('inboundllm.form.asrProvider.empty') + '</option>');
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     providerSelect.append($("<option>").attr("value", key).text(value));
                 });
@@ -289,6 +290,7 @@
             success: function(rsp) {
                 var voiceSourceSelect = $('#voiceSource');
                 voiceSourceSelect.empty();
+                voiceSourceSelect.append('<option value="">' + i18n('inboundllm.form.voiceSource.empty') + '</option>');
                 Object.entries(rsp.data).forEach(function([key, value]) {
                     voiceSourceSelect.append($("<option>").attr("value", key).text(value));
                 });
@@ -407,7 +409,11 @@
             var voiceSource = $('#voiceSource').val();
             var ttsLanguageCode = $('#ttsLanguageCode').val();
             var ttsModels = $(this).val();
-            loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            if (!ttsModels) {
+                $('#voiceCode').empty().append('<option value="">' + i18n('inboundllm.form.voiceCode.empty') + '</option>');
+            } else {
+                loadVoicesByVoiceSourceAndLanguage(voiceSource, ttsLanguageCode, ttsModels);
+            }
         });
 
         $(document).on('change', '#asrProvider', function() {
@@ -693,7 +699,7 @@
                 ttsModels: ttsModels
             },
             success: function(rsp) {
-                voiceSelect.empty();
+                voiceSelect.empty().append('<option value="">' + i18n('inboundllm.form.voiceCode.empty') + '</option>');
                 rsp.data.forEach(function(voice) {
                     voiceSelect.append($("<option>")
                         .attr("value", voice.voiceCode)

+ 5 - 5
ruoyi-admin/src/main/resources/templates/cc/extnum/add.html

@@ -36,21 +36,21 @@
     <script th:inline="javascript">
         var prefix = ctx + "cc/extnum"
 
-        $.validator.addMethod("pattern", function(value, element, regexp) {
-            return this.optional(element) || regexp.test(value);
-        }, "格式不正确");
+        // $.validator.addMethod("pattern", function(value, element, regexp) {
+        //     return this.optional(element) || regexp.test(value);
+        // }, "格式不正确");
 
         $("#form-num-add").validate({
             focusCleanup: true,
             rules: {
                 extNum: {
                     required: true,
-                    pattern: /^1\d{3}$/
+                    // pattern: /^1\d{3}$/
                 }
             },
             messages: {
                 extNum: {
-                    pattern: "请输入以1开头的4位数字分机号,例如:1001"
+                    // pattern: "请输入以1开头的4位数字分机号,例如:1001"
                 }
             }
         });

+ 74 - 0
ruoyi-admin/src/main/resources/templates/cc/extnum/batchAdd.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="zh" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <th:block th:include="include :: header('批量新增分机')" />
+</head>
+<body class="white-bg">
+    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
+        <form class="form-horizontal m" id="form-num-batch-add">
+            <div class="col-xs-12">
+                <div class="form-group">
+                    <label class="col-sm-3 control-label is-required">生成分机号数量:</label>
+                    <div class="col-sm-8">
+                        <input name="count" class="form-control" type="number" min="1" max="2000" required placeholder="请输入要生成的分机号数量(1-10000)">
+                    </div>
+                </div>
+            </div>
+            <div class="col-xs-12">
+                <div class="form-group">
+                    <label class="col-sm-3 control-label">起始分机号:</label>
+                    <div class="col-sm-8">
+                        <input name="startExtNum" class="form-control" type="number" min="1" placeholder="选填">
+                        <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 非必填,指定后将从该分机号开始生成</span>
+                    </div>
+                </div>
+            </div>
+            <div class="col-xs-12">
+                <div class="form-group">
+                    <label class="col-sm-3 control-label is-required">分机密码:</label>
+                    <div class="col-sm-8">
+                        <input name="extPass" class="form-control" type="text" placeholder="选填,所有分机密码将相同">
+                    </div>
+                </div>
+            </div>
+            <div class="col-xs-12">
+                <div class="form-group">
+                    <label class="col-sm-3 control-label">绑定工号:</label>
+                    <div class="col-sm-8">
+                        <input name="userCode" class="form-control" type="text" placeholder="选填,所有分机将绑定同一工号">
+                        <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 非必填,如需绑定请填写</span>
+                    </div>
+                </div>
+            </div>
+        </form>
+    </div>
+    <th:block th:include="include :: footer" />
+    <script th:inline="javascript">
+        var prefix = ctx + "cc/extnum"
+
+        $("#form-num-batch-add").validate({
+            focusCleanup: true,
+            rules: {
+                count: {
+                    required: true,
+                    digits: true,
+                    range: [1, 10000]
+                },
+            },
+            messages: {
+                count: {
+                    required: "请输入生成数量",
+                    digits: "请输入数字",
+                    range: "数量范围为1-10000"
+                },
+            }
+        });
+
+        function submitHandler() {
+            if ($.validate.form()) {
+                $.operate.save(prefix + "/batchAdd", $('#form-num-batch-add').serialize());
+            }
+        }
+    </script>
+</body>
+</html>

+ 8 - 0
ruoyi-admin/src/main/resources/templates/cc/extnum/extnum.html

@@ -31,6 +31,9 @@
                 <a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="cc:extnum:add">
                     <i class="fa fa-plus"></i> <span th:text="#{btn.add}"></span>
                 </a>
+                <a class="btn btn-info" onclick="batchAdd()" shiro:hasPermission="cc:extnum:batchAdd">
+                    <i class="fa fa-plus-circle"></i> 批量新增
+                </a>
                 <a class="btn btn-primary single disabled" onclick="$.operate.edit()" shiro:hasPermission="cc:extnum:edit">
                     <i class="fa fa-edit"></i> <span th:text="#{btn.edit}"></span>
                 </a>
@@ -86,6 +89,11 @@
             };
             $.table.init(options);
         });
+
+        // 批量新增分机
+        function batchAdd() {
+            $.modal.open("批量新增分机", prefix + "/batchAdd", '700', '500');
+        }
     </script>
 </body>
 </html>

+ 2 - 1
ruoyi-admin/src/main/resources/templates/index.html

@@ -37,7 +37,8 @@
         </div>
 		<a th:href="@{/index}">
 			<li class="logo hidden-xs">
-				<img th:src="@{${logoPath}}" alt="Logo" class="logo-img">
+				AI外呼平台
+<!--				<img th:src="@{${logoPath}}" alt="Logo" class="logo-img">-->
 			</li>
 		</a>
         <div class="sidebar-collapse">

+ 5 - 5
ruoyi-admin/src/main/resources/templates/login.html

@@ -106,15 +106,15 @@
             <div class="col-sm-7">
                 <div class="signin-info">
                     <div class="m-b"></div>
-                    <h4 th:text="#{user.login.welcome} + ' ' + #{sys.sysName} + '  ' + ${sysVersion}"></h4>
+                    <h2 th:text="#{user.login.welcome} + ' ' + #{sys.sysName} "></h2>
                 </div>
             </div>
             <div class="col-sm-5">
                 <form id="signupForm" autocomplete="off">
                     <h4 class="no-margins"><span th:text="#{user.login.submit}"></span>:</h4>
-                    <input type="text"     name="username" class="form-control uname"     th:attr="placeholder=#{user.login.username}" value="admin"    />
-                    <input type="password" name="password" class="form-control pword"     th:attr="placeholder=#{user.login.password}" value="admin123" />
-                    <input type="text"     name="extNum" class="form-control uname"     th:attr="placeholder=#{user.login.extnum}" value="1001"    />
+                    <input type="text"     name="username" class="form-control uname"     th:attr="placeholder=#{user.login.username}"    />
+                    <input type="password" name="password" class="form-control pword"     th:attr="placeholder=#{user.login.password}" />
+                    <input type="text"     name="extNum" class="form-control uname"     th:attr="placeholder=#{user.login.extnum}"    />
                     <select th:if="${showDynamicGroupid==true}" name="groupId" id="groupId" class="form-control m-t">
 
                     </select>
@@ -137,7 +137,7 @@
         </div>
         <div class="signup-footer">
             <div class="pull-left">
-                Copyright easycallcenter365 All Rights Reserved. <br>
+                Copyright 云联 All Rights Reserved. <br>
             </div>
         </div>
     </div>

+ 41 - 2
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -105,6 +105,45 @@ public class SysUser extends BaseEntity
     private Long[] postIds;
     /** 分机号 */
     private Long extNum;
+
+    /** 分机密码 */
+    private String extPass;
+
+    /** 分机绑定用户 */
+    private String userCode;
+    /** 用户来源0销售 1总后台 */
+    private String userSource;
+    /** 销售公司 */
+    private String companyName;
+    public void setCompanyName(String companyName) {
+        this.companyName = companyName;
+    }
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    public void setUserSource(String userSource) {
+        this.userSource = userSource;
+    }
+    public String getUserSource() {
+        return userSource;
+    }
+
+    public String getUserCode() {
+        return userCode;
+    }
+
+    public void setUserCode(String userCode) {
+        this.userCode = userCode;
+    }
+    public String getExtPass() {
+        return extPass;
+    }
+
+    public void setExtPass(String extPass) {
+        this.extPass = extPass;
+    }
+
     public Long getExtNum() {
         return extNum;
     }
@@ -258,7 +297,7 @@ public class SysUser extends BaseEntity
         this.avatar = avatar;
     }
 
-    @JsonIgnore
+//    @JsonIgnore
     public String getPassword()
     {
         return password;
@@ -269,7 +308,7 @@ public class SysUser extends BaseEntity
         this.password = password;
     }
 
-    @JsonIgnore
+//    @JsonIgnore
     public String getSalt()
     {
         return salt;

+ 0 - 16
ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java

@@ -292,22 +292,6 @@ public class ShiroConfig
         filterChainDefinitionMap.put("/i18n/**", "anon");
         filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
         filterChainDefinitionMap.put("/recordings/files", "anon");
-        filterChainDefinitionMap.put("/aicall/api/notice/call", "anon");
-        filterChainDefinitionMap.put("/aicall/api/ai/addCallList", "anon");
-        filterChainDefinitionMap.put("/aicall/api/common/addCallList", "anon");
-        filterChainDefinitionMap.put("/aicall/api/gateway/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/llmacount/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/voicecode/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/busigroup/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/calltask/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/records/list", "anon");
-        filterChainDefinitionMap.put("/aicall/api/ai/createTask", "anon");
-        filterChainDefinitionMap.put("/aicall/api/ai/startTask", "anon");
-        filterChainDefinitionMap.put("/aicall/api/ai/stopTask", "anon");
-        filterChainDefinitionMap.put("/aicall/api/local/addCall", "anon");
-        filterChainDefinitionMap.put("/aicall/api/phoneBar/params", "anon");
-        filterChainDefinitionMap.put("/aicall/api//phoneBar/extnum/bind", "anon");
-        filterChainDefinitionMap.put("/aicall/api/test/localChat", "anon");
         filterChainDefinitionMap.put("/api/ai/chat/stream", "anon");
 
         //开放公共api所有接口

+ 10 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysPasswordService.java

@@ -5,6 +5,8 @@ import javax.annotation.PostConstruct;
 import org.apache.shiro.cache.Cache;
 import org.apache.shiro.cache.CacheManager;
 import org.apache.shiro.crypto.hash.Md5Hash;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
@@ -25,6 +27,8 @@ import com.ruoyi.framework.manager.factory.AsyncFactory;
 @Component
 public class SysPasswordService
 {
+    private static final Logger log = LoggerFactory.getLogger(SysPasswordService.class);
+    
     @Autowired
     private CacheManager cacheManager;
 
@@ -70,7 +74,12 @@ public class SysPasswordService
 
     public boolean matches(SysUser user, String newPassword)
     {
-        return user.getPassword().equals(encryptPassword(user.getLoginName(), newPassword, user.getSalt()));
+        String encryptedPassword = encryptPassword(user.getLoginName(), newPassword, user.getSalt());
+        log.info("密码验证 - 用户名:{}, 输入密码:{}, 盐值:{}, 加密后密码:{}, 数据库密码:{}", 
+                user.getLoginName(), newPassword, user.getSalt(), encryptedPassword, user.getPassword());
+        boolean result = user.getPassword().equals(encryptedPassword);
+        log.info("密码验证结果: {}", result);
+        return result;
     }
 
     public void clearLoginRecordCache(String loginName)