吴树波 преди 3 дни
родител
ревизия
8f7a22aa88

+ 1 - 1
fs-comm-gateway/src/main/java/com/fs/comm/service/CommCallService.java

@@ -94,7 +94,7 @@ public class CommCallService {
             commMetricsService.increment("call.success");
             commMetricsService.increment("call.success");
             return response;
             return response;
         } catch (ServiceException ex) {
         } catch (ServiceException ex) {
-            boolean limitHit = ex.getMessage() != null && ex.getMessage().contains("限制");
+            boolean limitHit = CommGatewayApiLogRecorder.isLimitFailure(ex);
             recordResult = limitHit
             recordResult = limitHit
                     ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, gatewayId)
                     ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, gatewayId)
                     : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, gatewayId);
                     : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, gatewayId);

+ 83 - 9
fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayApiLogRecorder.java

@@ -7,10 +7,13 @@ import com.fs.comm.domain.CommGatewayApiLog;
 import com.fs.comm.dto.CommCallSendRequest;
 import com.fs.comm.dto.CommCallSendRequest;
 import com.fs.comm.dto.CommSmsSendRequest;
 import com.fs.comm.dto.CommSmsSendRequest;
 import com.fs.comm.model.CommGatewayBillingQuote;
 import com.fs.comm.model.CommGatewayBillingQuote;
+import com.fs.comm.support.CommTenantDataSourceHelper;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.ip.IpUtils;
+import com.fs.company.domain.CompanyCommGatewayLog;
+import com.fs.company.service.ICompanyCommGatewayLogService;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -20,7 +23,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
- * 通讯网关 API 调用日志记录(主库)
+ * 通讯网关 API 调用日志记录(主库 + 租户库
  */
  */
 @Slf4j
 @Slf4j
 @Service
 @Service
@@ -29,9 +32,15 @@ public class CommGatewayApiLogRecorder {
     @Autowired
     @Autowired
     private ICommGatewayApiLogService commGatewayApiLogService;
     private ICommGatewayApiLogService commGatewayApiLogService;
 
 
+    @Autowired
+    private ICompanyCommGatewayLogService companyCommGatewayLogService;
+
     @Autowired
     @Autowired
     private CommGatewayBillingService commGatewayBillingService;
     private CommGatewayBillingService commGatewayBillingService;
 
 
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
     public void recordCallAttempt(Long companyId, Long tenantId, CommCallSendRequest request,
     public void recordCallAttempt(Long companyId, Long tenantId, CommCallSendRequest request,
                                   CommApiRecordResult result, String calleePhone, String callerPhone,
                                   CommApiRecordResult result, String calleePhone, String callerPhone,
                                   Long gatewayId, long startMs) {
                                   Long gatewayId, long startMs) {
@@ -58,7 +67,7 @@ public class CommGatewayApiLogRecorder {
             logEntity.setCallerAccount(session.getAccount());
             logEntity.setCallerAccount(session.getAccount());
             logEntity.setAuthScope(session.getScope());
             logEntity.setAuthScope(session.getScope());
         }
         }
-        fillCompanyUserIdFromRequest(logEntity, request);
+        fillIdentityFromRequest(logEntity, request);
         logEntity.setApiType(apiType);
         logEntity.setApiType(apiType);
         logEntity.setApiPath(apiPath);
         logEntity.setApiPath(apiPath);
         logEntity.setRequestBody(JSON.toJSONString(request));
         logEntity.setRequestBody(JSON.toJSONString(request));
@@ -111,14 +120,23 @@ public class CommGatewayApiLogRecorder {
         logEntity.setBillingAmount(quote.getBillingAmount());
         logEntity.setBillingAmount(quote.getBillingAmount());
     }
     }
 
 
-    private void fillCompanyUserIdFromRequest(CommGatewayApiLog logEntity, Object request) {
-        if (logEntity.getCompanyUserId() != null || request == null) {
-            return;
-        }
+    private void fillIdentityFromRequest(CommGatewayApiLog logEntity, Object request) {
         if (request instanceof CommCallSendRequest) {
         if (request instanceof CommCallSendRequest) {
-            logEntity.setCompanyUserId(((CommCallSendRequest) request).getCompanyUserId());
+            CommCallSendRequest callRequest = (CommCallSendRequest) request;
+            if (callRequest.getCompanyUserId() != null) {
+                logEntity.setCompanyUserId(callRequest.getCompanyUserId());
+            }
         } else if (request instanceof CommSmsSendRequest) {
         } else if (request instanceof CommSmsSendRequest) {
-            logEntity.setCompanyUserId(((CommSmsSendRequest) request).getCompanyUserId());
+            CommSmsSendRequest smsRequest = (CommSmsSendRequest) request;
+            if (smsRequest.getCompanyId() != null) {
+                logEntity.setCompanyId(smsRequest.getCompanyId());
+            }
+            if (smsRequest.getCompanyUserId() != null) {
+                logEntity.setCompanyUserId(smsRequest.getCompanyUserId());
+            }
+        }
+        if (logEntity.getCompanyUserId() == null) {
+            logEntity.setCompanyUserId(CommAuthContext.getCompanyUserId());
         }
         }
     }
     }
 
 
@@ -135,6 +153,7 @@ public class CommGatewayApiLogRecorder {
         logEntity.setCostPrice(BigDecimal.ZERO);
         logEntity.setCostPrice(BigDecimal.ZERO);
         logEntity.setCalcPrice(BigDecimal.ZERO);
         logEntity.setCalcPrice(BigDecimal.ZERO);
         logEntity.setBillingQuantity(0);
         logEntity.setBillingQuantity(0);
+        logEntity.setBillingUnit(logEntity.getApiType());
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("code", 500);
         body.put("code", 500);
         body.put("msg", message);
         body.put("msg", message);
@@ -142,11 +161,66 @@ public class CommGatewayApiLogRecorder {
     }
     }
 
 
     private void record(CommGatewayApiLog logEntity) {
     private void record(CommGatewayApiLog logEntity) {
+        Long masterLogId = null;
         try {
         try {
             commGatewayApiLogService.saveLog(logEntity);
             commGatewayApiLogService.saveLog(logEntity);
+            masterLogId = logEntity.getLogId();
         } catch (Exception ex) {
         } catch (Exception ex) {
-            log.error("写入通讯网关调用日志失败", ex);
+            log.error("写入主库通讯网关调用日志失败", ex);
+        }
+        saveTenantLog(logEntity, masterLogId);
+    }
+
+    private void saveTenantLog(CommGatewayApiLog logEntity, Long masterLogId) {
+        if (logEntity == null || logEntity.getTenantId() == null) {
+            return;
+        }
+        try {
+            commTenantDataSourceHelper.ensureTenant(logEntity.getTenantId());
+            companyCommGatewayLogService.saveLog(toTenantLog(logEntity, masterLogId));
+        } catch (Exception ex) {
+            log.error("写入租户通讯网关调用日志失败 tenantId={}, companyId={}",
+                    logEntity.getTenantId(), logEntity.getCompanyId(), ex);
+        }
+    }
+
+    private CompanyCommGatewayLog toTenantLog(CommGatewayApiLog source, Long masterLogId) {
+        CompanyCommGatewayLog target = new CompanyCommGatewayLog();
+        target.setMasterLogId(masterLogId);
+        target.setCompanyId(source.getCompanyId());
+        target.setCompanyUserId(source.getCompanyUserId());
+        target.setCallerAccount(source.getCallerAccount());
+        target.setApiType(source.getApiType());
+        target.setApiPath(source.getApiPath());
+        target.setRequestBody(source.getRequestBody());
+        target.setResponseBody(source.getResponseBody());
+        target.setResultCode(source.getResultCode());
+        target.setResultMsg(source.getResultMsg());
+        target.setSuccess(source.getSuccess());
+        target.setLimitHit(source.getLimitHit());
+        target.setLimitReason(source.getLimitReason());
+        target.setCalleePhone(source.getCalleePhone());
+        target.setCallerPhone(source.getCallerPhone());
+        target.setGatewayId(source.getGatewayId());
+        target.setBillingAmount(source.getBillingAmount());
+        target.setCostPrice(source.getCostPrice());
+        target.setCalcPrice(source.getCalcPrice());
+        target.setBillingQuantity(source.getBillingQuantity());
+        target.setBillingUnit(source.getBillingUnit());
+        target.setClientIp(source.getClientIp());
+        target.setAuthScope(source.getAuthScope());
+        target.setDurationMs(source.getDurationMs());
+        target.setCreateTime(source.getCreateTime());
+        return target;
+    }
+
+    /** 是否为频率/QPS 类限制失败 */
+    public static boolean isLimitFailure(ServiceException ex) {
+        if (ex == null || StringUtils.isBlank(ex.getMessage())) {
+            return false;
         }
         }
+        String message = ex.getMessage();
+        return message.contains("限制") || message.contains("超限") || message.contains("频率");
     }
     }
 
 
     public CommApiRecordResult buildLimitFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
     public CommApiRecordResult buildLimitFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {

+ 6 - 1
fs-comm-gateway/src/main/java/com/fs/comm/service/CommSmsService.java

@@ -50,6 +50,11 @@ public class CommSmsService {
             if (companyId == null) {
             if (companyId == null) {
                 throw new ServiceException("未获取到公司信息");
                 throw new ServiceException("未获取到公司信息");
             }
             }
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+            if (request != null) {
+                calleePhone = commVoiceLimitService.resolveSmsCalleePhone(
+                        request.getCalleeId(), request.getCustomerId(), request.getPhone());
+            }
             callerKey = commVoiceLimitService.buildCompanyCallerKey(companyId, null);
             callerKey = commVoiceLimitService.buildCompanyCallerKey(companyId, null);
 
 
             commRateLimitService.checkTenantQps(tenantId);
             commRateLimitService.checkTenantQps(tenantId);
@@ -80,7 +85,7 @@ public class CommSmsService {
             commMetricsService.increment("sms.success");
             commMetricsService.increment("sms.success");
             return response;
             return response;
         } catch (ServiceException ex) {
         } catch (ServiceException ex) {
-            boolean limitHit = ex.getMessage() != null && ex.getMessage().contains("限制");
+            boolean limitHit = CommGatewayApiLogRecorder.isLimitFailure(ex);
             recordResult = limitHit
             recordResult = limitHit
                     ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, null)
                     ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, null)
                     : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, null);
                     : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, null);

+ 10 - 6
fs-comm-gateway/对接文档.md

@@ -562,13 +562,17 @@ location /comm/ {
 
 
 ## 11. 附录:相关数据表
 ## 11. 附录:相关数据表
 
 
-| 表名 | 说明 |
-|------|------|
-| company_voice_robotic_call_log_callphone | AI 外呼执行日志 |
-| company_voice_robotic_call_log_sendmsg | 短信发送日志 |
-| company_voice_robotic_call_log_addwx | 加微执行日志(其他模块写入) |
+| 表名 | 库 | 说明 |
+|------|-----|------|
+| comm_gateway_api_log | 主库 | 网关 API 调用日志(频率计数、Admin 查询) |
+| company_comm_gateway_log | 租户库 | 网关 API 调用记录(成功/失败/限频均写入) |
+| company_voice_robotic_call_log_callphone | 租户库 | AI 外呼执行日志 |
+| company_voice_robotic_call_log_sendmsg | 租户库 | 短信发送日志 |
+| company_voice_robotic_call_log_addwx | 租户库 | 加微执行日志(其他模块写入) |
+
+外呼/短信业务日志与 `company_comm_gateway_log` 均写入**当前租户库**;`comm_gateway_api_log` 写入主库用于全平台统计与频率限制计数。
 
 
-日志写入均路由至**当前租户库**,非主库。
+存量租户需执行:`fs-service/src/main/resources/db/20250604-company-comm-gateway-log.sql`
 
 
 ---
 ---
 
 

+ 47 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyCommGatewayLogController.java

@@ -0,0 +1,47 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.domain.CompanyCommGatewayLog;
+import com.fs.company.service.ICompanyCommGatewayLogService;
+import com.fs.framework.security.SecurityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 通讯网关 API 调用记录(租户库)
+ */
+@RestController
+@RequestMapping("/company/commGatewayLog")
+public class CompanyCommGatewayLogController extends BaseController {
+
+    @Autowired
+    private ICompanyCommGatewayLogService companyCommGatewayLogService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyCommGatewayLog query) {
+        query.setCompanyId(currentCompanyId());
+        startPage();
+        List<CompanyCommGatewayLog> list = companyCommGatewayLogService.selectList(query);
+        return getDataTable(list);
+    }
+
+    @GetMapping("/{logId}")
+    public AjaxResult getInfo(@PathVariable Long logId) {
+        CompanyCommGatewayLog log = companyCommGatewayLogService.selectById(logId);
+        if (log == null || !currentCompanyId().equals(log.getCompanyId())) {
+            return AjaxResult.error("记录不存在");
+        }
+        return AjaxResult.success(log);
+    }
+
+    private Long currentCompanyId() {
+        return SecurityUtils.getLoginUser().getUser().getCompanyId();
+    }
+}

+ 5 - 0
fs-service/src/main/java/com/fs/comm/service/CommVoiceLimitService.java

@@ -122,6 +122,11 @@ public class CommVoiceLimitService {
         return normalizePhone(PhoneUtil.decryptAutoPhone(callees.getPhone()));
         return normalizePhone(PhoneUtil.decryptAutoPhone(callees.getPhone()));
     }
     }
 
 
+    /** 解析短信被叫号码(供日志与限频使用) */
+    public String resolveSmsCalleePhone(Long calleeId, Long customerId, String phone) {
+        return resolveSmsPhone(calleeId, customerId, phone);
+    }
+
     private String resolveSmsPhone(Long calleeId, Long customerId, String phone) {
     private String resolveSmsPhone(Long calleeId, Long customerId, String phone) {
         if (StringUtils.isNotBlank(phone)) {
         if (StringUtils.isNotBlank(phone)) {
             return normalizePhone(PhoneUtil.decryptAutoPhone(phone.trim()));
             return normalizePhone(PhoneUtil.decryptAutoPhone(phone.trim()));

+ 49 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyCommGatewayLog.java

@@ -0,0 +1,49 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 通讯网关 API 调用记录(租户库 company_comm_gateway_log)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyCommGatewayLog extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long logId;
+    /** 主库 comm_gateway_api_log.log_id */
+    private Long masterLogId;
+    private Long companyId;
+    private Long companyUserId;
+    private String callerAccount;
+    private String apiType;
+    private String apiPath;
+    private String requestBody;
+    private String responseBody;
+    private Integer resultCode;
+    private String resultMsg;
+    private Integer success;
+    private Integer limitHit;
+    private String limitReason;
+    private String calleePhone;
+    private String callerPhone;
+    private Long gatewayId;
+    private BigDecimal billingAmount;
+    private BigDecimal costPrice;
+    private BigDecimal calcPrice;
+    private Integer billingQuantity;
+    private String billingUnit;
+    private String clientIp;
+    private String authScope;
+    private Integer durationMs;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyCommGatewayLogMapper.java

@@ -0,0 +1,14 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyCommGatewayLog;
+
+import java.util.List;
+
+public interface CompanyCommGatewayLogMapper {
+
+    int insertCompanyCommGatewayLog(CompanyCommGatewayLog log);
+
+    CompanyCommGatewayLog selectCompanyCommGatewayLogById(Long logId);
+
+    List<CompanyCommGatewayLog> selectCompanyCommGatewayLogList(CompanyCommGatewayLog query);
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyCommGatewayLogService.java

@@ -0,0 +1,14 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CompanyCommGatewayLog;
+
+import java.util.List;
+
+public interface ICompanyCommGatewayLogService {
+
+    void saveLog(CompanyCommGatewayLog log);
+
+    CompanyCommGatewayLog selectById(Long logId);
+
+    List<CompanyCommGatewayLog> selectList(CompanyCommGatewayLog query);
+}

+ 35 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyCommGatewayLogServiceImpl.java

@@ -0,0 +1,35 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyCommGatewayLog;
+import com.fs.company.mapper.CompanyCommGatewayLogMapper;
+import com.fs.company.service.ICompanyCommGatewayLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CompanyCommGatewayLogServiceImpl implements ICompanyCommGatewayLogService {
+
+    @Autowired
+    private CompanyCommGatewayLogMapper companyCommGatewayLogMapper;
+
+    @Override
+    public void saveLog(CompanyCommGatewayLog log) {
+        if (log.getCreateTime() == null) {
+            log.setCreateTime(DateUtils.getNowDate());
+        }
+        companyCommGatewayLogMapper.insertCompanyCommGatewayLog(log);
+    }
+
+    @Override
+    public CompanyCommGatewayLog selectById(Long logId) {
+        return companyCommGatewayLogMapper.selectCompanyCommGatewayLogById(logId);
+    }
+
+    @Override
+    public List<CompanyCommGatewayLog> selectList(CompanyCommGatewayLog query) {
+        return companyCommGatewayLogMapper.selectCompanyCommGatewayLogList(query);
+    }
+}

+ 0 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java

@@ -40,7 +40,6 @@ public class ContactInfo {
 
 
     /** 手机号 */
     /** 手机号 */
     private String phone;
     private String phone;
-    private Long companyId;
 
 
     /** 渠道特有字段(不同渠道有不同的扩展信息) */
     /** 渠道特有字段(不同渠道有不同的扩展信息) */
     private Map<String, Object> extra;
     private Map<String, Object> extra;

+ 0 - 5
fs-service/src/main/java/com/fs/company/service/workflow/impl/SensitiveWordServiceImpl.java

@@ -131,11 +131,6 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
         return result;
         return result;
     }
     }
 
 
-    @Override
-    public boolean isHighRiskSensitiveWord(String content, Long companyId) {
-        return false;
-    }
-
     private void loadFromOtherSources(List<Map<String, Object>> result) {
     private void loadFromOtherSources(List<Map<String, Object>> result) {
         if (fastGptChatReplaceWordsMapper != null) {
         if (fastGptChatReplaceWordsMapper != null) {
             try {
             try {

+ 45 - 0
fs-service/src/main/resources/db/20250604-company-comm-gateway-log.sql

@@ -0,0 +1,45 @@
+-- 租户库:通讯网关 API 调用记录(存量租户执行,幂等)
+SET @dbname = DATABASE();
+SET @tablename = 'company_comm_gateway_log';
+
+SET @preparedStatement = (SELECT IF(
+  (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
+   WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename) > 0,
+  'SELECT 1',
+  'CREATE TABLE `company_comm_gateway_log` (
+    `log_id` bigint NOT NULL AUTO_INCREMENT COMMENT ''主键'',
+    `master_log_id` bigint DEFAULT NULL COMMENT ''主库日志ID'',
+    `company_id` bigint DEFAULT NULL COMMENT ''公司ID'',
+    `company_user_id` bigint DEFAULT NULL COMMENT ''调用人用户ID'',
+    `caller_account` varchar(100) DEFAULT NULL COMMENT ''调用人账号'',
+    `api_type` varchar(20) NOT NULL COMMENT ''接口类型 call/sms'',
+    `api_path` varchar(200) DEFAULT NULL COMMENT ''请求路径'',
+    `request_body` mediumtext COMMENT ''请求参数'',
+    `response_body` mediumtext COMMENT ''返回参数'',
+    `result_code` int DEFAULT NULL COMMENT ''业务code'',
+    `result_msg` varchar(500) DEFAULT NULL COMMENT ''结果说明'',
+    `success` tinyint(1) DEFAULT 0 COMMENT ''是否成功 0否1是'',
+    `limit_hit` tinyint(1) DEFAULT 0 COMMENT ''是否触发频率限制 0否1是'',
+    `limit_reason` varchar(200) DEFAULT NULL COMMENT ''限制原因'',
+    `callee_phone` varchar(32) DEFAULT NULL COMMENT ''被叫号码'',
+    `caller_phone` varchar(32) DEFAULT NULL COMMENT ''主叫号码/线路标识'',
+    `gateway_id` bigint DEFAULT NULL COMMENT ''外呼线路ID'',
+    `billing_amount` decimal(12,4) DEFAULT 0.0000 COMMENT ''计费金额'',
+    `cost_price` decimal(12,4) DEFAULT 0.0000 COMMENT ''成本价(单价)'',
+    `calc_price` decimal(12,4) DEFAULT 0.0000 COMMENT ''计算价/售价(单价)'',
+    `billing_quantity` int DEFAULT 1 COMMENT ''计费数量'',
+    `billing_unit` varchar(20) DEFAULT NULL COMMENT ''计费单位 call/sms'',
+    `client_ip` varchar(64) DEFAULT NULL COMMENT ''客户端IP'',
+    `auth_scope` varchar(20) DEFAULT NULL COMMENT ''鉴权范围 external/internal'',
+    `duration_ms` int DEFAULT NULL COMMENT ''耗时毫秒'',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
+    PRIMARY KEY (`log_id`),
+    KEY `idx_company_callee_time` (`company_id`,`callee_phone`,`create_time`),
+    KEY `idx_company_caller_time` (`company_id`,`caller_phone`,`create_time`),
+    KEY `idx_company_api_time` (`company_id`,`api_type`,`create_time`),
+    KEY `idx_master_log_id` (`master_log_id`)
+  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''通讯网关API调用记录(租户库)'''
+));
+PREPARE createIfNotExists FROM @preparedStatement;
+EXECUTE createIfNotExists;
+DEALLOCATE PREPARE createIfNotExists;

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

@@ -17954,6 +17954,44 @@ CREATE TABLE `company_ai_workflow`
     INDEX             `index_del`(`del_flag`) USING BTREE
     INDEX             `index_del`(`del_flag`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
 -- ----------------------------
 -- ----------------------------
+-- Table structure for company_comm_gateway_log
+-- ----------------------------
+DROP TABLE IF EXISTS `company_comm_gateway_log`;
+CREATE TABLE `company_comm_gateway_log`
+(
+    `log_id`           bigint         NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `master_log_id`    bigint         DEFAULT NULL COMMENT '主库日志ID',
+    `company_id`       bigint         DEFAULT NULL COMMENT '公司ID',
+    `company_user_id`  bigint         DEFAULT NULL COMMENT '调用人用户ID',
+    `caller_account`   varchar(100)   DEFAULT NULL COMMENT '调用人账号',
+    `api_type`         varchar(20)    NOT NULL COMMENT '接口类型 call/sms',
+    `api_path`         varchar(200)   DEFAULT NULL COMMENT '请求路径',
+    `request_body`     mediumtext     COMMENT '请求参数',
+    `response_body`    mediumtext     COMMENT '返回参数',
+    `result_code`      int            DEFAULT NULL COMMENT '业务code',
+    `result_msg`       varchar(500)   DEFAULT NULL COMMENT '结果说明',
+    `success`          tinyint(1)     DEFAULT 0 COMMENT '是否成功 0否1是',
+    `limit_hit`        tinyint(1)     DEFAULT 0 COMMENT '是否触发频率限制 0否1是',
+    `limit_reason`     varchar(200)   DEFAULT NULL COMMENT '限制原因',
+    `callee_phone`     varchar(32)    DEFAULT NULL COMMENT '被叫号码',
+    `caller_phone`     varchar(32)    DEFAULT NULL COMMENT '主叫号码/线路标识',
+    `gateway_id`       bigint         DEFAULT NULL COMMENT '外呼线路ID',
+    `billing_amount`   decimal(12, 4) DEFAULT 0.0000 COMMENT '计费金额',
+    `cost_price`       decimal(12, 4) DEFAULT 0.0000 COMMENT '成本价(单价)',
+    `calc_price`       decimal(12, 4) DEFAULT 0.0000 COMMENT '计算价/售价(单价)',
+    `billing_quantity` int            DEFAULT 1 COMMENT '计费数量',
+    `billing_unit`     varchar(20)    DEFAULT NULL COMMENT '计费单位 call/sms',
+    `client_ip`        varchar(64)    DEFAULT NULL COMMENT '客户端IP',
+    `auth_scope`       varchar(20)    DEFAULT NULL COMMENT '鉴权范围 external/internal',
+    `duration_ms`      int            DEFAULT NULL COMMENT '耗时毫秒',
+    `create_time`      datetime       DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    PRIMARY KEY (`log_id`) USING BTREE,
+    INDEX              `idx_company_callee_time`(`company_id`, `callee_phone`, `create_time`) USING BTREE,
+    INDEX              `idx_company_caller_time`(`company_id`, `caller_phone`, `create_time`) USING BTREE,
+    INDEX              `idx_company_api_time`(`company_id`, `api_type`, `create_time`) USING BTREE,
+    INDEX              `idx_master_log_id`(`master_log_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '通讯网关API调用记录(租户库)' ROW_FORMAT = DYNAMIC;
+-- ----------------------------
 -- Table structure for company_bind_gateway
 -- Table structure for company_bind_gateway
 -- ----------------------------
 -- ----------------------------
 DROP TABLE IF EXISTS `company_bind_gateway`;
 DROP TABLE IF EXISTS `company_bind_gateway`;

+ 80 - 0
fs-service/src/main/resources/mapper/company/CompanyCommGatewayLogMapper.xml

@@ -0,0 +1,80 @@
+<?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.CompanyCommGatewayLogMapper">
+
+    <resultMap id="CompanyCommGatewayLogResult" type="com.fs.company.domain.CompanyCommGatewayLog">
+        <result property="logId" column="log_id"/>
+        <result property="masterLogId" column="master_log_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="callerAccount" column="caller_account"/>
+        <result property="apiType" column="api_type"/>
+        <result property="apiPath" column="api_path"/>
+        <result property="requestBody" column="request_body"/>
+        <result property="responseBody" column="response_body"/>
+        <result property="resultCode" column="result_code"/>
+        <result property="resultMsg" column="result_msg"/>
+        <result property="success" column="success"/>
+        <result property="limitHit" column="limit_hit"/>
+        <result property="limitReason" column="limit_reason"/>
+        <result property="calleePhone" column="callee_phone"/>
+        <result property="callerPhone" column="caller_phone"/>
+        <result property="gatewayId" column="gateway_id"/>
+        <result property="billingAmount" column="billing_amount"/>
+        <result property="costPrice" column="cost_price"/>
+        <result property="calcPrice" column="calc_price"/>
+        <result property="billingQuantity" column="billing_quantity"/>
+        <result property="billingUnit" column="billing_unit"/>
+        <result property="clientIp" column="client_ip"/>
+        <result property="authScope" column="auth_scope"/>
+        <result property="durationMs" column="duration_ms"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <sql id="selectCompanyCommGatewayLogVo">
+        select log_id, master_log_id, company_id, company_user_id, caller_account, api_type, api_path,
+               request_body, response_body, result_code, result_msg, success, limit_hit, limit_reason,
+               callee_phone, caller_phone, gateway_id, billing_amount, cost_price, calc_price, billing_quantity,
+               billing_unit, client_ip, auth_scope, duration_ms, create_time
+        from company_comm_gateway_log
+    </sql>
+
+    <select id="selectCompanyCommGatewayLogList" resultMap="CompanyCommGatewayLogResult">
+        <include refid="selectCompanyCommGatewayLogVo"/>
+        <where>
+            <if test="companyId != null">and company_id = #{companyId}</if>
+            <if test="companyUserId != null">and company_user_id = #{companyUserId}</if>
+            <if test="apiType != null and apiType != ''">and api_type = #{apiType}</if>
+            <if test="success != null">and success = #{success}</if>
+            <if test="limitHit != null">and limit_hit = #{limitHit}</if>
+            <if test="calleePhone != null and calleePhone != ''">and callee_phone like concat('%', #{calleePhone}, '%')</if>
+            <if test="callerAccount != null and callerAccount != ''">and caller_account like concat('%', #{callerAccount}, '%')</if>
+            <if test="params.beginTime != null and params.beginTime != ''">
+                and create_time &gt;= #{params.beginTime}
+            </if>
+            <if test="params.endTime != null and params.endTime != ''">
+                and create_time &lt;= #{params.endTime}
+            </if>
+        </where>
+        order by log_id desc
+    </select>
+
+    <select id="selectCompanyCommGatewayLogById" resultMap="CompanyCommGatewayLogResult">
+        <include refid="selectCompanyCommGatewayLogVo"/>
+        where log_id = #{logId}
+    </select>
+
+    <insert id="insertCompanyCommGatewayLog" useGeneratedKeys="true" keyProperty="logId">
+        insert into company_comm_gateway_log
+        (master_log_id, company_id, company_user_id, caller_account, api_type, api_path,
+         request_body, response_body, result_code, result_msg, success, limit_hit, limit_reason,
+         callee_phone, caller_phone, gateway_id, billing_amount, cost_price, calc_price, billing_quantity,
+         billing_unit, client_ip, auth_scope, duration_ms, create_time)
+        values
+        (#{masterLogId}, #{companyId}, #{companyUserId}, #{callerAccount}, #{apiType}, #{apiPath},
+         #{requestBody}, #{responseBody}, #{resultCode}, #{resultMsg}, #{success}, #{limitHit}, #{limitReason},
+         #{calleePhone}, #{callerPhone}, #{gatewayId}, #{billingAmount}, #{costPrice}, #{calcPrice}, #{billingQuantity},
+         #{billingUnit}, #{clientIp}, #{authScope}, #{durationMs}, #{createTime})
+    </insert>
+
+</mapper>