boss 2 недель назад
Родитель
Сommit
970acb4ed5

+ 54 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java

@@ -30,6 +30,8 @@ public class AdminCompanyBridgeController extends BaseController {
     @Autowired(required = false)
     private ICompanyVoiceApiService companyVoiceApiService;
     @Autowired(required = false)
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+    @Autowired(required = false)
     private ICompanyVoicePackageOrderService companyVoicePackageOrderService;
     @Autowired(required = false)
     private ICompanySmsService companySmsService;
@@ -70,6 +72,58 @@ public class AdminCompanyBridgeController extends BaseController {
         return getDataTable(list);
     }
 
+    // ========== 通话接口-租户分配 ==========
+    /** 查询接口已分配的租户列表 */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
+    @GetMapping("/admin/voice-api/tenants/{apiId}")
+    public AjaxResult voiceApiTenants(@PathVariable Long apiId) {
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null ?
+            companyVoiceApiTenantService.selectTenantsByApiId(apiId) : new ArrayList<>();
+        return AjaxResult.success(list);
+    }
+
+    /** 查询租户已分配的接口列表 */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
+    @GetMapping("/admin/voice-api/apis/{companyId}")
+    public AjaxResult voiceApiApis(@PathVariable Long companyId) {
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null ?
+            companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId) : new ArrayList<>();
+        return AjaxResult.success(list);
+    }
+
+    /** 分配接口给租户(支持批量) */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "分配通话接口给租户", businessType = BusinessType.INSERT)
+    @PostMapping("/admin/voice-api/assignTenants")
+    public AjaxResult assignTenants(@RequestBody Map<String, Object> body) {
+        if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
+        Long apiId = Long.valueOf(body.get("apiId").toString());
+        @SuppressWarnings("unchecked")
+        List<Integer> companyIds = (List<Integer>) body.get("companyIds");
+        List<Long> ids = new ArrayList<>();
+        for (Integer id : companyIds) { ids.add(id.longValue()); }
+        companyVoiceApiTenantService.batchAssignTenants(apiId, ids);
+        return AjaxResult.success();
+    }
+
+    /** 取消分配 */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
+    @DeleteMapping("/admin/voice-api/unassignTenant")
+    public AjaxResult unassignTenant(@RequestParam Long apiId, @RequestParam Long companyId) {
+        if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
+        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, companyId));
+    }
+
+    /** 查询接口已分配租户数量 */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
+    @GetMapping("/admin/voice-api/tenantCount/{apiId}")
+    public AjaxResult voiceApiTenantCount(@PathVariable Long apiId) {
+        Integer count = companyVoiceApiTenantService != null ?
+            companyVoiceApiTenantService.selectTenantCountByApiId(apiId) : 0;
+        return AjaxResult.success("ok", count);
+    }
+
     // ========== 坐席管理 /company/companyVoiceCaller ==========
     @PreAuthorize("@ss.hasPermi('company:companyVoiceCaller:list')")
     @GetMapping("/company/companyVoiceCaller/list")

+ 163 - 0
fs-company/src/main/java/com/fs/company/AddSmsApiMenu.java

@@ -0,0 +1,163 @@
+package com.fs.company;
+
+import java.sql.*;
+import java.io.*;
+
+public class AddSmsApiMenu {
+    public static void main(String[] args) {
+        String url = "jdbc:mysql://cq-cdb-8fjmemkb.sql.tencentcdb.com:27220/ylrz_saas?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false";
+        String user = "root";
+        String password = "Ylrz_1q2w3e4r5t6y";
+        
+        String outFile = "d:/AICODE/saas/add_sms_api_menu_result.txt";
+        PrintWriter pw = null;
+        try {
+            pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(outFile), "UTF-8"));
+        } catch (Exception e) {
+            System.err.println("无法创建输出文件: " + e.getMessage());
+            return;
+        }
+        
+        try {
+            Class.forName("com.mysql.cj.jdbc.Driver");
+            pw.println("DEBUG: Driver loaded");
+            pw.flush();
+            
+            Connection conn = DriverManager.getConnection(url, user, password);
+            pw.println("DEBUG: Connected to database");
+            pw.flush();
+            
+            // 1. 查找通信管理分组
+            String findGroupSql = "SELECT menu_id, menu_name, parent_id, order_num, path, component FROM sys_menu WHERE menu_name = '通信管理' AND menu_type = 'M' AND status = '0'";
+            Statement stmt = conn.createStatement();
+            ResultSet rs = stmt.executeQuery(findGroupSql);
+            
+            long groupId = -1;
+            String groupName = "";
+            pw.println("\n=== 通信管理分组 ===");
+            while (rs.next()) {
+                groupId = rs.getLong("menu_id");
+                groupName = rs.getString("menu_name");
+                pw.println("menu_id=" + groupId + ", menu_name=" + groupName + ", parent_id=" + rs.getLong("parent_id"));
+            }
+            rs.close();
+            stmt.close();
+            
+            if (groupId == -1) {
+                pw.println("ERROR: 找不到通信管理分组!");
+                conn.close();
+                pw.close();
+                return;
+            }
+            
+            // 2. 查看通信管理下的现有子菜单
+            String findChildrenSql = "SELECT menu_id, menu_name, order_num, path, component FROM sys_menu WHERE parent_id = " + groupId + " AND menu_type IN ('M','C') AND status = '0' ORDER BY order_num";
+            Statement stmt2 = conn.createStatement();
+            ResultSet rs2 = stmt2.executeQuery(findChildrenSql);
+            
+            pw.println("\n=== 通信管理下的子菜单 ===");
+            int maxOrderNum = 0;
+            long maxMenuId = 0;
+            boolean smsApiExists = false;
+            while (rs2.next()) {
+                long mid = rs2.getLong("menu_id");
+                String mname = rs2.getString("menu_name");
+                int order = rs2.getInt("order_num");
+                String mpath = rs2.getString("path");
+                pw.println("menu_id=" + mid + ", menu_name=" + mname + ", order_num=" + order + ", path=" + mpath);
+                if (order > maxOrderNum) maxOrderNum = order;
+                if (mid > maxMenuId) maxMenuId = mid;
+                if ("smsApi".equals(mpath) || "短信接口".equals(mname)) smsApiExists = true;
+            }
+            rs2.close();
+            stmt2.close();
+            
+            if (smsApiExists) {
+                pw.println("\n短信接口菜单已存在,跳过插入");
+                conn.close();
+                pw.close();
+                return;
+            }
+            
+            // 3. 插入短信接口菜单
+            // 找一个不冲突的menu_id:在现有最大menu_id基础上+1
+            String findMaxIdSql = "SELECT MAX(menu_id) as max_id FROM sys_menu";
+            Statement stmt3 = conn.createStatement();
+            ResultSet rs3 = stmt3.executeQuery(findMaxIdSql);
+            long newMenuId = 28261; // 默认值
+            if (rs3.next()) {
+                long dbMax = rs3.getLong("max_id");
+                newMenuId = Math.max(dbMax + 1, 28261);
+            }
+            rs3.close();
+            stmt3.close();
+            
+            // 短信相关菜单排在短信管理之后,短信套餐之前
+            // 找短信管理的order_num
+            String findSmsOrderSql = "SELECT order_num FROM sys_menu WHERE parent_id = " + groupId + " AND path = 'sms'";
+            Statement stmt4 = conn.createStatement();
+            ResultSet rs4 = stmt4.executeQuery(findSmsOrderSql);
+            int smsOrder = 8; // 默认
+            if (rs4.next()) {
+                smsOrder = rs4.getInt("order_num");
+            }
+            rs4.close();
+            stmt4.close();
+            
+            // 将短信套餐和之后的菜单order_num后移
+            String shiftOrdersSql = "UPDATE sys_menu SET order_num = order_num + 1 WHERE parent_id = " + groupId + " AND order_num > " + smsOrder;
+            Statement stmt5 = conn.createStatement();
+            int shiftCount = stmt5.executeUpdate(shiftOrdersSql);
+            pw.println("\n后移了 " + shiftCount + " 个菜单的order_num");
+            stmt5.close();
+            
+            // 插入短信接口菜单
+            String insertSql = "INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, perms, is_frame, is_cache, create_by, create_time) " +
+                "VALUES (" + newMenuId + ", '短信接口', " + groupId + ", " + (smsOrder + 1) + ", 'smsApi', 'admin/smsApi/index', 'C', 'el-icon-connection', '0', '0', '', 0, 0, 'admin', NOW())";
+            Statement stmt6 = conn.createStatement();
+            int insertCount = stmt6.executeUpdate(insertSql);
+            pw.println("\n插入短信接口菜单: menu_id=" + newMenuId + ", 影响" + insertCount + "行");
+            stmt6.close();
+            
+            // 给超级管理员角色授权 (role_id=1)
+            String insertRoleMenuSql = "INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, " + newMenuId + ")";
+            Statement stmt7 = conn.createStatement();
+            int rmCount = stmt7.executeUpdate(insertRoleMenuSql);
+            pw.println("插入角色菜单关联: role_id=1, menu_id=" + newMenuId + ", 影响" + rmCount + "行");
+            stmt7.close();
+            
+            // 4. 验证插入结果
+            String verifySql = "SELECT menu_id, menu_name, parent_id, order_num, path, component FROM sys_menu WHERE menu_id = " + newMenuId;
+            Statement stmt8 = conn.createStatement();
+            ResultSet rs8 = stmt8.executeQuery(verifySql);
+            pw.println("\n=== 验证插入结果 ===");
+            while (rs8.next()) {
+                pw.println("menu_id=" + rs8.getLong("menu_id") + ", menu_name=" + rs8.getString("menu_name") + 
+                    ", parent_id=" + rs8.getLong("parent_id") + ", order_num=" + rs8.getInt("order_num") + 
+                    ", path=" + rs8.getString("path") + ", component=" + rs8.getString("component"));
+            }
+            rs8.close();
+            stmt8.close();
+            
+            // 5. 显示更新后的通信管理下子菜单
+            String verifyChildrenSql = "SELECT menu_id, menu_name, order_num, path, component FROM sys_menu WHERE parent_id = " + groupId + " AND menu_type IN ('M','C') AND status = '0' ORDER BY order_num";
+            Statement stmt9 = conn.createStatement();
+            ResultSet rs9 = stmt9.executeQuery(verifyChildrenSql);
+            pw.println("\n=== 更新后通信管理下的子菜单 ===");
+            while (rs9.next()) {
+                pw.println("menu_id=" + rs9.getLong("menu_id") + ", menu_name=" + rs9.getString("menu_name") + 
+                    ", order_num=" + rs9.getInt("order_num") + ", path=" + rs9.getString("path"));
+            }
+            rs9.close();
+            stmt9.close();
+            
+            conn.close();
+            pw.println("\nDONE!");
+        } catch (Exception e) {
+            pw.println("ERROR: " + e.getMessage());
+            e.printStackTrace(pw);
+        }
+        
+        pw.close();
+    }
+}

+ 16 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceApiController.java

@@ -2,11 +2,14 @@ package com.fs.company.controller.company;
 
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.controller.BaseController;
+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.ServletUtils;
 import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.CompanyVoiceApiTenant;
 import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.ICompanyVoiceApiTenantService;
 import com.fs.company.service.ICompanyVoiceMobileService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -33,6 +36,8 @@ public class CompanyVoiceApiController extends BaseController {
     @Autowired
     private ICompanyVoiceApiService companyVoiceApiService;
     @Autowired
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+    @Autowired
     private TokenService tokenService;
 
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
@@ -75,4 +80,15 @@ public class CompanyVoiceApiController extends BaseController {
         return voiceService.getSipAccount(loginUser.getUser().getUserId());
     }
 
+    @ApiOperation("查询当前租户可用的通话接口列表")
+    @GetMapping(value="/myApis")
+    public AjaxResult myApis()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany() != null ? loginUser.getCompany().getCompanyId() : null;
+        if (companyId == null) { return AjaxResult.error("请选择租户"); }
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId);
+        return AjaxResult.success(list);
+    }
+
 }

+ 97 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApiTenant.java

@@ -0,0 +1,97 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+
+import java.util.Date;
+
+/**
+ * 通话接口-租户分配关系对象 company_voice_api_tenant
+ *
+ * @author fs
+ * @date 2026-05-21
+ */
+public class CompanyVoiceApiTenant extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    private Long id;
+
+    /** 通话接口ID */
+    private Long apiId;
+
+    /** 租户ID */
+    private Long companyId;
+
+    /** 状态 1启用 0禁用 */
+    private Integer status;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 租户名(非表字段,关联查询用) */
+    private String companyName;
+
+    /** 接口名(非表字段,关联查询用) */
+    private String apiName;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getApiId() {
+        return apiId;
+    }
+
+    public void setApiId(Long apiId) {
+        this.apiId = apiId;
+    }
+
+    public Long getCompanyId() {
+        return companyId;
+    }
+
+    public void setCompanyId(Long companyId) {
+        this.companyId = companyId;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    @Override
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    @Override
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    public void setCompanyName(String companyName) {
+        this.companyName = companyName;
+    }
+
+    public String getApiName() {
+        return apiName;
+    }
+
+    public void setApiName(String apiName) {
+        this.apiName = apiName;
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceCaller.java

@@ -30,6 +30,10 @@ public class CompanyVoiceCaller extends BaseEntity
     @Excel(name = "用户ID")
     private Long companyUserId;
 
+    /** 通话接口ID */
+    @Excel(name = "通话接口ID")
+    private Long apiId;
+
     /** 坐席号 */
     @Excel(name = "坐席号")
     private String callerNo;
@@ -73,6 +77,14 @@ public class CompanyVoiceCaller extends BaseEntity
         this.companyUserId = companyUserId;
     }
 
+    public Long getApiId() {
+        return apiId;
+    }
+
+    public void setApiId(Long apiId) {
+        this.apiId = apiId;
+    }
+
     public String getCallerNo() {
         return callerNo;
     }

+ 77 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiTenantMapper.java

@@ -0,0 +1,77 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 通话接口-租户分配关系Mapper接口
+ *
+ * @author fs
+ * @date 2026-05-21
+ */
+public interface CompanyVoiceApiTenantMapper
+{
+    /**
+     * 查询分配关系
+     */
+    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
+
+    /**
+     * 按接口ID+租户ID查询分配关系
+     */
+    public CompanyVoiceApiTenant selectByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
+
+    /**
+     * 查询接口已分配的租户列表(含租户名)
+     */
+    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
+
+    /**
+     * 查询租户已分配的接口列表(含接口名)
+     */
+    public List<CompanyVoiceApiTenant> selectApisByCompanyId(Long companyId);
+
+    /**
+     * 查询分配关系列表
+     */
+    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
+
+    /**
+     * 新增分配关系
+     */
+    public int insertCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    /**
+     * 批量新增分配关系
+     */
+    public int batchInsertCompanyVoiceApiTenant(@Param("list") List<CompanyVoiceApiTenant> list);
+
+    /**
+     * 修改分配关系
+     */
+    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    /**
+     * 删除分配关系
+     */
+    public int deleteCompanyVoiceApiTenantById(Long id);
+
+    /**
+     * 按接口ID+租户ID删除分配关系
+     */
+    public int deleteByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
+
+    /**
+     * 按接口ID删除所有分配关系
+     */
+    public int deleteByApiId(Long apiId);
+
+    /**
+     * 查询接口已分配的租户数量
+     */
+    @Select("SELECT COUNT(1) FROM company_voice_api_tenant WHERE api_id = #{apiId} AND status = 1")
+    Integer selectTenantCountByApiId(Long apiId);
+}

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

@@ -64,8 +64,8 @@ public interface CompanyVoiceCallerMapper
      */
     public int deleteCompanyVoiceCallerByIds(Long[] callingIds);
     @Select({"<script> " +
-            "select vc.*,u.nick_name as company_user_nick_name,c.company_name " +
-            "from company_voice_caller vc left join company c on c.company_id=vc.company_id left join  company_user u on u.user_id=vc.company_user_id " +
+            "select vc.*,u.nick_name as company_user_nick_name,c.company_name,a.api_name " +
+            "from company_voice_caller vc left join company c on c.company_id=vc.company_id left join  company_user u on u.user_id=vc.company_user_id left join company_voice_api a on a.api_id=vc.api_id " +
             "where 1=1 " +
             "<if test = 'maps.companyId != null  '> " +
             "and vc.company_id = #{maps.companyId}" +

+ 64 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiTenantService.java

@@ -0,0 +1,64 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CompanyVoiceApiTenant;
+
+import java.util.List;
+
+/**
+ * 通话接口-租户分配关系Service接口
+ *
+ * @author fs
+ * @date 2026-05-21
+ */
+public interface ICompanyVoiceApiTenantService
+{
+    /**
+     * 查询分配关系
+     */
+    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
+
+    /**
+     * 按接口ID+租户ID查询分配关系
+     */
+    public CompanyVoiceApiTenant selectByApiAndCompany(Long apiId, Long companyId);
+
+    /**
+     * 查询接口已分配的租户列表(含租户名)
+     */
+    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
+
+    /**
+     * 查询租户已分配的接口列表(含接口名,仅启用的)
+     */
+    public List<CompanyVoiceApiTenant> selectEnabledApisByCompanyId(Long companyId);
+
+    /**
+     * 查询分配关系列表
+     */
+    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
+
+    /**
+     * 分配接口给租户
+     */
+    public int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    /**
+     * 批量分配接口给租户
+     */
+    public int batchAssignTenants(Long apiId, List<Long> companyIds);
+
+    /**
+     * 取消分配
+     */
+    public int unassignTenant(Long apiId, Long companyId);
+
+    /**
+     * 修改分配关系状态
+     */
+    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    /**
+     * 查询接口已分配的租户数量(启用状态)
+     */
+    public Integer selectTenantCountByApiId(Long apiId);
+}

+ 125 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java

@@ -0,0 +1,125 @@
+package com.fs.company.service.impl;
+
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
+import com.fs.company.service.ICompanyVoiceApiTenantService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 通话接口-租户分配关系Service实现
+ *
+ * @author fs
+ * @date 2026-05-21
+ */
+@Service
+public class CompanyVoiceApiTenantServiceImpl implements ICompanyVoiceApiTenantService
+{
+    @Autowired
+    private CompanyVoiceApiTenantMapper companyVoiceApiTenantMapper;
+
+    @Override
+    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id)
+    {
+        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantById(id);
+    }
+
+    @Override
+    public CompanyVoiceApiTenant selectByApiAndCompany(Long apiId, Long companyId)
+    {
+        return companyVoiceApiTenantMapper.selectByApiAndCompany(apiId, companyId);
+    }
+
+    @Override
+    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId)
+    {
+        return companyVoiceApiTenantMapper.selectTenantsByApiId(apiId);
+    }
+
+    @Override
+    public List<CompanyVoiceApiTenant> selectEnabledApisByCompanyId(Long companyId)
+    {
+        CompanyVoiceApiTenant param = new CompanyVoiceApiTenant();
+        param.setCompanyId(companyId);
+        param.setStatus(1);
+        return companyVoiceApiTenantMapper.selectApisByCompanyId(companyId);
+    }
+
+    @Override
+    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param)
+    {
+        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantList(param);
+    }
+
+    @Override
+    public int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant)
+    {
+        // 检查是否已存在分配关系
+        CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndCompany(
+                companyVoiceApiTenant.getApiId(), companyVoiceApiTenant.getCompanyId());
+        if (existing != null)
+        {
+            // 已存在则更新状态为启用
+            existing.setStatus(1);
+            return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);
+        }
+        if (companyVoiceApiTenant.getStatus() == null)
+        {
+            companyVoiceApiTenant.setStatus(1);
+        }
+        return companyVoiceApiTenantMapper.insertCompanyVoiceApiTenant(companyVoiceApiTenant);
+    }
+
+    @Override
+    public int batchAssignTenants(Long apiId, List<Long> companyIds)
+    {
+        if (companyIds == null || companyIds.isEmpty())
+        {
+            return 0;
+        }
+        List<CompanyVoiceApiTenant> toInsert = new ArrayList<>();
+        for (Long companyId : companyIds)
+        {
+            CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndCompany(apiId, companyId);
+            if (existing == null)
+            {
+                CompanyVoiceApiTenant rel = new CompanyVoiceApiTenant();
+                rel.setApiId(apiId);
+                rel.setCompanyId(companyId);
+                rel.setStatus(1);
+                toInsert.add(rel);
+            }
+            else if (existing.getStatus() == null || existing.getStatus() == 0)
+            {
+                existing.setStatus(1);
+                companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);
+            }
+        }
+        if (!toInsert.isEmpty())
+        {
+            return companyVoiceApiTenantMapper.batchInsertCompanyVoiceApiTenant(toInsert);
+        }
+        return 0;
+    }
+
+    @Override
+    public int unassignTenant(Long apiId, Long companyId)
+    {
+        return companyVoiceApiTenantMapper.deleteByApiAndCompany(apiId, companyId);
+    }
+
+    @Override
+    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant)
+    {
+        return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(companyVoiceApiTenant);
+    }
+
+    @Override
+    public Integer selectTenantCountByApiId(Long apiId)
+    {
+        return companyVoiceApiTenantMapper.selectTenantCountByApiId(apiId);
+    }
+}

+ 8 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceCallerListVO.java

@@ -26,6 +26,14 @@ public class CompanyVoiceCallerListVO implements Serializable
     @Excel(name = "用户ID")
     private Long companyUserId;
 
+    /** 通话接口ID */
+    @Excel(name = "通话接口ID")
+    private Long apiId;
+
+    /** 通话接口名 */
+    @Excel(name = "接口名")
+    private String apiName;
+
     /** 坐席号 */
     @Excel(name = "坐席号")
     private String callerNo;

+ 37 - 15
fs-service/src/main/java/com/fs/voice/service/impl/VoiceServiceImpl.java

@@ -61,6 +61,8 @@ public class VoiceServiceImpl implements IVoiceService
     @Autowired
     private ICompanyVoiceApiService companyVoiceApiService;
     @Autowired
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+    @Autowired
     private ICompanyService companyService;
 
     @Autowired
@@ -84,22 +86,32 @@ public class VoiceServiceImpl implements IVoiceService
         if(company.getStatus()==0){
             return R.error("公司已停用");
         }
-        if(company.getVoiceApiId()==null){
+        CompanyUser user=userService.selectCompanyUserById(userId);
+        CompanyVoiceCaller caller=callerService.selectCompanyVoiceCallerByUserId(companyId,userId);
+        if(caller==null){
+            return R.error("未绑定坐席");
+        }
+        // 优先从坐席绑定的接口获取,回退到公司默认接口
+        Long apiId = caller.getApiId();
+        if(apiId == null){
+            apiId = company.getVoiceApiId();
+        }
+        if(apiId == null){
             return R.error("未配置外呼接口");
         }
-        CompanyVoiceApi api=companyVoiceApiService.selectCompanyVoiceApiById(company.getVoiceApiId());
+        // 验证接口已分配给该租户
+        CompanyVoiceApiTenant apiTenant = companyVoiceApiTenantService.selectByApiAndCompany(apiId, companyId);
+        if(apiTenant == null || apiTenant.getStatus() == 0){
+            return R.error("该通话接口未分配给当前租户");
+        }
+        CompanyVoiceApi api=companyVoiceApiService.selectCompanyVoiceApiById(apiId);
         if(api==null){
             return R.error("外呼接口不存在");
         }
         if(api.getStatus().equals(0)){
             return R.error("外呼接口已禁用");
         }
-        CompanyUser user=userService.selectCompanyUserById(userId);
-        CompanyVoiceCaller caller=callerService.selectCompanyVoiceCallerByUserId(companyId,userId);
         CompanyVoice companyVoice=companyVoiceService.selectCompanyVoiceByCompanyId(user.getCompanyId());
-        if(caller==null){
-            return R.error("未绑定坐席");
-        }
         if(companyVoice.getTimes()<=0){
             return R.error("剩余时长不足,请购买套餐");
         }
@@ -319,22 +331,32 @@ public class VoiceServiceImpl implements IVoiceService
         if(company.getStatus()==0){
             return R.error("公司已停用");
         }
-        if(company.getVoiceApiId()==null){
+        CompanyUser user=userService.selectCompanyUserById(userId);
+        CompanyVoiceCaller caller=callerService.selectCompanyVoiceCallerByUserId(companyId,userId);
+        CompanyVoice companyVoice=companyVoiceService.selectCompanyVoiceByCompanyId(user.getCompanyId());
+        if(caller==null){
+            return R.error("未绑定坐席");
+        }
+        // 优先从坐席绑定的接口获取,回退到公司默认接口
+        Long apiId = caller.getApiId();
+        if(apiId == null){
+            apiId = company.getVoiceApiId();
+        }
+        if(apiId == null){
             return R.error("未配置外呼接口");
         }
-        CompanyVoiceApi api=companyVoiceApiService.selectCompanyVoiceApiById(company.getVoiceApiId());
+        // 验证接口已分配给该租户
+        CompanyVoiceApiTenant apiTenant = companyVoiceApiTenantService.selectByApiAndCompany(apiId, companyId);
+        if(apiTenant == null || apiTenant.getStatus() == 0){
+            return R.error("该通话接口未分配给当前租户");
+        }
+        CompanyVoiceApi api=companyVoiceApiService.selectCompanyVoiceApiById(apiId);
         if(api==null){
             return R.error("外呼接口不存在");
         }
         if(api.getStatus().equals(0)){
             return R.error("外呼接口已禁用");
         }
-        CompanyUser user=userService.selectCompanyUserById(userId);
-        CompanyVoiceCaller caller=callerService.selectCompanyVoiceCallerByUserId(companyId,userId);
-        CompanyVoice companyVoice=companyVoiceService.selectCompanyVoiceByCompanyId(user.getCompanyId());
-        if(caller==null){
-            return R.error("未绑定坐席");
-        }
         if(companyVoice.getTimes()<=0){
             return R.error("剩余时长不足,请购买套餐");
         }

+ 108 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceApiTenantMapper.xml

@@ -0,0 +1,108 @@
+<?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.CompanyVoiceApiTenantMapper">
+
+    <resultMap type="CompanyVoiceApiTenant" id="CompanyVoiceApiTenantResult">
+        <result property="id"         column="id"          />
+        <result property="apiId"      column="api_id"      />
+        <result property="companyId"  column="company_id"  />
+        <result property="status"     column="status"      />
+        <result property="createTime" column="create_time" />
+        <result property="companyName" column="company_name" />
+        <result property="apiName"    column="api_name"    />
+    </resultMap>
+
+    <sql id="selectCompanyVoiceApiTenantVo">
+        select t.id, t.api_id, t.company_id, t.status, t.create_time
+        from company_voice_api_tenant t
+    </sql>
+
+    <select id="selectCompanyVoiceApiTenantById" resultMap="CompanyVoiceApiTenantResult">
+        <include refid="selectCompanyVoiceApiTenantVo"/>
+        where t.id = #{id}
+    </select>
+
+    <select id="selectByApiAndCompany" resultMap="CompanyVoiceApiTenantResult">
+        <include refid="selectCompanyVoiceApiTenantVo"/>
+        where t.api_id = #{apiId} and t.company_id = #{companyId}
+    </select>
+
+    <!-- 查询接口已分配的租户列表(含租户名) -->
+    <select id="selectTenantsByApiId" resultMap="CompanyVoiceApiTenantResult">
+        select t.id, t.api_id, t.company_id, t.status, t.create_time,
+               c.company_name
+        from company_voice_api_tenant t
+        left join company c on c.company_id = t.company_id
+        where t.api_id = #{apiId}
+        order by t.id desc
+    </select>
+
+    <!-- 查询租户已分配的接口列表(含接口名) -->
+    <select id="selectApisByCompanyId" resultMap="CompanyVoiceApiTenantResult">
+        select t.id, t.api_id, t.company_id, t.status, t.create_time,
+               a.api_name
+        from company_voice_api_tenant t
+        left join company_voice_api a on a.api_id = t.api_id
+        where t.company_id = #{companyId}
+        order by t.id desc
+    </select>
+
+    <select id="selectCompanyVoiceApiTenantList" resultMap="CompanyVoiceApiTenantResult">
+        <include refid="selectCompanyVoiceApiTenantVo"/>
+        <where>
+            <if test="apiId != null"> and t.api_id = #{apiId}</if>
+            <if test="companyId != null"> and t.company_id = #{companyId}</if>
+            <if test="status != null"> and t.status = #{status}</if>
+        </where>
+        order by t.id desc
+    </select>
+
+    <insert id="insertCompanyVoiceApiTenant" useGeneratedKeys="true" keyProperty="id">
+        insert into company_voice_api_tenant
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="apiId != null">api_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="apiId != null">#{apiId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+    </insert>
+
+    <insert id="batchInsertCompanyVoiceApiTenant">
+        insert into company_voice_api_tenant (api_id, company_id, status, create_time)
+        values
+        <foreach item="item" collection="list" separator=",">
+            (#{item.apiId}, #{item.companyId}, #{item.status}, NOW())
+        </foreach>
+    </insert>
+
+    <update id="updateCompanyVoiceApiTenant">
+        update company_voice_api_tenant
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="apiId != null">api_id = #{apiId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanyVoiceApiTenantById">
+        delete from company_voice_api_tenant where id = #{id}
+    </delete>
+
+    <delete id="deleteByApiAndCompany">
+        delete from company_voice_api_tenant where api_id = #{apiId} and company_id = #{companyId}
+    </delete>
+
+    <delete id="deleteByApiId">
+        delete from company_voice_api_tenant where api_id = #{apiId}
+    </delete>
+
+</mapper>

+ 5 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceCallerMapper.xml

@@ -8,6 +8,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="callerId"    column="caller_id"    />
         <result property="companyId"    column="company_id"    />
         <result property="companyUserId"    column="company_user_id"    />
+        <result property="apiId"    column="api_id"    />
         <result property="callerNo"    column="caller_no"    />
         <result property="mobile"    column="mobile"    />
         <result property="status"    column="status"    />
@@ -16,7 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectCompanyVoiceCallerVo">
-        select caller_id, company_id, company_user_id, caller_no, mobile, status, remark,bind_time from company_voice_caller
+        select caller_id, company_id, company_user_id, api_id, caller_no, mobile, status, remark,bind_time from company_voice_caller
     </sql>
 
     <select id="selectCompanyVoiceCallerList" resultMap="CompanyVoiceCallerResult">
@@ -40,6 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="companyId != null">company_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
+            <if test="apiId != null">api_id,</if>
             <if test="callerNo != null">caller_no,</if>
             <if test="mobile != null">mobile,</if>
             <if test="status != null">status,</if>
@@ -49,6 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="apiId != null">#{apiId},</if>
             <if test="callerNo != null">#{callerNo},</if>
             <if test="mobile != null">#{mobile},</if>
             <if test="status != null">#{status},</if>
@@ -62,6 +65,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="SET" suffixOverrides=",">
             <if test="companyId != null">company_id = #{companyId},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="apiId != null">api_id = #{apiId},</if>
             <if test="callerNo != null">caller_no = #{callerNo},</if>
             <if test="mobile != null">mobile = #{mobile},</if>
             <if test="status != null">status = #{status},</if>

+ 18 - 0
sql/add_sms_api_menu.sql

@@ -0,0 +1,18 @@
+-- ============================================================
+-- 短信接口菜单添加脚本
+-- ============================================================
+
+-- 查找短信管理分组(通信管理 2400)下的最大order_num
+-- 当前菜单: 2408(短信管理,order=8), 2409(短信套餐,order=9), 2410(短信订单,order=10)
+-- 在短信管理(2408)之后、短信套餐(2409)之前插入短信接口菜单
+
+-- 插入短信接口菜单 (menu_id 2408.5 取 24081)
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, perms, is_frame, create_by, create_time)
+VALUES (24081, '短信接口', 2400, 9, 'smsApi', 'admin/smsApi/index', 'C', 'el-icon-connection', '0', '0', '', 0, 'admin', NOW());
+
+-- 将短信套餐和短信订单的order_num后移
+UPDATE sys_menu SET order_num = 10 WHERE menu_id = 2409;
+UPDATE sys_menu SET order_num = 11 WHERE menu_id = 2410;
+
+-- 给超级管理员角色(role_id=1)授权
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 24081);

+ 85 - 0
sql/voice_api_tenant_migration.sql

@@ -0,0 +1,85 @@
+-- ============================================================
+-- 通话接口多租户分配改造 - 数据库变更脚本
+-- 执行顺序: 先建表 → ALTER → 数据迁移
+-- ============================================================
+
+-- 1. 新建表: 通话接口-租户分配关系
+CREATE TABLE IF NOT EXISTS `company_voice_api_tenant` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `api_id` bigint NOT NULL COMMENT '通话接口ID',
+  `company_id` bigint NOT NULL COMMENT '租户ID',
+  `status` tinyint DEFAULT 1 COMMENT '状态 1启用 0禁用',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_api_company` (`api_id`, `company_id`),
+  KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话接口-租户分配关系';
+
+-- 2. 新建缺失表: 通话套餐
+CREATE TABLE IF NOT EXISTS `company_voice_package` (
+  `package_id` bigint NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
+  `package_name` varchar(100) DEFAULT NULL COMMENT '套餐名',
+  `price` decimal(10,2) DEFAULT NULL COMMENT '价格',
+  `times` bigint DEFAULT NULL COMMENT '时长(分)',
+  `status` int DEFAULT 1 COMMENT '状态 0禁用 1正常',
+  `expire_price` decimal(10,2) DEFAULT NULL COMMENT '超出后每分钟价格',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`package_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话套餐';
+
+-- 3. 新建缺失表: 中间号
+CREATE TABLE IF NOT EXISTS `company_voice_mobile` (
+  `mobile_id` bigint NOT NULL AUTO_INCREMENT COMMENT '中间号ID',
+  `api_id` bigint DEFAULT NULL COMMENT '接口ID',
+  `mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
+  `status` int DEFAULT 1 COMMENT '状态',
+  `company_id` bigint DEFAULT NULL COMMENT '租户ID',
+  `mobile_type` int DEFAULT NULL COMMENT '号码类型 1公共 2私有',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`mobile_id`),
+  KEY `idx_api_id` (`api_id`),
+  KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中间号';
+
+-- 4. company_voice_caller 增加 api_id 字段(坐席关联通话接口)
+-- 先检查字段是否已存在
+SET @dbname = DATABASE();
+SET @tablename = 'company_voice_caller';
+SET @columnname = 'api_id';
+SET @preparedStatement = (SELECT IF(
+  (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+   WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
+  'SELECT 1',
+  'ALTER TABLE company_voice_caller ADD COLUMN api_id bigint DEFAULT NULL COMMENT ''通话接口ID'' AFTER company_user_id'
+));
+PREPARE alterIfNotExists FROM @preparedStatement;
+EXECUTE alterIfNotExists;
+DEALLOCATE PREPARE alterIfNotExists;
+
+-- 添加索引(如果不存在)
+SET @indexname = 'idx_caller_api_id';
+SET @preparedStatement = (SELECT IF(
+  (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+   WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND INDEX_NAME = @indexname) > 0,
+  'SELECT 1',
+  'ALTER TABLE company_voice_caller ADD INDEX idx_caller_api_id (api_id)'
+));
+PREPARE addIndexIfNotExists FROM @preparedStatement;
+EXECUTE addIndexIfNotExists;
+DEALLOCATE PREPARE addIndexIfNotExists;
+
+-- 5. 数据迁移: 将现有的 company.voice_api_id 绑定迁移到分配关系表
+INSERT IGNORE INTO company_voice_api_tenant (api_id, company_id, status, create_time)
+SELECT voice_api_id, company_id, 1, NOW()
+FROM company
+WHERE voice_api_id IS NOT NULL;
+
+-- 6. 数据迁移: 为现有坐席设置默认接口
+UPDATE company_voice_caller c
+JOIN company co ON co.company_id = c.company_id
+SET c.api_id = co.voice_api_id
+WHERE c.api_id IS NULL AND co.voice_api_id IS NOT NULL;