Przeglądaj źródła

APP代码提交

yjwang 3 dni temu
rodzic
commit
ec062b2d75
17 zmienionych plików z 778 dodań i 3 usunięć
  1. 4 1
      fs-company-app/src/main/java/com/fs/app/controller/UserController.java
  2. 391 0
      fs-company-app/src/main/java/com/fs/app/controller/aiSipCall/AiSipCallController.java
  3. 52 0
      fs-company-app/src/main/java/com/fs/app/controller/crm/CrmAPPMsgController.java
  4. 15 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  5. 13 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  6. 21 0
      fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java
  7. 38 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java
  8. 12 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  9. 13 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java
  10. 6 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  11. 76 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  12. 41 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  13. 9 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  14. 26 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  15. 48 0
      fs-service/src/main/resources/db/tenant-initTable.sql
  16. 11 1
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml
  17. 2 1
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallUserMapper.xml

+ 4 - 1
fs-company-app/src/main/java/com/fs/app/controller/UserController.java

@@ -464,7 +464,10 @@ public class UserController extends AppBaseController {
     @PostMapping("/setPwd")
     public R setPwd(HttpServletRequest request, @RequestBody EditPwdParam param) {
         try {
-
+            // 新密码格式校验(与login一致)
+            if (!PatternUtils.checkPassword(param.getPassword())) {
+                return R.error("密码格式不正确,需包含字母、数字和特殊字符,长度为 8-20位");
+            }
             CompanyUser user = userService.selectCompanyUserById(Long.parseLong(getUserId()));
             if (!SecurityUtils.matchesPassword(param.getOldPassword(), user.getPassword())) {
                 return R.error("旧密码错误");

+ 391 - 0
fs-company-app/src/main/java/com/fs/app/controller/aiSipCall/AiSipCallController.java

@@ -0,0 +1,391 @@
+package com.fs.app.controller.aiSipCall;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
+import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.domain.CcCustInfo;
+import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
+import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
+import com.fs.aiSipCall.service.IAiSipCallUserService;
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.company.util.OrderUtils;
+import com.fs.company.vo.easycall.EasyCallOutBoundVO;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.param.CrmCustomeReceiveParam;
+import com.fs.crm.param.CrmFullCustomerListQueryParam;
+import com.fs.crm.param.CrmMyCustomerListQueryParam;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.crm.vo.CrmFullCustomerListQueryVO;
+import com.fs.crm.vo.CrmMyCustomerListQueryVO;
+import com.fs.his.utils.PhoneUtil;
+import com.github.pagehelper.PageHelper;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 销售端APP - aiSIP软电话接口(手动外呼)
+ * <p>
+ * 提供给APP端浏览器/JsSIP软电话工具条所需的最小可用后端能力:
+ * <ul>
+ *     <li>拉取当前销售绑定的SIP分机账号 - 用于注册SIP UA</li>
+ *     <li>拉取IPCC工具条基础参数 - 用于建立IPCC WebSocket控制通道</li>
+ *     <li>通话前手机号密文转换 - 配合密文拨号模式</li>
+ *     <li>通话沟通信息查询/沟通记录补录</li>
+ * </ul>
+ * <p>
+ * 注意:本Controller仅复用 fs-service 中已有Service方法,
+ * 不修改 fs-service / fs-company / fs-framework 等任何已有模块代码。
+ * <p>
+ * 仍待用户决策的能力(依赖 fs-service 暂未提供的方法/字段,详见随附差缺清单):
+ * <ul>
+ *     <li>已补齐:agentLogin、callEndSyncByUuid、getCustCommunicationInfo(dialMode)</li>
+ * </ul>
+ *
+ * @author migrated from his_java/fs-company AiSipCallUserController & AiSipCallOutboundCdrController
+ */
+@Slf4j
+@Api("软电话接口")
+@RestController
+@RequestMapping("/app/aiSipCall")
+public class AiSipCallController extends AppBaseController {
+
+    @Autowired
+    private IAiSipCallUserService aiSipCallUserService;
+
+    @Autowired
+    private IAiSipCallOutboundCdrService aiSipCallOutboundCdrService;
+
+    @Autowired
+    private ICompanyUserService companyUserService;
+
+    @Autowired
+    private EasyCallMapper easyCallMapper;
+
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
+    private final String AUDIO_BASE_URL = "http://129.28.164.235:8899";
+
+    /**
+     * 是否使用自有线路(迁移自 his_java sip.call.myGateway,原放在 AiSipCallUserServiceImpl)。
+     * <p>SaaS 老 ServiceImpl 已被精简没有兜底逻辑,故在 Controller 层注入并保持 his_java 行为一致。
+     */
+    @Value("${sip.call.myGateway:false}")
+    private boolean isMyGateway;
+
+    /**
+     * 手动外呼网关前缀(迁移自 his_java sip.call.manualGatewayPrefix)
+     */
+    @Value("${sip.call.manualGatewayPrefix:weizhi}")
+    private String manualGatewayPrefix;
+
+    /**
+     * 公共线路网关前缀(迁移自 his_java sip.call.publicGatewayPrefix)
+     */
+    @Value("${sip.call.publicGatewayPrefix:outbound}")
+    private String publicGatewayPrefix;
+
+    /**
+     * 加密手机号末尾随机串长度(与前端约定,与 his_java 的 RandomUtil.generateRandomCode 等价)
+     */
+    private static final int RANDOM_TAIL_LEN = 6;
+
+    private static final char[] RANDOM_CHARS =
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
+
+    private static final SecureRandom RANDOM = new SecureRandom();
+
+    /**
+     * XOR加密公钥(与 his_java 的 PhoneUtil.PUBLIC_KEY_STR 保持一致,避免污染 SaaS 原有 PhoneUtil)
+     */
+    private static final String XOR_KEY = "ylrz112233";
+
+    /**
+     * 查询当前登录销售绑定的SIP分机账号
+     * <p>前端 softPhone.vue 启动时调用,用于初始化JsSIP UA配置(user/domain/extPass)。
+     */
+    @Login
+    @ApiOperation("查询当前销售的SIP分机账号")
+    @GetMapping("/myCallUser")
+    public AjaxResult myCallUser(AiSipCallUser aiSipCallUser) {
+        if (aiSipCallUser == null) {
+            aiSipCallUser = new AiSipCallUser();
+        }
+        aiSipCallUser.setCompanyUserId(getCompanyUserId());
+        List<AiSipCallUser> list = aiSipCallUserService.selectAiSipCallUserList(aiSipCallUser);
+        if (list != null && !list.isEmpty()) {
+            return AjaxResult.success(list.get(0));
+        }
+        return AjaxResult.error("未创建sip角色");
+    }
+
+    /**
+     * 查询aiSIP工具条基础配置参数
+     * <p>前端获取IPCC WebSocket地址、外呼网关等参数。
+     *
+     * @param param 至少包含 extNum(分机号)
+     */
+    @Login
+    @ApiOperation("查询aiSIP工具条基础参数")
+    @PostMapping("/getToolbarBasicParam")
+    public AjaxResult getToolbarBasicParam(@RequestBody Map<String, String> param) {
+        if (param == null || param.get("extNum") == null) {
+            return AjaxResult.error("分机号参数缺失");
+        }
+        // ===== 步骤1:迁移自 fs-company AiSipCallUserController#getToolbarBasicParam =====
+        // 优先使用当前销售所属公司绑定的网关ids
+        Long companyId = getCompanyId();
+        if (companyId != null) {
+            String ids = aiSipCallUserService.getGateWayIdListByCompanyId(companyId);
+            if (StringUtils.isNotBlank(ids)) {
+                param.put("myGateway", ids);
+            }
+        }
+        // ===== 步骤2:迁移自 his_java AiSipCallUserServiceImpl#getToolbarBasicParam =====
+        // 公司没绑定网关 + 前端也没传 myGateway,按 yml 配置兜底(与 his_java 行为一致)
+//        String myGateway = param.get("myGateway");
+//        if (StringUtils.isBlank(myGateway)) {
+//            if (isMyGateway) {
+//                // 自己有线路 → 使用手动外呼网关前缀
+//                param.put("wgName", manualGatewayPrefix);
+//            } else {
+//                // 没有给默认线路 → 使用公共线路前缀
+//                param.put("wgName", publicGatewayPrefix);
+//            }
+//        }
+        return aiSipCallUserService.getToolbarBasicParam(param);
+    }
+
+    /**
+     * 获取手动外呼客户沟通信息
+     *
+     * @param phoneNum 手机号(明文或密文)
+     * @param callType 类型 1呼入 2外呼
+     * @param uuid     通话UUID
+     * @param dialMode plaintext / encrypted (为空按明文处理)
+     */
+    @Login
+    @ApiOperation("获取手动外呼客户沟通信息")
+    @GetMapping("/getCustCommunicationInfo")
+    public AjaxResult getCustCommunicationInfo(
+            @ApiParam(value = "手机号", required = true) @RequestParam("phoneNum") String phoneNum,
+            @ApiParam(value = "1呼入 2外呼", required = true) @RequestParam("callType") Integer callType,
+            @ApiParam(value = "通话UUID", required = true) @RequestParam("uuid") String uuid,
+            @ApiParam(value = "拨号模式 plaintext/encrypted") @RequestParam(value = "dialMode", required = false) String dialMode) {
+        return aiSipCallOutboundCdrService.getCustCommunicationInfo(phoneNum, callType, uuid, dialMode);
+    }
+
+    /**
+     * 新增保存手动外呼沟通记录
+     */
+    @Login
+    @ApiOperation("新增手动外呼沟通记录")
+    @PostMapping("/add/custcallrecord")
+    public AjaxResult addCustcallrecord(@RequestBody CcCustInfo ccCustInfo) {
+        return aiSipCallOutboundCdrService.addCustcallrecord(ccCustInfo);
+    }
+
+    /**
+     * 手机号密文转换:先解密再用xor重新加密 + 末尾追加随机串
+     * <p>用于密文拨号模式:前端把"原始密文+6位随机数"丢上来,后端返回"新密文+6位随机数"。
+     */
+    @Login
+    @ApiOperation("手机号密文转换(解密->xor加密)")
+    @PostMapping("/encryptMobile")
+    public AjaxResult encryptMobile(@RequestBody Map<String, String> request) {
+        String combined = request != null ? request.get("data") : null;
+        if (combined == null || combined.length() <= RANDOM_TAIL_LEN) {
+            return AjaxResult.error("加密参数缺失或格式不正确");
+        }
+        // 截掉末尾随机串
+        String original = combined.substring(0, combined.length() - RANDOM_TAIL_LEN);
+        try {
+            String decrypted = PhoneUtil.decryptPhone(original);
+            String encrypted = xorEncrypt(decrypted);
+            return AjaxResult.success("获取成功", encrypted + generateRandomTail());
+        } catch (Exception e) {
+            log.error("[aiSipCall][app] encryptMobile失败", e);
+            return AjaxResult.error("获取加密手机号失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 坐席登录IPCC外呼平台
+     * <p>前端 JsSIP 工具条启动后,使用此接口让 IPCC 平台知晓"该分机已上线、可派发呼叫"。
+     *
+     * @param param 至少包含 username/password/extNum 等表单参数
+     */
+    @Login
+    @ApiOperation("坐席登录IPCC外呼平台")
+    @PostMapping("/agentLogin")
+    public AjaxResult agentLogin(@RequestBody Map<String, Object> param) {
+        return aiSipCallUserService.agentLogin(param);
+    }
+
+    /**
+     * 通话挂断后根据UUID同步话单
+     * <p>前端 JsSIP 监听到 BYE/挂机后调用本接口,由后端从 IPCC 拉取话单并落库。
+     */
+    @Login
+    @ApiOperation("通话挂断后同步话单")
+    @PostMapping("/callEndSyncByUuid")
+    public AjaxResult callEndSyncByUuid(@RequestBody AiSipCallOutboundCdr request) {
+        if (request == null || StringUtils.isBlank(request.getUuid())) {
+            return AjaxResult.error("获取手动外呼通话记录同步失败,uuid为空");
+        }
+        // 用APP登录态填充必备的归属字段(覆盖前端任何尝试性传值,避免越权)
+        request.setSourceType("0");
+        Long companyId = getCompanyId();
+        Long companyUserId = getCompanyUserId();
+        request.setCompanyId(companyId);
+        request.setCompanyUserId(companyUserId);
+        if (companyUserId != null) {
+            CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
+            if (companyUser != null) {
+                request.setCompanyUserName(companyUser.getUserName());
+            }
+        }
+        request.setStatus(0);
+        aiSipCallOutboundCdrService.callEndSyncByUuid(request);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 获取有效客户列表(effective_customer=1)
+     * <p>按销售id、创建时间范围、加密手机号、备注等条件筛选。
+     * 手机号传入明文,后端加密后进行LIKE匹配。
+     */
+    @Login
+    @ApiOperation("获取有效客户列表")
+    @GetMapping("/getEffectiveCustomerList")
+    public TableDataInfo getEffectiveCustomerList(
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "mobile", required = false) String mobile,
+            @RequestParam(value = "remark", required = false) String remark,
+            @RequestParam(value = "callStatus", required = false) Integer callStatus,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        String encryptedMobile = null;
+        if (StringUtils.isNotBlank(mobile)) {
+            encryptedMobile = PhoneUtil.encryptPhone(mobile);
+        }
+        PageHelper.startPage(pageNum, pageSize);
+        List<CrmCustomer> list = crmCustomerMapper.selectEffectiveCustomerList(
+                getCompanyUserId(), startTime, endTime, encryptedMobile, remark, callStatus);
+        if (list != null) {
+            for (CrmCustomer c : list) {
+                if (StringUtils.isNotBlank(c.getMobile())) {
+                    try {
+                        String decrypted = PhoneUtil.decryptPhone(c.getMobile());
+                        c.setMobile(decrypted.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                        if (Integer.valueOf(1).equals(c.getCanDecrypt())) {
+                            c.setDecryptedMobile(decrypted);
+                        }
+                    } catch (Exception e) {
+                        log.warn("[aiSipCall] 解密手机号失败, customerId={}", c.getCustomerId());
+                    }
+                }
+            }
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据客户ID查询呼叫记录
+     */
+    @Login
+    @ApiOperation("根据客户ID查询呼叫记录")
+    @GetMapping("/getCallRecordByCustomerId")
+    public TableDataInfo getCallRecordByCustomerId(
+            @RequestParam("customerId") Long customerId,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        AiSipCallOutboundCdr query = new AiSipCallOutboundCdr();
+        query.setCustomerId(customerId);
+        PageHelper.startPage(pageNum, pageSize);
+        List<AiSipCallOutboundCdr> list = aiSipCallOutboundCdrService.selectAiSipCallOutboundCdrList(query);
+        //拼接音频文件
+        list.forEach(data -> {
+            if (StringUtils.isNotBlank(data.getWavfile())) {
+                data.setWavfile(AUDIO_BASE_URL + data.getWavfile());
+            }
+        });
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据客户ID获取解密后的手机号明文
+     */
+    @Login
+    @ApiOperation("根据客户ID获取解密手机号")
+    @GetMapping("/decryptMobileByCustomerId")
+    public AjaxResult decryptMobileByCustomerId(@RequestParam("customerId") Long customerId) {
+        String encryptedMobile = crmCustomerMapper.selectCrmCustomerPhoneByCustomerId(customerId);
+        if (StringUtils.isBlank(encryptedMobile)) {
+            return AjaxResult.error("该客户无手机号");
+        }
+        try {
+            String decrypted = PhoneUtil.decryptPhone(encryptedMobile);
+            return AjaxResult.success("获取成功", decrypted);
+        } catch (Exception e) {
+            log.error("[aiSipCall] 解密手机号失败, customerId={}", customerId, e);
+            return AjaxResult.error("手机号解密失败");
+        }
+    }
+
+
+    /**
+     * 生成6位随机串(替代 his_java 中的 com.fs.his.utils.RandomUtil#generateRandomCode)
+     */
+    private String generateRandomTail() {
+        StringBuilder sb = new StringBuilder(RANDOM_TAIL_LEN);
+        for (int i = 0; i < RANDOM_TAIL_LEN; i++) {
+            sb.append(RANDOM_CHARS[RANDOM.nextInt(RANDOM_CHARS.length)]);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * XOR 加密(与 his_java 的 PhoneUtil.xorEncrypt 算法一致)。
+     * <p>内联在此,避免修改 SaaS 原有 PhoneUtil。
+     */
+    private String xorEncrypt(String data) {
+        byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
+        byte[] keyBytes = XOR_KEY.getBytes(StandardCharsets.UTF_8);
+        byte[] result = new byte[dataBytes.length];
+        for (int i = 0; i < dataBytes.length; i++) {
+            result[i] = (byte) (dataBytes[i] ^ keyBytes[i % keyBytes.length]);
+        }
+        return Base64.getEncoder().encodeToString(result);
+    }
+}

+ 52 - 0
fs-company-app/src/main/java/com/fs/app/controller/crm/CrmAPPMsgController.java

@@ -0,0 +1,52 @@
+package com.fs.app.controller.crm;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.crm.service.ICrmMsgService;
+import com.fs.crm.vo.CrmMsgTypeVO;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.system.vo.DictVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *  消息Controller(APP端)
+ *
+ * @author fs
+ * @date 2021-04-16
+ */
+@RestController
+@RequestMapping("/crm/msg")
+public class CrmAPPMsgController extends AppBaseController
+{
+    @Autowired
+    private ICrmMsgService crmMsgService;
+    @Autowired
+    private ISysDictDataService dictDataService;
+
+
+    @Login
+    @GetMapping("/getMsg")
+    public R getMsg(){
+        // APP端鉴权:从APPToken中解析companyUserId
+        Long companyUserId = getCompanyUserId();
+        //获取所有类型
+        List<DictVO> types = dictDataService.selectDictDataListByType("crm_msg_type");
+        List<CrmMsgTypeVO> counts = new ArrayList<>();
+        for(DictVO v : types){
+            Long count = crmMsgService.selectCrmMsgCountByUserId(companyUserId, Integer.parseInt(v.getDictValue()));
+            CrmMsgTypeVO typeBO = new CrmMsgTypeVO();
+            typeBO.setMsgType(Integer.parseInt(v.getDictValue()));
+            typeBO.setTotal(count);
+            typeBO.setMsgTypeName(v.getDictLabel());
+            counts.add(typeBO);
+        }
+        return R.ok().put("counts", counts);
+    }
+}

+ 15 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java

@@ -19,6 +19,8 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.mapper.EasyCallMapper;
 import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.framework.service.TokenService;
+import com.fs.his.utils.PhoneUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -26,8 +28,12 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
 
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 /**
  * aiSIP手动外呼通话记录Controller
@@ -46,6 +52,15 @@ public class AiSipCallOutboundCdrController extends BaseController
     private EasyCallMapper easyCallMapper;
     @Autowired
     TenantDataSourceManager tenantDataSourceManager;
+    @Autowired
+    private TokenService tokenService;
+
+    /** XOR加密公钥(与 his_java PhoneUtil.PUBLIC_KEY_STR 保持一致) */
+    private static final String XOR_KEY = "ylrz112233";
+    private static final int RANDOM_TAIL_LEN = 6;
+    private static final char[] RANDOM_CHARS =
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
+    private static final SecureRandom RANDOM = new SecureRandom();
 
     /**
      * 查询aiSIP手动外呼通话记录列表

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java

@@ -106,6 +106,7 @@ public class AiSipCallUserController extends BaseController
             aiSipCallUser.setCreateBy(loginUser.getUser().getUserName());
         }
         aiSipCallUser.setCreateTime(new Date());
+        aiSipCallUser.setUserSource("0");
         return toAjax(aiSipCallUserService.insertAiSipCallUser(aiSipCallUser));
     }
 
@@ -141,6 +142,7 @@ public class AiSipCallUserController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         aiSipCallUser.setUpdateBy(loginUser.getUser().getUserName());
         aiSipCallUser.setUpdateTime(new Date());
+        aiSipCallUser.setUserSource("0");
         return toAjax(aiSipCallUserService.updateAiSipCallUser(aiSipCallUser));
     }
 
@@ -232,4 +234,15 @@ public class AiSipCallUserController extends BaseController
         }
         return aiSipCallUserService.getToolbarBasicParam(param);
     }
+
+    /**
+     * 登录外呼平台接口
+     * @param param 参数
+     * @return AjaxResult 结果
+     */
+    @PostMapping("/agentLogin")
+    public AjaxResult agentLogin(@RequestBody Map<String,Object> param)
+    {
+        return aiSipCallUserService.agentLogin(param);
+    }
 }

+ 21 - 0
fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java

@@ -95,6 +95,11 @@ public class RemoteCommon {
      */
     public static final String QUERY_OUTBOUNDCDR_LIST_API = "/aicall/api/outboundcdrList";
 
+    /**
+     * 登录外呼平台接口
+     */
+    public static final String AI_CALL_LOGIN_API = "/login";
+
     /**
      * 发送get请求
      * @param url   地址
@@ -123,4 +128,20 @@ public class RemoteCommon {
         }
         return null;
     }
+
+    /**
+     * 发送POST表单提交请求
+     * @param url   地址
+     * @param params 表单参数
+     * @return  String  结果
+     */
+    public static String sendPostForm(String url, java.util.Map<String, Object> params){
+        try{
+            return HttpUtil.post(url, params, 10 * 1000);
+        }catch (Exception e){
+            e.printStackTrace();
+            log.info("sendPostForm error");
+        }
+        return null;
+    }
 }

+ 38 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java

@@ -69,6 +69,44 @@ public class AiSipCallOutboundCdr implements Serializable {
     @Excel(name = "挂断原因")
     private String hangupCause;
 
+    /** 录音文件URL */
+    private String wavfile;
+
+    /** 外呼类型(0销售后台 1总后台 3APP) */
+    @Excel(name = "外呼类型", readConverterExp = "0=销售后台,1=总后台,3=APP")
+    private String sourceType;
+
+    /** 客户ID */
+    @Excel(name = "客户ID")
+    private Long customerId;
+
+    /** 销售公司ID */
+    @Excel(name = "销售公司ID")
+    private Long companyId;
+
+    /** 销售ID */
+    @Excel(name = "销售ID")
+    private Long companyUserId;
+
+    /** 销售公司名称 */
+    @Excel(name = "销售公司名称")
+    private String companyName;
+
+    /** 销售账号 */
+    @Excel(name = "销售账号")
+    private String companyUserName;
+
+    /** 总后台用户ID */
+    @Excel(name = "总后台用户ID")
+    private Long sysUserId;
+
+    /** 总后台用户账号 */
+    @Excel(name = "总后台用户账号")
+    private String sysUserName;
+
+    /** 状态(0正常 1删除) */
+    private Integer status;
+
     /** 通话总时长起止 */
     private Long timeLenStart;
     private Long timeLenEnd;

+ 12 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java

@@ -98,5 +98,17 @@ public class AiSipCallUser extends BaseEntity{
     private Long companyId;
     private Long companyUserId;
 
+    /** 用户来源0销售 1总后台 */
+    private String userSource;
+    /** 总后台用户ID */
+    private Long sysUserId;
+    /** 总后台用户账号 */
+    private String sysUserName;
+    /** 分机密码(SIP注册必需) */
+    private String extPass;
+    /** 分机绑定用户工号 */
+    private String userCode;
+    /** 销售公司名称 */
+    private String companyName;
 
 }

+ 13 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java

@@ -67,8 +67,21 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     AjaxResult getCustCommunicationInfo(String phoneNum, Integer callType, String uuid);
 
+    /**
+     * 获取手动外呼客户沟通信息(带拨号模式 dialMode)
+     * <p>当 dialMode = "encrypted" 时,phoneNum 为密文,需先解密再透传给 IPCC。
+     * <p>本方法为 his_java 兼容方法,原 3 参方法保留不动。
+     */
+    AjaxResult getCustCommunicationInfo(String phoneNum, Integer callType, String uuid, String dialMode);
+
     AjaxResult addCustcallrecord(CcCustInfo ccCustInfo);
 
+    /**
+     * 通话挂断后根据UUID同步话单(迁移自 his_java,APP/PC软电话挂机回调使用)
+     * <p>调用方需先填好 sourceType / companyId / companyUserId / companyUserName / status / sysUserId 等字段。
+     */
+    void callEndSyncByUuid(AiSipCallOutboundCdr request);
+
     CompletableFuture<String> scheduledGetCallRecord();
 
     int syncByUuid(ApiCallRecordByUuidQueryParams req, EasyCallOutBoundVO callPhoneRes);

+ 6 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java

@@ -99,4 +99,10 @@ public interface IAiSipCallUserService extends IService<AiSipCallUser>{
      * @return
      */
     String getGateWayIdListByCompanyId(Long companyId);
+
+    /**
+     * 坐席登录IPCC外呼平台(迁移自 his_java,APP/PC软电话工具条登录使用)
+     * @param param 包含 username/password/extNum 等表单参数
+     */
+    AjaxResult agentLogin(java.util.Map<String, Object> param);
 }

+ 76 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java

@@ -17,6 +17,7 @@ import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
 import com.fs.aiSipCall.utils.DateUtils;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.company.domain.CompanyAiWorkflowExec;
 import com.fs.company.domain.CrmCustomerCallLog;
 import com.fs.company.domain.CompanyVoiceRoboticBusiness;
@@ -36,6 +37,7 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.net.URLDecoder;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -208,6 +210,80 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
         return AjaxResult.error();
     }
 
+    /**
+     * 获取手动外呼客户沟通信息(带拨号模式 dialMode,迁移自 his_java)
+     */
+    @Override
+    public AjaxResult getCustCommunicationInfo(String phoneNum, Integer callType, String uuid, String dialMode) {
+        if (StringUtils.isNotBlank(dialMode) && "encrypted".equals(dialMode)) {
+            //密文需要解密
+            phoneNum = PhoneUtil.decryptPhone(phoneNum);
+        }
+        String paramStr = "?phoneNum=" + phoneNum + "&callType=" + callType + "&uuid=" + uuid;
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.GET_CUST_COMMUNICATION_INFO_API + paramStr);
+        String msg;
+        if (StringUtils.isNotBlank(result)) {
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if (jsonObject.getInteger("code") == 0) {
+                return JSONObject.parseObject(result, AjaxResult.class);
+            } else {
+                msg = "获取手动外呼客户沟通信息失败:" + jsonObject.getString("msg");
+            }
+        } else {
+            msg = "获取手动外呼客户沟通信息失败:接口返回为空";
+        }
+        return AjaxResult.error(msg);
+    }
+
+    /**
+     * 通话挂断后根据UUID同步话单(迁移自 his_java,APP/PC软电话挂机回调使用)
+     */
+    @Override
+    public void callEndSyncByUuid(AiSipCallOutboundCdr request) {
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.QUERY_OUTBOUNDCDR_LIST_API, JSONObject.toJSONString(request));
+        if (StringUtils.isNotBlank(result)) {
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+            if (code != null && code == 0) {
+                String rows = jsonObject.getString("rows");
+                if (StringUtils.isNotBlank(rows)) {
+                    List<AiSipCallOutboundCdr> list = JSONObject.parseArray(rows, AiSipCallOutboundCdr.class);
+                    if (list != null && !list.isEmpty()) {
+                        AiSipCallOutboundCdr data = list.get(0);
+                        data.setSourceType("3");
+                        data.setCompanyId(request.getCompanyId());
+                        data.setCompanyUserId(request.getCompanyUserId());
+                        data.setCompanyUserName(request.getCompanyUserName());
+                        data.setStatus(request.getStatus());
+                        data.setSysUserId(request.getSysUserId());
+                        data.setSysUserName(request.getSysUserName());
+                        data.setCustomerId(request.getCustomerId());
+                        //拼接数据
+                        String filename = data.getRecordFilename().startsWith("/") ? data.getRecordFilename().substring(1) : data.getRecordFilename();
+                        data.setWavfile("/recordings/files?filename=" + filename);
+                        // 对opnum字段进行URL解码,防止乱码
+                        if (StringUtils.isNotBlank(data.getOpnum())) {
+                            try {
+                                data.setOpnum(URLDecoder.decode(data.getOpnum(), "UTF-8"));
+                            } catch (Exception e) {
+                                log.error("opnum字段URL解码失败,原值:{}", data.getOpnum(), e);
+                            }
+                        }
+                        this.save(data);
+                    } else {
+                        log.error("获取手动外呼记录接口转化数据失败:原数据:{},原因:{}", rows, jsonObject.getString("msg"));
+                    }
+                } else {
+                    log.error("获取手动外呼记录接口获取rows失败:原因:{}", jsonObject.getString("msg"));
+                }
+            } else {
+                log.error("同步手动外呼记录接口失败:返回状态码:{},原因:{}", code, jsonObject.getString("msg"));
+            }
+        } else {
+            log.error("同步手动外呼记录接口失败:无返回结果");
+        }
+    }
+
     private final AtomicBoolean isRunning = new AtomicBoolean(false);
     @Override
     @Async

+ 41 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java

@@ -101,6 +101,9 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
                     if(remoteAiSipCallUser.getCreateTime() != null){
                         aiSipCallUser.setCreateTime(remoteAiSipCallUser.getCreateTime());
                     }
+                    if(StringUtils.isNotBlank(remoteAiSipCallUser.getExtPass())){
+                        aiSipCallUser.setExtPass(remoteAiSipCallUser.getExtPass());
+                    }
                     int i = baseMapper.insertAiSipCallUser(aiSipCallUser);
                     if( i> 0){
                         //绑定companyUser的aiSIP外呼用户
@@ -146,6 +149,11 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
                 }else{
                     throw new RuntimeException("分机号已被绑定,请刷新后重试");
                 }
+                //获取分机密码
+                if(bind.getExtensionPass() != null){
+                    aiSipCallUser.setExtPass(bind.getExtensionPass());
+                    baseMapper.updateAiSipCallUser(aiSipCallUser);
+                }
             }
             if (aiSipCallUser.getCompanyUserId() != null && aiSipCallUser.getUserId() != null) {
                 companyUserMapper.updateCompanyUserByAiSipCall(aiSipCallUser.getCompanyUserId(), aiSipCallUser.getUserId());
@@ -167,6 +175,15 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         if(StringUtils.isNotBlank(result)){
             JSONObject jsonObject = JSONObject.parseObject(result);
             if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                AiSipCallUser remoteAiSipCallUser = JSONObject.parseObject(data, AiSipCallUser.class);
+                if(remoteAiSipCallUser != null){
+                    if(StringUtils.isNotBlank(remoteAiSipCallUser.getExtPass())){
+                        aiSipCallUser.setExtPass(remoteAiSipCallUser.getExtPass());
+                    }
+                }else{
+                    log.error("新增时解析aiSIP外呼用户数据为空");
+                }
                 return baseMapper.updateAiSipCallUser(aiSipCallUser);
             }else{
                 log.error("修改aiSIP外呼任务失败:{}", jsonObject.getString("msg"));
@@ -194,6 +211,9 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
             result.setNewExtNum(newExtNum);
             result.setOldExtNum(oldExtNum);
             result.setNewUserCode(aiSipCallUser.getLoginName());
+            if(StringUtils.isNotBlank(oldUser.getExtPass()) && StringUtils.isNotBlank(bind.getExtensionPass())){
+                aiSipCallUser.setExtPass(bind.getExtensionPass());
+            }
             int rows = aiSipCallUserMapper.updateAiSipCallUser(aiSipCallUser);
             //解除绑定
             companyExtensionBindService.clearBindByExtNum(oldExtNum, aiSipCallUser.getCompanyId(), aiSipCallUser.getCompanyUserId());
@@ -310,4 +330,25 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         return "";
     }
 
+    /**
+     * 坐席登录IPCC外呼平台(迁移自 his_java)
+     */
+    @Override
+    public AjaxResult agentLogin(Map<String, Object> param) {
+        String result = RemoteCommon.sendPostForm(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.AI_CALL_LOGIN_API, param);
+        String msg;
+        if (StringUtils.isNotBlank(result)) {
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+            if (code != null && code == 0) {
+                return JSONObject.parseObject(result, AjaxResult.class);
+            } else {
+                msg = jsonObject.getString("msg");
+            }
+        } else {
+            msg = "登录外呼平台接口失败:接口返回为空";
+        }
+        return AjaxResult.error(msg);
+    }
+
 }

+ 9 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -1,6 +1,7 @@
 package com.fs.crm.domain;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
@@ -197,4 +198,12 @@ public class CrmCustomer extends BaseEntity
     //    最后一次设置外呼记录id
     private Long lastEffectiveCallphoneLogId;
 
+    /** 是否可解密手机号 1=可解密 */
+    @TableField(exist = false)
+    private Integer canDecrypt;
+
+    /** 解密后的手机号明文 */
+    @TableField(exist = false)
+    private String decryptedMobile;
+
 }

+ 26 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -1012,6 +1012,32 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
     @Select("select customer_id  from  crm_customer where company_id = #{companyId} and  mobile=#{remarkMobile} limit 1")
     Long selectCrmCustomerByCrmMobileAndCompanyId(@Param("companyId") Long companyId, @Param("remarkMobile") String remarkMobile);
 
+    /**
+     * 查询有效客户列表(effective_customer=1)
+     * callStatus: 0-全部, 1-已解密(有外呼记录), 2-未解密(无外呼记录)
+     */
+    @Select({"<script> " +
+            "SELECT c.*, CASE WHEN cdr.customer_id IS NOT NULL THEN 1 ELSE 0 END AS canDecrypt " +
+            "FROM crm_customer c " +
+            "LEFT JOIN (SELECT DISTINCT customer_id FROM ai_sip_call_outbound_cdr WHERE status = 0) cdr ON cdr.customer_id = c.customer_id " +
+            "WHERE c.effective_customer = 1 " +
+            "<if test='receiveUserId != null'> AND c.receive_user_id = #{receiveUserId} </if>" +
+            "<if test='startTime != null and startTime != \"\"'> AND c.create_time &gt; #{startTime} </if>" +
+            "<if test='endTime != null and endTime != \"\"'> AND c.create_time &lt;= #{endTime} </if>" +
+            "<if test='mobile != null and mobile != \"\"'> AND c.mobile LIKE CONCAT('%', #{mobile}, '%') </if>" +
+            "<if test='remark != null and remark != \"\"'> AND c.remark LIKE CONCAT('%', #{remark}, '%') </if>" +
+            "<if test='callStatus != null and callStatus == 1'> AND EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr2 WHERE cdr2.customer_id = c.customer_id) </if>" +
+            "<if test='callStatus != null and callStatus == 2'> AND NOT EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr2 WHERE cdr2.customer_id = c.customer_id) </if>" +
+            " ORDER BY c.customer_id DESC " +
+            "</script>"})
+    List<CrmCustomer> selectEffectiveCustomerList(
+            @Param("receiveUserId") Long receiveUserId,
+            @Param("startTime") String startTime,
+            @Param("endTime") String endTime,
+            @Param("mobile") String mobile,
+            @Param("remark") String remark,
+            @Param("callStatus") Integer callStatus);
+
     /**
      * 批量插入客户
      *

+ 48 - 0
fs-service/src/main/resources/db/tenant-initTable.sql

@@ -18412,6 +18412,12 @@ CREATE TABLE `ai_sip_call_user`
     `company_id`      bigint NULL DEFAULT NULL COMMENT '企业ID',
     `company_user_id` bigint NULL DEFAULT NULL COMMENT '销售ID',
     `gateway_ids`     varchar(255)  NULL DEFAULT NULL COMMENT '网关ids',
+    `ext_pass`        varchar(64)   NULL DEFAULT NULL COMMENT '分机密码(SIP注册必需)',
+    `user_code`       varchar(64)   NULL DEFAULT NULL COMMENT '分机绑定用户工号',
+    `user_source`     varchar(2)    NULL DEFAULT '0' COMMENT '用户来源(0销售 1总后台)',
+    `sys_user_id`     bigint        NULL DEFAULT NULL COMMENT '总后台用户ID',
+    `sys_user_name`   varchar(64)   NULL DEFAULT NULL COMMENT '总后台用户账号',
+    `company_name`    varchar(128)  NULL DEFAULT NULL COMMENT '销售公司名称',
     PRIMARY KEY (`user_id`) USING BTREE
 ) ENGINE = InnoDB  AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'sip用户信息表' ROW_FORMAT = DYNAMIC;
 
@@ -18436,6 +18442,15 @@ CREATE TABLE `ai_sip_call_outbound_cdr`
     `record_filename` varchar(100)  NOT NULL COMMENT '录音文件名',
     `chat_content`    text  NULL COMMENT '对话内容',
     `hangup_cause`    varchar(50)   NOT NULL COMMENT '挂断原因',
+    `wavfile`            varchar(255) NULL DEFAULT NULL COMMENT '录音文件URL',
+    `source_type`        varchar(2)   NULL DEFAULT NULL COMMENT '外呼类型(0销售后台 1总后台)',
+    `company_id`         bigint       NULL DEFAULT NULL COMMENT '销售公司ID',
+    `company_user_id`    bigint       NULL DEFAULT NULL COMMENT '销售ID',
+    `company_name`       varchar(128) NULL DEFAULT NULL COMMENT '销售公司名称',
+    `company_user_name`  varchar(64)  NULL DEFAULT NULL COMMENT '销售账号',
+    `sys_user_id`        bigint       NULL DEFAULT NULL COMMENT '总后台用户ID',
+    `sys_user_name`      varchar(64)  NULL DEFAULT NULL COMMENT '总后台用户账号',
+    `status`             tinyint      NULL DEFAULT 0    COMMENT '状态(0正常 1删除)',
     PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
 
@@ -18606,3 +18621,36 @@ CREATE TABLE `company_extension_bind`
 
 SET
 FOREIGN_KEY_CHECKS = 1;
+
+-- ----------------------------
+-- Table structure for crm_customer_call_app_log
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_customer_call_app_log`;
+CREATE TABLE `crm_customer_call_app_log`
+(
+    `log_id`           bigint NOT NULL AUTO_INCREMENT,
+    `customer_id`      bigint NULL DEFAULT NULL COMMENT '客户ID',
+    `run_time`         datetime NULL DEFAULT NULL COMMENT '记录调用时间',
+    `run_param`        text  NULL COMMENT '调用参数',
+    `result`           text  NULL COMMENT '回调返回结果',
+    `status`           tinyint NULL DEFAULT NULL COMMENT '执行状态:1、执行中,2、执行成功,3、执行失败',
+    `create_time`      datetime NULL DEFAULT NULL COMMENT '创建时间',
+    `record_path`      varchar(1000)  NULL DEFAULT NULL COMMENT '录音地址',
+    `content_list`     text  NULL COMMENT '通话详细列表',
+    `caller_num`       varchar(50)  NULL DEFAULT NULL COMMENT '客户号码',
+    `callee_num`       varchar(50)  NULL DEFAULT NULL COMMENT '话术号码',
+    `uuid`             varchar(100)  NULL DEFAULT NULL COMMENT '通话的唯一标识',
+    `call_create_time` bigint NULL DEFAULT NULL COMMENT '呼入时间',
+    `call_answer_time` bigint NULL DEFAULT NULL COMMENT '应答时间',
+    `intention`        varchar(255)  NULL DEFAULT NULL COMMENT '客户类型',
+    `company_id`       int NULL DEFAULT NULL COMMENT '公司id',
+    `company_user_id`  bigint NULL DEFAULT NULL COMMENT '销售id',
+    `call_time`        int NULL DEFAULT NULL COMMENT '通话时长,单位秒',
+    `cost`             decimal(10, 2) NULL DEFAULT NULL COMMENT '花费金额',
+    `create_by`        varchar(255)  NULL DEFAULT NULL,
+    `update_by`        varchar(255)  NULL DEFAULT NULL,
+    `update_time`      datetime NULL DEFAULT NULL
+    PRIMARY KEY (`log_id`) USING BTREE,
+    INDEX              `company_and_company_user_idx`(`company_id`, `company_user_id`) USING BTREE,
+    INDEX              `customer_id_idx`(`customer_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;

+ 11 - 1
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml

@@ -19,10 +19,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="recordFilename"    column="record_filename"    />
         <result property="chatContent"    column="chat_content"    />
         <result property="hangupCause"    column="hangup_cause"    />
+        <result property="sourceType"    column="source_type"    />
+        <result property="customerId"    column="customer_id"    />
     </resultMap>
 
     <sql id="selectAiSipCallOutboundCdrVo">
-        select id, caller, opnum, callee, start_time, answered_time, end_time, uuid, call_type, time_len, time_len_valid, record_filename, chat_content, hangup_cause from ai_sip_call_outbound_cdr
+        select id, caller, opnum, callee, start_time, answered_time, end_time, uuid, call_type, time_len, time_len_valid, record_filename, chat_content, hangup_cause, source_type, customer_id,wavfile from ai_sip_call_outbound_cdr
     </sql>
 
     <select id="selectAiSipCallOutboundCdrList" parameterType="AiSipCallOutboundCdr" resultMap="AiSipCallOutboundCdrResult">
@@ -41,6 +43,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="recordFilename != null  and recordFilename != ''"> and record_filename like concat('%', #{recordFilename}, '%')</if>
             <if test="chatContent != null  and chatContent != ''"> and chat_content = #{chatContent}</if>
             <if test="hangupCause != null  and hangupCause != ''"> and hangup_cause = #{hangupCause}</if>
+            <if test="sourceType != null  and sourceType != ''"> and source_type = #{sourceType}</if>
+            <if test="customerId != null"> and customer_id = #{customerId}</if>
             <if test="timeLenStart != null  and timeLenStart != ''"> and time_len &gt;= #{timeLenStart}</if>
             <if test="timeLenEnd != null  and timeLenEnd != ''"> and time_len &lt;= #{timeLenEnd}</if>
             <if test="startTimeStartLong != null  and startTimeStartLong != ''"> and start_time &gt;= #{startTimeStartLong}</if>
@@ -75,6 +79,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="recordFilename != null and recordFilename != ''">record_filename,</if>
             <if test="chatContent != null">chat_content,</if>
             <if test="hangupCause != null and hangupCause != ''">hangup_cause,</if>
+            <if test="sourceType != null and sourceType != ''">source_type,</if>
+            <if test="customerId != null">customer_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="id != null">#{id},</if>
@@ -91,6 +97,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="recordFilename != null and recordFilename != ''">#{recordFilename},</if>
             <if test="chatContent != null">#{chatContent},</if>
             <if test="hangupCause != null and hangupCause != ''">#{hangupCause},</if>
+            <if test="sourceType != null and sourceType != ''">#{sourceType},</if>
+            <if test="customerId != null">#{customerId},</if>
          </trim>
     </insert>
 
@@ -110,6 +118,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="recordFilename != null and recordFilename != ''">record_filename = #{recordFilename},</if>
             <if test="chatContent != null">chat_content = #{chatContent},</if>
             <if test="hangupCause != null and hangupCause != ''">hangup_cause = #{hangupCause},</if>
+            <if test="sourceType != null and sourceType != ''">source_type = #{sourceType},</if>
+            <if test="customerId != null">customer_id = #{customerId},</if>
         </trim>
         where id = #{id}
     </update>

+ 2 - 1
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallUserMapper.xml

@@ -34,7 +34,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectAiSipCallUserVo">
-        select user_id, dept_id, login_name, user_name, user_type, email, phonenumber, sex, avatar, password, salt, status, del_flag, login_ip, login_date, pwd_update_date, remark, logo, ext_num, create_by, create_time, update_by, update_time, company_id,company_user_id,gateway_ids from ai_sip_call_user
+        select user_id, dept_id, login_name, user_name, user_type, email, phonenumber, sex, avatar, password, salt, status, del_flag, login_ip, login_date, pwd_update_date, remark, logo, ext_num, create_by, create_time, update_by, update_time, company_id,company_user_id,gateway_ids,ext_pass from ai_sip_call_user
     </sql>
 
     <select id="selectAiSipCallUserList" parameterType="AiSipCallUser" resultMap="AiSipCallUserResult">
@@ -154,6 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="extNum != null and extNum != ''">ext_num = #{extNum},</if>
             <if test="companyId != null">company_id = #{companyId},</if>
             <if test="gatewayIds != null and gatewayIds != ''">gateway_ids = #{gatewayIds},</if>
+            <if test="extPass != null and extPass != ''">ext_pass = #{extPass},</if>
         </trim>
         where user_id = #{userId}
     </update>