Explorar o código

通话api改动

lmx hai 2 días
pai
achega
3ad2b8bc8d

+ 27 - 10
fs-admin-saas/src/main/java/com/fs/company/controller/CompanyVoiceApiController.java

@@ -8,7 +8,9 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
 import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiQueryService;
 import com.fs.voice.service.IVoiceService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -17,7 +19,7 @@ import org.springframework.web.bind.annotation.*;
 import java.util.List;
 
 /**
- * 呼叫接口Controller
+ * 呼叫接口Controller(租户总后台:读租户库 company_voice_api)
  *
  * @author fs
  * @date 2021-10-04
@@ -28,16 +30,19 @@ public class CompanyVoiceApiController extends BaseController {
     @Autowired
     private ICompanyVoiceApiService companyVoiceApiService;
     @Autowired
+    private ITenantCompanyVoiceApiQueryService tenantCompanyVoiceApiQueryService;
+    @Autowired
     private IVoiceService voiceService;
 
     /**
-     * 查询呼叫接口列表
+     * 查询呼叫接口列表(租户库,含售价/优先级等同步字段)
      */
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceApi companyVoiceApi) {
         startPage();
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(companyVoiceApi);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(
+                toTenantQuery(companyVoiceApi));
         return getDataTable(list);
     }
 
@@ -48,8 +53,9 @@ public class CompanyVoiceApiController extends BaseController {
     @Log(title = "呼叫接口", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(CompanyVoiceApi companyVoiceApi) {
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(companyVoiceApi);
-        ExcelUtil<CompanyVoiceApi> util = new ExcelUtil<CompanyVoiceApi>(CompanyVoiceApi.class);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(
+                toTenantQuery(companyVoiceApi));
+        ExcelUtil<TenantCompanyVoiceApi> util = new ExcelUtil<>(TenantCompanyVoiceApi.class);
         return util.exportExcel(list, "companyVoiceApi");
     }
 
@@ -59,7 +65,7 @@ public class CompanyVoiceApiController extends BaseController {
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:query')")
     @GetMapping(value = "/{apiId}")
     public AjaxResult getInfo(@PathVariable("apiId") Long apiId) {
-        return AjaxResult.success(companyVoiceApiService.selectCompanyVoiceApiById(apiId));
+        return AjaxResult.success(tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiById(apiId));
     }
 
     /**
@@ -94,11 +100,22 @@ public class CompanyVoiceApiController extends BaseController {
 
     @GetMapping("/getVoiceApiList")
     public R getVoiceApiList() {
-        CompanyVoiceApi map = new CompanyVoiceApi();
-        map.setStatus(1);
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(map);
+        TenantCompanyVoiceApi query = new TenantCompanyVoiceApi();
+        query.setStatus(1);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(query);
         return R.ok().put("data", list);
     }
 
-
+    private TenantCompanyVoiceApi toTenantQuery(CompanyVoiceApi companyVoiceApi) {
+        TenantCompanyVoiceApi query = new TenantCompanyVoiceApi();
+        if (companyVoiceApi == null) {
+            return query;
+        }
+        query.setApiName(companyVoiceApi.getApiName());
+        if (companyVoiceApi.getApiType() != null) {
+            query.setApiType(String.valueOf(companyVoiceApi.getApiType()));
+        }
+        query.setStatus(companyVoiceApi.getStatus());
+        return query;
+    }
 }

+ 18 - 10
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java

@@ -104,6 +104,7 @@ public class CompanyVoiceApiTenantController extends BaseController {
                 if (tenantMap.get("tenantName") != null) {
                     tenant.setTenantName(tenantMap.get("tenantName").toString());
                 }
+                applyPricingFieldsFromMap(tenant, tenantMap);
                 if (tenant.getTenantId() != null) {
                     list.add(tenant);
                 }
@@ -208,23 +209,30 @@ public class CompanyVoiceApiTenantController extends BaseController {
 
     private CompanyVoiceApiTenant parsePricingFields(Map<String, Object> body) {
         CompanyVoiceApiTenant pricing = new CompanyVoiceApiTenant();
-        if (body.get("salePrice") != null && StringUtils.isNotEmpty(body.get("salePrice").toString())) {
-            pricing.setSalePrice(new java.math.BigDecimal(body.get("salePrice").toString()));
+        applyPricingFieldsFromMap(pricing, body);
+        return pricing;
+    }
+
+    private void applyPricingFieldsFromMap(CompanyVoiceApiTenant tenant, Map<String, Object> map) {
+        if (tenant == null || map == null) {
+            return;
         }
-        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
-            pricing.setPriority(Integer.valueOf(body.get("priority").toString()));
+        if (map.get("salePrice") != null && StringUtils.isNotEmpty(map.get("salePrice").toString())) {
+            tenant.setSalePrice(new java.math.BigDecimal(map.get("salePrice").toString()));
         }
-        if (body.get("isPrimary") != null && StringUtils.isNotEmpty(body.get("isPrimary").toString())) {
-            pricing.setIsPrimary(Integer.valueOf(body.get("isPrimary").toString()));
+        if (map.get("priority") != null && StringUtils.isNotEmpty(map.get("priority").toString())) {
+            tenant.setPriority(Integer.valueOf(map.get("priority").toString()));
         }
-        Object selectable = body.get("selectable");
+        if (map.get("isPrimary") != null && StringUtils.isNotEmpty(map.get("isPrimary").toString())) {
+            tenant.setIsPrimary(Integer.valueOf(map.get("isPrimary").toString()));
+        }
+        Object selectable = map.get("selectable");
         if (selectable == null) {
-            selectable = body.get("allowManual");
+            selectable = map.get("allowManual");
         }
         if (selectable != null && StringUtils.isNotEmpty(selectable.toString())) {
-            pricing.setSelectable(selectable.toString());
+            tenant.setSelectable(selectable.toString());
         }
-        return pricing;
     }
 
     @SuppressWarnings("unchecked")

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/tenant/TenantCompanyVoiceApi.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain.tenant;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 租户库 company_voice_api 表实体(仅用于租户数据源读写,非总后台主库)
+ */
+@Data
+public class TenantCompanyVoiceApi {
+
+    private Long apiId;
+
+    private String apiName;
+
+    /** 租户库为 varchar,与主库 int 类型区分存储 */
+    private String apiType;
+
+    private String apiJson;
+
+    private Integer status;
+
+    private String remark;
+
+    private Integer priority;
+
+    private Integer selectable;
+
+    private BigDecimal salePrice;
+
+    private Integer isPrimary;
+
+    private String apiUrl;
+
+    private String dialogUrl;
+
+    private Integer isDel;
+
+    private Date createTime;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/tenant/TenantCompanyVoiceApiMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper.tenant;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 租户库 company_voice_api 表 Mapper(仅在切换租户数据源后调用)
+ */
+public interface TenantCompanyVoiceApiMapper {
+
+    TenantCompanyVoiceApi selectByApiId(@Param("apiId") Long apiId);
+
+    List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query);
+
+    int upsertTenantVoiceApi(TenantCompanyVoiceApi row);
+
+    int updateStatusByApiId(@Param("apiId") Long apiId, @Param("status") Integer status);
+
+    int markDeletedByApiId(@Param("apiId") Long apiId);
+}

+ 39 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiServiceImpl.java

@@ -5,6 +5,9 @@ import com.fs.company.domain.CompanyVoiceApi;
 import com.fs.company.mapper.CompanyVoiceApiMapper;
 import com.fs.company.service.ICompanyVoiceApiService;
 import com.fs.company.service.ICompanyVoiceApiTenantService;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiSyncService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -20,12 +23,17 @@ import java.util.List;
 @Service
 public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService 
 {
+    private static final Logger log = LoggerFactory.getLogger(CompanyVoiceApiServiceImpl.class);
+
     @Autowired
     private CompanyVoiceApiMapper companyVoiceApiMapper;
 
     @Autowired
     private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
 
+    @Autowired
+    private ITenantCompanyVoiceApiSyncService tenantCompanyVoiceApiSyncService;
+
     /**
      * 查询呼叫接口
      * 
@@ -73,7 +81,9 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
     public int updateCompanyVoiceApi(CompanyVoiceApi companyVoiceApi)
     {
         prepareForSave(companyVoiceApi);
-        return companyVoiceApiMapper.updateCompanyVoiceApi(companyVoiceApi);
+        int rows = companyVoiceApiMapper.updateCompanyVoiceApi(companyVoiceApi);
+        syncMasterApiToAllTenantDbsQuietly(companyVoiceApi.getApiId());
+        return rows;
     }
 
     /** 保存前补全默认值(数据写入 account/password/api_url/dialog_url 等新字段,不使用 apiJson) */
@@ -105,7 +115,15 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
             return 0;
         }
         companyVoiceApiTenantService.disableTenantsByApiIds(apiIds);
-        return companyVoiceApiMapper.deleteCompanyVoiceApiByIds(apiIds);
+        int rows = companyVoiceApiMapper.deleteCompanyVoiceApiByIds(apiIds);
+        if (apiIds != null)
+        {
+            for (Long apiId : apiIds)
+            {
+                syncMasterApiToAllTenantDbsQuietly(apiId);
+            }
+        }
+        return rows;
     }
 
     /**
@@ -123,11 +141,29 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
             return 0;
         }
         companyVoiceApiTenantService.disableTenantsByApiId(apiId);
-        return companyVoiceApiMapper.deleteCompanyVoiceApiById(apiId);
+        int rows = companyVoiceApiMapper.deleteCompanyVoiceApiById(apiId);
+        syncMasterApiToAllTenantDbsQuietly(apiId);
+        return rows;
     }
 
     @Override
     public Integer selectCompanyVoiceApiCount() {
         return companyVoiceApiMapper.selectCompanyVoiceApiCount();
     }
+
+    private void syncMasterApiToAllTenantDbsQuietly(Long apiId)
+    {
+        if (tenantCompanyVoiceApiSyncService == null || apiId == null)
+        {
+            return;
+        }
+        try
+        {
+            tenantCompanyVoiceApiSyncService.syncMasterApiToAllTenantDbs(apiId);
+        }
+        catch (Exception e)
+        {
+            log.warn("[TenantVoiceApiSync] 主库接口变更同步租户库失败 apiId={}", apiId, e);
+        }
+    }
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 218
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java


+ 15 - 0
fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiQueryService.java

@@ -0,0 +1,15 @@
+package com.fs.company.service.tenant;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+
+import java.util.List;
+
+/**
+ * 租户库 company_voice_api 查询(租户总后台列表/详情)
+ */
+public interface ITenantCompanyVoiceApiQueryService {
+
+    List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query);
+
+    TenantCompanyVoiceApi selectTenantCompanyVoiceApiById(Long apiId);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiSyncService.java

@@ -0,0 +1,29 @@
+package com.fs.company.service.tenant;
+
+import java.util.List;
+
+/**
+ * 总后台主库 → 租户库 company_voice_api 同步服务(仅操作租户数据源,非主库 CRUD)
+ */
+public interface ITenantCompanyVoiceApiSyncService {
+
+    /**
+     * 将主库接口定义 + 租户绑定关系同步到指定租户库
+     */
+    void syncBindingToTenantDb(Long tenantId, Long apiId);
+
+    /**
+     * 主库接口变更后,同步到所有已绑定该接口的租户库
+     */
+    void syncMasterApiToAllTenantDbs(Long apiId);
+
+    /**
+     * 解除绑定时,将租户库对应接口状态置为禁用
+     */
+    void disableInTenantDb(Long tenantId, Long apiId);
+
+    /**
+     * 批量同步绑定(定价/状态批量变更后)
+     */
+    void syncBindingsToTenantDb(List<Long> bindingIds);
+}

+ 26 - 0
fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiQueryServiceImpl.java

@@ -0,0 +1,26 @@
+package com.fs.company.service.tenant.impl;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import com.fs.company.mapper.tenant.TenantCompanyVoiceApiMapper;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiQueryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class TenantCompanyVoiceApiQueryServiceImpl implements ITenantCompanyVoiceApiQueryService {
+
+    @Autowired
+    private TenantCompanyVoiceApiMapper tenantCompanyVoiceApiMapper;
+
+    @Override
+    public List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query) {
+        return tenantCompanyVoiceApiMapper.selectTenantCompanyVoiceApiList(query);
+    }
+
+    @Override
+    public TenantCompanyVoiceApi selectTenantCompanyVoiceApiById(Long apiId) {
+        return tenantCompanyVoiceApiMapper.selectByApiId(apiId);
+    }
+}

+ 222 - 0
fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiSyncServiceImpl.java

@@ -0,0 +1,222 @@
+package com.fs.company.service.tenant.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.call.config.CallApiConfig;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import com.fs.company.mapper.CompanyVoiceApiMapper;
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
+import com.fs.company.mapper.tenant.TenantCompanyVoiceApiMapper;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiSyncService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 总后台主库外呼接口/绑定同步至租户库 company_voice_api。
+ * 内部通过 TenantDataSourceManager.ensureSwitchByTenantId 切换租户数据源。
+ */
+@Service
+public class TenantCompanyVoiceApiSyncServiceImpl implements ITenantCompanyVoiceApiSyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(TenantCompanyVoiceApiSyncServiceImpl.class);
+
+    @Autowired
+    private CompanyVoiceApiMapper companyVoiceApiMapper;
+
+    @Autowired
+    private CompanyVoiceApiTenantMapper companyVoiceApiTenantMapper;
+
+    @Autowired
+    private TenantCompanyVoiceApiMapper tenantCompanyVoiceApiMapper;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Override
+    public void syncBindingToTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        ensureMasterDataSource();
+        CompanyVoiceApi masterApi = companyVoiceApiMapper.selectCompanyVoiceApiById(apiId);
+        CompanyVoiceApiTenant binding = companyVoiceApiTenantMapper.selectByApiAndTenant(apiId, tenantId);
+        if (masterApi == null) {
+            markDeletedInTenantDb(tenantId, apiId);
+            return;
+        }
+        TenantCompanyVoiceApi tenantRow = buildTenantRow(masterApi, binding);
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.upsertTenantVoiceApi(tenantRow));
+    }
+
+    @Override
+    public void syncMasterApiToAllTenantDbs(Long apiId) {
+        if (apiId == null) {
+            return;
+        }
+        ensureMasterDataSource();
+        CompanyVoiceApi masterApi = companyVoiceApiMapper.selectCompanyVoiceApiById(apiId);
+        List<CompanyVoiceApiTenant> bindings = companyVoiceApiTenantMapper.selectTenantsByApiId(apiId);
+        if (bindings == null || bindings.isEmpty()) {
+            return;
+        }
+        if (masterApi == null) {
+            for (CompanyVoiceApiTenant binding : bindings) {
+                if (binding == null || binding.getTenantId() == null) {
+                    continue;
+                }
+                try {
+                    markDeletedInTenantDb(binding.getTenantId(), apiId);
+                } catch (Exception e) {
+                    log.error("[TenantVoiceApiSync] 主库接口已删除,同步禁用租户库失败 apiId={}, tenantId={}",
+                            apiId, binding.getTenantId(), e);
+                }
+            }
+            return;
+        }
+        for (CompanyVoiceApiTenant binding : bindings) {
+            if (binding == null || binding.getTenantId() == null) {
+                continue;
+            }
+            try {
+                TenantCompanyVoiceApi tenantRow = buildTenantRow(masterApi, binding);
+                runInTenantDb(binding.getTenantId(),
+                        () -> tenantCompanyVoiceApiMapper.upsertTenantVoiceApi(tenantRow));
+            } catch (Exception e) {
+                log.error("[TenantVoiceApiSync] 同步主库接口到租户库失败 apiId={}, tenantId={}",
+                        apiId, binding.getTenantId(), e);
+            }
+        }
+    }
+
+    @Override
+    public void disableInTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.updateStatusByApiId(apiId, 0));
+    }
+
+    /** 主库接口已删除时,租户库标记为删除(is_del=1) */
+    private void markDeletedInTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.markDeletedByApiId(apiId));
+    }
+
+    @Override
+    public void syncBindingsToTenantDb(List<Long> bindingIds) {
+        if (bindingIds == null || bindingIds.isEmpty()) {
+            return;
+        }
+        ensureMasterDataSource();
+        Set<String> synced = new HashSet<>();
+        for (Long bindingId : bindingIds) {
+            if (bindingId == null) {
+                continue;
+            }
+            CompanyVoiceApiTenant binding = companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantById(bindingId);
+            if (binding == null || binding.getTenantId() == null || binding.getApiId() == null) {
+                continue;
+            }
+            String key = binding.getTenantId() + ":" + binding.getApiId();
+            if (!synced.add(key)) {
+                continue;
+            }
+            try {
+                syncBindingToTenantDb(binding.getTenantId(), binding.getApiId());
+            } catch (Exception e) {
+                log.error("[TenantVoiceApiSync] 批量同步绑定失败 bindingId={}", bindingId, e);
+            }
+        }
+    }
+
+    private TenantCompanyVoiceApi buildTenantRow(CompanyVoiceApi masterApi, CompanyVoiceApiTenant binding) {
+        TenantCompanyVoiceApi row = new TenantCompanyVoiceApi();
+        row.setApiId(masterApi.getApiId());
+        row.setApiName(masterApi.getApiName());
+        row.setApiType(masterApi.getApiType() != null ? String.valueOf(masterApi.getApiType()) : null);
+        row.setApiJson(buildApiJson(masterApi));
+        row.setApiUrl(masterApi.getApiUrl());
+        row.setDialogUrl(masterApi.getDialogUrl());
+        row.setRemark(masterApi.getRemark());
+        row.setIsDel(resolveIsDel(masterApi));
+        row.setStatus(resolveEffectiveStatus(masterApi, binding));
+        if (binding != null) {
+            row.setSalePrice(binding.getSalePrice());
+            row.setPriority(binding.getPriority() != null ? binding.getPriority() : 1);
+            row.setIsPrimary(binding.getIsPrimary() != null ? binding.getIsPrimary() : 0);
+            row.setSelectable(parseSelectable(binding.getSelectable()));
+        } else {
+            row.setPriority(1);
+            row.setIsPrimary(0);
+            row.setSelectable(0);
+        }
+        return row;
+    }
+
+    private int resolveIsDel(CompanyVoiceApi masterApi) {
+        if (masterApi == null || masterApi.getIsDel() == null) {
+            return 0;
+        }
+        return masterApi.getIsDel() == 1 ? 1 : 0;
+    }
+
+    private int resolveEffectiveStatus(CompanyVoiceApi masterApi, CompanyVoiceApiTenant binding) {
+        int masterStatus = masterApi.getStatus() != null && masterApi.getStatus() == 1 ? 1 : 0;
+        if (binding == null) {
+            return 0;
+        }
+        int bindingStatus = binding.getStatus() != null && binding.getStatus() == 1 ? 1 : 0;
+        return masterStatus == 1 && bindingStatus == 1 ? 1 : 0;
+    }
+
+    private String buildApiJson(CompanyVoiceApi masterApi) {
+        if (StringUtils.isNotEmpty(masterApi.getApiJson())) {
+            return masterApi.getApiJson();
+        }
+        if (StringUtils.isEmpty(masterApi.getAccount())
+                && StringUtils.isEmpty(masterApi.getPassword())
+                && StringUtils.isEmpty(masterApi.getApiUrl())) {
+            return null;
+        }
+        CallApiConfig config = new CallApiConfig();
+        config.setAccount(masterApi.getAccount());
+        config.setPassword(masterApi.getPassword());
+        config.setUrl(masterApi.getApiUrl());
+        return JSONUtil.toJsonStr(config);
+    }
+
+    private int parseSelectable(String selectable) {
+        if (selectable == null) {
+            return 0;
+        }
+        return "1".equals(selectable) || "true".equalsIgnoreCase(selectable) ? 1 : 0;
+    }
+
+    private void runInTenantDb(Long tenantId, Runnable action) {
+        ensureMasterDataSource();
+        try {
+            tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+            action.run();
+        } finally {
+            tenantDataSourceManager.clear();
+            ensureMasterDataSource();
+        }
+    }
+
+    private void ensureMasterDataSource() {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+    }
+}

+ 1 - 1
fs-service/src/main/java/com/fs/proxy/domain/ServiceFeeConfig.java

@@ -24,7 +24,7 @@ public class ServiceFeeConfig extends BaseEntity
     @Excel(name = "配置名称")
     private String configName;
 
-    /** 服务类型:1-AI模型训练 2-课程流量费 3-直播流量费 4-AI TOKEN 5-短信发送 6-手拨外呼 7-AI智能外呼 8-微助手 9-账户费 */
+    /** 服务类型:1-AI模型训练 2-课程流量费 3-直播流量费 4-AI TOKEN 5-短信发送 6-手拨外呼 7-AI智能外呼 8-微助手 9-账户费 10-线路外呼费用 */
     @Excel(name = "服务类型")
     private Integer serviceType;
 

+ 14 - 6
fs-service/src/main/resources/db/tenant-initTable.sql

@@ -2418,12 +2418,20 @@ CREATE TABLE `company_voice`
 DROP TABLE IF EXISTS `company_voice_api`;
 CREATE TABLE `company_voice_api`
 (
-    `api_id`   int NOT NULL AUTO_INCREMENT COMMENT 'ID',
-    `api_name` varchar(100)   NULL DEFAULT NULL COMMENT 'API接口名',
-    `api_type` varchar(50)   NULL DEFAULT NULL COMMENT 'KEY',
-    `api_json` varchar(2000)   NULL DEFAULT NULL COMMENT '接口地址',
-    `status`   tinyint(1) NULL DEFAULT NULL COMMENT '状态',
-    `remark`   varchar(2000)   NULL DEFAULT NULL,
+    `api_id`      int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `api_name`    varchar(100)   NULL DEFAULT NULL COMMENT 'API接口名',
+    `api_type`    varchar(50)   NULL DEFAULT NULL COMMENT 'KEY',
+    `api_json`    varchar(2000)   NULL DEFAULT NULL COMMENT '接口配置JSON',
+    `status`      tinyint(1) NULL DEFAULT NULL COMMENT '状态',
+    `remark`      varchar(2000)   NULL DEFAULT NULL,
+    `priority`    int NULL DEFAULT 1 COMMENT '优先级(数字越小越靠前)',
+    `selectable`  tinyint NULL DEFAULT 0 COMMENT '允许手动选择(0否1是)',
+    `sale_price`  decimal(10, 4) NULL DEFAULT NULL COMMENT '租户售价(元/分钟)',
+    `is_primary`  tinyint NULL DEFAULT 0 COMMENT '是否主线路(0否1是)',
+    `api_url`     varchar(500)   NULL DEFAULT NULL COMMENT '接口地址',
+    `dialog_url`  varchar(500)   NULL DEFAULT NULL COMMENT '话术跳转地址',
+    `is_del`      tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0否1是)',
+    `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
     PRIMARY KEY (`api_id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '呼叫接口' ROW_FORMAT = DYNAMIC;
 

+ 74 - 0
fs-service/src/main/resources/mapper/company/tenant/TenantCompanyVoiceApiMapper.xml

@@ -0,0 +1,74 @@
+<?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.tenant.TenantCompanyVoiceApiMapper">
+
+    <resultMap type="com.fs.company.domain.tenant.TenantCompanyVoiceApi" id="TenantCompanyVoiceApiResult">
+        <result property="apiId"       column="api_id"      />
+        <result property="apiName"     column="api_name"    />
+        <result property="apiType"     column="api_type"    />
+        <result property="apiJson"     column="api_json"    />
+        <result property="status"      column="status"      />
+        <result property="remark"      column="remark"      />
+        <result property="priority"    column="priority"    />
+        <result property="selectable"  column="selectable"  />
+        <result property="salePrice"   column="sale_price"  />
+        <result property="isPrimary"   column="is_primary"  />
+        <result property="apiUrl"      column="api_url"     />
+        <result property="dialogUrl"   column="dialog_url"  />
+        <result property="isDel"       column="is_del"      />
+        <result property="createTime"  column="create_time" />
+    </resultMap>
+
+    <select id="selectByApiId" resultMap="TenantCompanyVoiceApiResult">
+        select api_id, api_name, api_type, api_json, status, remark,
+               priority, selectable, sale_price, is_primary, api_url, dialog_url, is_del, create_time
+        from company_voice_api
+        where api_id = #{apiId}
+    </select>
+
+    <select id="selectTenantCompanyVoiceApiList" resultMap="TenantCompanyVoiceApiResult">
+        select api_id, api_name, api_type, api_json, status, remark,
+               priority, selectable, sale_price, is_primary, api_url, dialog_url, is_del, create_time
+        from company_voice_api
+        <where>
+            and (is_del = 0 or is_del is null)
+            <if test="apiName != null and apiName != ''"> and api_name like concat('%', #{apiName}, '%')</if>
+            <if test="apiType != null and apiType != ''"> and api_type = #{apiType}</if>
+            <if test="status != null"> and status = #{status}</if>
+        </where>
+        order by priority asc, api_id desc
+    </select>
+
+    <insert id="upsertTenantVoiceApi">
+        insert into company_voice_api
+            (api_id, api_name, api_type, api_json, status, remark, priority, selectable,
+             sale_price, is_primary, api_url, dialog_url, is_del, create_time)
+        values
+            (#{apiId}, #{apiName}, #{apiType}, #{apiJson}, #{status}, #{remark}, #{priority}, #{selectable},
+             #{salePrice}, #{isPrimary}, #{apiUrl}, #{dialogUrl}, #{isDel}, NOW())
+        on duplicate key update
+            api_name = values(api_name),
+            api_type = values(api_type),
+            api_json = values(api_json),
+            status = values(status),
+            remark = values(remark),
+            priority = values(priority),
+            selectable = values(selectable),
+            sale_price = values(sale_price),
+            is_primary = values(is_primary),
+            api_url = values(api_url),
+            dialog_url = values(dialog_url),
+            is_del = values(is_del)
+    </insert>
+
+    <update id="updateStatusByApiId">
+        update company_voice_api set status = #{status} where api_id = #{apiId}
+    </update>
+
+    <update id="markDeletedByApiId">
+        update company_voice_api set status = 0, is_del = 1 where api_id = #{apiId}
+    </update>
+
+</mapper>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio