Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/master'

zyy 2 nedēļas atpakaļ
vecāks
revīzija
8359191f82

+ 92 - 0
fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java

@@ -0,0 +1,92 @@
+package com.fs.his.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.*;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/6 14:28
+ * @description EasyCallCenter365 外呼管理控制器
+ * <p>
+ * 所有接口均需要登录态,通过 TokenService 获取当前登录用户的 companyId。
+ * 接口地址前缀:/company/easyCall
+ * 对应三方服务器:http://129.28.164.235:8899
+ */
+@Api(tags = "EasyCallCenter365外呼管理")
+@RestController
+@RequestMapping("/easyCall")
+public class EasyCallController extends BaseController {
+
+    @Autowired
+    private IEasyCallService easyCallService;
+    @Autowired
+    ISysConfigService iSysConfigService;
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/list")
+    public R getGatewayList() {
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/getGatewayCompanyList")
+    public R getGatewayCompanyList() {
+        List<EasyCallGatewayVO> resList = new ArrayList<>();
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
+        // 如果原始列表为空,直接返回空列表
+        if (list == null || list.isEmpty()) {
+            return R.ok().put("data", resList);
+        }
+        SysConfig cidConf = iSysConfigService.selectConfigByConfigKey("cId.config");
+        if (null != cidConf && StringUtils.isNotBlank(cidConf.getConfigValue())) {
+            JSONObject jo = JSONObject.parseObject(cidConf.getConfigValue());
+            if (null != jo && jo.containsKey("showGatewayIds")) {
+                List<Long> gatewayIdList = jo.getJSONArray("showGatewayIds").toJavaList(Long.class);
+                if (gatewayIdList != null && !gatewayIdList.isEmpty()) {
+                    // 将配置中的网关ID列表转换为Set集合便于快速匹配
+                    Set<Long> gatewayIdSet = gatewayIdList.stream()
+                            .filter(id -> id != null)
+                            .collect(Collectors.toSet());
+                    // 过滤list,只保留id在配置集合中的网关
+                    resList = list.stream()
+                            .filter(gateway -> gateway.getId() != null && gatewayIdSet.contains(gateway.getId()))
+                            .collect(Collectors.toList());
+                } else {
+                    // 配置为空时返回原始列表
+                    resList = list;
+                }
+            } else {
+                // 未配置showGatewayIds时返回原始列表
+                resList = list;
+            }
+        } else {
+            // 未找到配置时返回原始列表
+            resList = list;
+        }
+        return R.ok().put("data", resList);
+    }
+}

+ 28 - 9
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -56,8 +56,10 @@ import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 import com.fs.company.service.ICompanySmsCommonLogsService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 
 @Service
 @Slf4j
@@ -104,6 +106,9 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private CompanyVoiceRoboticCallBlacklistServiceImpl companyVoiceRoboticCallBlacklistService;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -922,7 +927,10 @@ public class SmsServiceImpl implements ISmsService
         }
     }
 
-    private static ConcurrentHashMap<Long, Object> smsCompanyLock = new ConcurrentHashMap();
+    /**
+     * 短信扣减分布式锁key前缀
+     */
+    private static final String SMS_SUB_LOCK_PREFIX = "SMS_SUB_LOCK:";
 
     /**
      * copy一份 batchSmsOp
@@ -1002,12 +1010,17 @@ public class SmsServiceImpl implements ISmsService
                             }
                             logs.setNumber(counts);
                             smsLogsService.insertCompanySmsLogs(logs);
-                            Object lock = smsCompanyLock.computeIfAbsent(logs.getCompanyId(), k -> new Object());
-                            //任务调用时可能存在并发情况 这里优化一下扣减时条数准确
-                            synchronized (lock) {
+                            // 使用Redisson分布式锁,按公司ID级别锁定,确保集群环境下扣减准确
+                            String lockKey = SMS_SUB_LOCK_PREFIX + logs.getCompanyId();
+                            RLock lock = redissonClient.getLock(lockKey);
+                            try {
+                                lock.lock(30, TimeUnit.SECONDS);
                                 companySmsService.subCompanySms(logs.getCompanyId(), logs.getNumber());
+                            } finally {
+                                if (lock.isHeldByCurrentThread()) {
+                                    lock.unlock();
+                                }
                             }
-//                            companySmsService.subCompanySms(logs.getCompanyId(),logs.getNumber());
                             resultCount++;
                         }
                     }
@@ -1044,10 +1057,16 @@ public class SmsServiceImpl implements ISmsService
                     if (sendSmsReturn.getResult()!=null&&sendSmsReturn.getResult().equals("0")){
                         logs.setMid(sendSmsReturn.getMsgid());
                         smsLogsService.insertCompanySmsLogs(logs);
-//                        companySmsService.subCompanySms(logs.getCompanyId(),logs.getNumber());
-                        Object lock = smsCompanyLock.computeIfAbsent(logs.getCompanyId(), k -> new Object());
-                        synchronized (lock) {
+                        // 使用Redisson分布式锁,按公司ID级别锁定,确保集群环境下扣减准确
+                        String lockKey = SMS_SUB_LOCK_PREFIX + logs.getCompanyId();
+                        RLock lock = redissonClient.getLock(lockKey);
+                        try {
+                            lock.lock(30, TimeUnit.SECONDS);
                             companySmsService.subCompanySms(logs.getCompanyId(), logs.getNumber());
+                        } finally {
+                            if (lock.isHeldByCurrentThread()) {
+                                lock.unlock();
+                            }
                         }
                     }
                 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/Company.java

@@ -143,4 +143,7 @@ public class Company extends BaseEntity
 
     @TableField(exist = false)
     private List<Long> ids;
+
+    @TableField(exist = false)
+    private List<Long> showGatewayIds;
 }

+ 22 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyBindGateway.java

@@ -0,0 +1,22 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/1 16:22
+ * @description
+ */
+@Data
+public class CompanyBindGateway {
+    private  Integer id;
+    //网关线路id
+    private Long gatewayId;
+    //公司id
+    private Long companyId;
+    //创建时间
+    private Date createTime;
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyBindGatewayMapper.java

@@ -0,0 +1,17 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/1 16:34
+ * @description
+ */
+public interface CompanyBindGatewayMapper {
+
+    int deleteDataByCompanyId(@Param("companyId") Long companyId);
+
+    int insertData(@Param("companyId") Long companyId, @Param("gatewayIds") List<Long> gatewayIds);
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -253,4 +253,6 @@ public interface CompanyMapper
 
     @Select("select company_id,company_name from company where FIND_IN_SET( company_id , (select company_ids from qw_company where corp_id = #{corpId})) ")
     List<Company> getCompanyList(@Param("corpId") String corpId);
+
+    String getGateWayList(@Param("companyId") Long companyId);
 }

+ 61 - 23
fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java

@@ -4,15 +4,19 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.vo.easycall.*;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpUtil;
 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 java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * EasyCallCenter365 外呼服务实现类
@@ -27,34 +31,60 @@ import java.util.List;
 @Slf4j
 public class EasyCallServiceImpl implements IEasyCallService {
 
-    /** EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取 */
+    /**
+     * EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取
+     */
     @Value("${easycall.base-url:http://129.28.164.235:8899}")
     private String baseUrl;
 
     // =================== EasyCallCenter365 接口路径常量 ===================
-    /** 获取外呼网关列表 */
-    private static final String API_GATEWAY_LIST     = "/aicall/api/gateway/list";
-    /** 获取大模型配置列表 */
-    private static final String API_LLMACCOUNT_LIST  = "/aicall/api/llmacount/list";
-    /** 获取音色列表 */
-    private static final String API_VOICECODE_LIST   = "/aicall/api/voicecode/list";
-    /** 获取技能组(业务组)列表 */
-    private static final String API_BUSIGROUP_LIST   = "/aicall/api/busigroup/list";
-    /** 分页查询任务列表 */
-    private static final String API_CALLTASK_LIST    = "/aicall/api/calltask/list";
-    /** 分页查询通话记录 */
-    private static final String API_RECORDS_LIST     = "/aicall/api/records/list";
-    /** 创建外呼任务 */
-    private static final String API_CREATE_TASK      = "/aicall/api/ai/createTask";
-    /** 启动外呼任务(GET,携带 batchId 参数) */
-    private static final String API_START_TASK       = "/aicall/api/ai/startTask";
-    /** 停止外呼任务(GET,携带 batchId 参数) */
-    private static final String API_STOP_TASK        = "/aicall/api/ai/stopTask";
-    /** AI外呼专用追加名单(仅支持 AI 外呼任务,phoneList 为纯号码列表) */
+    /**
+     * 获取外呼网关列表
+     */
+    private static final String API_GATEWAY_LIST = "/aicall/api/gateway/list";
+    /**
+     * 获取大模型配置列表
+     */
+    private static final String API_LLMACCOUNT_LIST = "/aicall/api/llmacount/list";
+    /**
+     * 获取音色列表
+     */
+    private static final String API_VOICECODE_LIST = "/aicall/api/voicecode/list";
+    /**
+     * 获取技能组(业务组)列表
+     */
+    private static final String API_BUSIGROUP_LIST = "/aicall/api/busigroup/list";
+    /**
+     * 分页查询任务列表
+     */
+    private static final String API_CALLTASK_LIST = "/aicall/api/calltask/list";
+    /**
+     * 分页查询通话记录
+     */
+    private static final String API_RECORDS_LIST = "/aicall/api/records/list";
+    /**
+     * 创建外呼任务
+     */
+    private static final String API_CREATE_TASK = "/aicall/api/ai/createTask";
+    /**
+     * 启动外呼任务(GET,携带 batchId 参数)
+     */
+    private static final String API_START_TASK = "/aicall/api/ai/startTask";
+    /**
+     * 停止外呼任务(GET,携带 batchId 参数)
+     */
+    private static final String API_STOP_TASK = "/aicall/api/ai/stopTask";
+    /**
+     * AI外呼专用追加名单(仅支持 AI 外呼任务,phoneList 为纯号码列表)
+     */
     private static final String API_AI_ADD_CALL_LIST = "/aicall/api/ai/addCallList";
-    /** 通用追加名单(同时支持 AI 外呼和通知提醒任务,支持传入提醒内容和业务数据) */
-    private static final String API_COMMON_ADD_LIST  = "/aicall/api/common/addCallList";
+    /**
+     * 通用追加名单(同时支持 AI 外呼和通知提醒任务,支持传入提醒内容和业务数据)
+     */
+    private static final String API_COMMON_ADD_LIST = "/aicall/api/common/addCallList";
 
+    @Autowired
+    CompanyMapper companyMapper;
     // =================== 基础数据查询接口 ===================
 
     /**
@@ -68,7 +98,15 @@ public class EasyCallServiceImpl implements IEasyCallService {
         JSONObject result = doGet(url);
         // 从响应的 data 字段中取出网关数组并转为 Java 对象列表
         JSONArray data = result.getJSONArray("data");
-        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallGatewayVO.class);
+        List<EasyCallGatewayVO> resList = data == null ? new ArrayList<>() : data.toJavaList(EasyCallGatewayVO.class);
+        if (null != companyId && null != resList && !resList.isEmpty()) {
+            String gateWayList = companyMapper.getGateWayList(companyId);
+            if(StringUtils.isNotBlank(gateWayList)){
+                List<Long> collect = Arrays.stream(gateWayList.split(",")).map(item -> Long.valueOf(item.trim())).collect(Collectors.toList());
+                resList = resList.stream().filter(item -> collect.contains(item.getId())).collect(Collectors.toList());
+            }
+        }
+        return resList;
     }
 
     /**

+ 28 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -140,7 +140,8 @@ public class CompanyServiceImpl implements ICompanyService
 
     @Autowired
     private ICompanyConfigService companyConfigService;
-
+    @Autowired
+    CompanyBindGatewayMapper companyBindGatewayMapper;
 
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
@@ -365,6 +366,15 @@ public class CompanyServiceImpl implements ICompanyService
             if(redPackageMoney!=null){
                 company.setRedPackageMoney(new BigDecimal(redPackageMoney));
             }
+            try {
+                String gateWayList = companyMapper.getGateWayList(companyId);
+                if(StringUtils.isNotBlank(gateWayList)){
+                    List<Long> collect = Arrays.stream(gateWayList.split(",")).map(item -> Long.valueOf(item.trim())).collect(Collectors.toList());
+                    company.setShowGatewayIds(collect);
+                }
+            } catch (Exception ex) {
+                logger.error("查询网关列表失败:{}", ex);
+            }
         }
         return company;
     }
@@ -504,6 +514,9 @@ public class CompanyServiceImpl implements ICompanyService
             // 红包余额缓存
             String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
             redisCache.setCacheObject(companyMoneyKey, new BigDecimal(0.00).toString());
+            if(null != company.getShowGatewayIds() && !company.getShowGatewayIds().isEmpty()){
+                handleCompanyBindGateway(company.getCompanyId(),company.getShowGatewayIds());
+            }
             return R.ok();
         }
         else
@@ -525,8 +538,22 @@ public class CompanyServiceImpl implements ICompanyService
         if(company.isUpdateMiniApp()){
             bindMiniApp(company);
         }
+        if(null != company.getShowGatewayIds() && !company.getShowGatewayIds().isEmpty()){
+            handleCompanyBindGateway(company.getCompanyId(),company.getShowGatewayIds());
+        }
         return companyMapper.updateCompany(company);
     }
+
+    /**
+     * 处理公司绑定网关路线
+     * @param companyId
+     * @param gatewayIds
+     */
+    public void handleCompanyBindGateway(Long companyId,List<Long> gatewayIds){
+       companyBindGatewayMapper.deleteDataByCompanyId(companyId);
+       companyBindGatewayMapper.insertData(companyId, gatewayIds);
+    }
+
     // 绑定小程序
     public void bindMiniApp(Company company){
         companyMiniappService.removeByCompanyId(company.getCompanyId());

+ 20 - 0
fs-service/src/main/resources/mapper/company/CompanyBindGatewayMapper.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyBindGatewayMapper">
+
+    <delete id="deleteDataByCompanyId" parameterType="java.lang.Long">
+        delete from company_bind_gateway where company_id = #{companyId}
+    </delete>
+
+    <insert id="insertData">
+        <if test="gatewayIds != null and gatewayIds.size() > 0">
+            insert into company_bind_gateway(company_id, gateway_id,create_time) values
+            <foreach item="item" collection="gatewayIds" separator=",">
+                (#{companyId},#{item},now())
+            </foreach>
+        </if>
+    </insert>
+
+</mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/company/CompanyMapper.xml

@@ -277,4 +277,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <select id="getGateWayList" resultType="java.lang.String">
+        select GROUP_CONCAT(gateway_id) from company_bind_gateway where company_id = #{companyId}
+    </select>
+
 </mapper>