Преглед на файлове

修复编码问题,加上外呼独立模块的一些功能

吴树波 преди 4 дни
родител
ревизия
eb87be6566

+ 0 - 16
.codegraph/.gitignore

@@ -1,16 +0,0 @@
-# CodeGraph data files
-# These are local to each machine and should not be committed
-
-# Database
-*.db
-*.db-wal
-*.db-shm
-
-# Cache
-cache/
-
-# Logs
-*.log
-
-# Hook markers
-.dirty

+ 51 - 270
fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayApiLogRecorder.java

@@ -1,461 +1,242 @@
 package com.fs.comm.service;
 package com.fs.comm.service;
 
 
-
-
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
-
 import com.fs.comm.auth.CommSession;
 import com.fs.comm.auth.CommSession;
-
 import com.fs.comm.context.CommAuthContext;
 import com.fs.comm.context.CommAuthContext;
-
 import com.fs.comm.domain.CommGatewayApiLog;
 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.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 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.beans.factory.annotation.Value;
-
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
-
-
 import java.math.BigDecimal;
 import java.math.BigDecimal;
-
 import java.util.HashMap;
 import java.util.HashMap;
-
 import java.util.Map;
 import java.util.Map;
 
 
-
-
 /**
 /**
-
  * 通讯网关 API 调用日志记录(主库)
  * 通讯网关 API 调用日志记录(主库)
-
  */
  */
-
 @Slf4j
 @Slf4j
-
 @Service
 @Service
-
 public class CommGatewayApiLogRecorder {
 public class CommGatewayApiLogRecorder {
 
 
-
-
-    @Value("${comm.gateway.call-unit-price:0.12}")
-
-    private BigDecimal callUnitPrice;
-
-
-
-    @Value("${comm.gateway.sms-unit-price:0.05}")
-
-    private BigDecimal smsUnitPrice;
-
-
-
     @Autowired
     @Autowired
-
     private ICommGatewayApiLogService commGatewayApiLogService;
     private ICommGatewayApiLogService commGatewayApiLogService;
 
 
-
+    @Autowired
+    private CommGatewayBillingService commGatewayBillingService;
 
 
     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) {
-
         record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_CALL, "/comm/call/send",
         record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_CALL, "/comm/call/send",
-
                 request, result, calleePhone, callerPhone, gatewayId, startMs));
                 request, result, calleePhone, callerPhone, gatewayId, startMs));
-
     }
     }
 
 
-
-
     public void recordSmsAttempt(Long companyId, Long tenantId, CommSmsSendRequest request,
     public void recordSmsAttempt(Long companyId, Long tenantId, CommSmsSendRequest request,
-
                                  CommApiRecordResult result, String calleePhone, String callerPhone,
                                  CommApiRecordResult result, String calleePhone, String callerPhone,
-
                                  Long gatewayId, long startMs) {
                                  Long gatewayId, long startMs) {
-
         record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_SMS, "/comm/sms/send",
         record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_SMS, "/comm/sms/send",
-
                 request, result, calleePhone, callerPhone, gatewayId, startMs));
                 request, result, calleePhone, callerPhone, gatewayId, startMs));
-
     }
     }
 
 
-
-
     private CommGatewayApiLog buildBaseLog(Long companyId, Long tenantId, String apiType, String apiPath,
     private CommGatewayApiLog buildBaseLog(Long companyId, Long tenantId, String apiType, String apiPath,
-
                                            Object request, CommApiRecordResult result, String calleePhone,
                                            Object request, CommApiRecordResult result, String calleePhone,
-
                                            String callerPhone, Long gatewayId, long startMs) {
                                            String callerPhone, Long gatewayId, long startMs) {
-
         CommSession session = CommAuthContext.get();
         CommSession session = CommAuthContext.get();
-
-        CommGatewayApiLog log = new CommGatewayApiLog();
-
-        log.setTenantId(tenantId);
-
-        log.setCompanyId(companyId);
-
+        CommGatewayApiLog logEntity = new CommGatewayApiLog();
+        logEntity.setTenantId(tenantId);
+        logEntity.setCompanyId(companyId);
         if (session != null) {
         if (session != null) {
-
-            log.setCompanyUserId(session.getCompanyUserId());
-
-            log.setCallerAccount(session.getAccount());
-
-            log.setAuthScope(session.getScope());
-
+            logEntity.setCompanyUserId(session.getCompanyUserId());
+            logEntity.setCallerAccount(session.getAccount());
+            logEntity.setAuthScope(session.getScope());
         }
         }
-
-        fillCompanyUserIdFromRequest(log, request);
-
-        log.setApiType(apiType);
-
-        log.setApiPath(apiPath);
-
-        log.setRequestBody(JSON.toJSONString(request));
-
-        log.setDurationMs((int) (System.currentTimeMillis() - startMs));
-
+        fillCompanyUserIdFromRequest(logEntity, request);
+        logEntity.setApiType(apiType);
+        logEntity.setApiPath(apiPath);
+        logEntity.setRequestBody(JSON.toJSONString(request));
+        logEntity.setDurationMs((int) (System.currentTimeMillis() - startMs));
         try {
         try {
-
-            log.setClientIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
-
+            logEntity.setClientIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
         } catch (Exception ignored) {
         } catch (Exception ignored) {
-
         }
         }
 
 
-
-
         if (result != null) {
         if (result != null) {
-
-            log.setResponseBody(result.getResponseBody());
-
-            log.setResultCode(result.getResultCode());
-
-            log.setResultMsg(result.getResultMsg());
-
-            log.setSuccess(result.isSuccess() ? 1 : 0);
-
-            log.setLimitHit(result.isLimitHit() ? 1 : 0);
-
-            log.setLimitReason(result.getLimitReason());
-
-            log.setCalleePhone(result.getCalleePhone());
-
-            log.setCallerPhone(result.getCallerPhone());
-
-            log.setGatewayId(result.getGatewayId());
-
-            if (result.isSuccess()) {
-
-                log.setBillingAmount(CommGatewayApiLog.API_TYPE_CALL.equals(apiType) ? callUnitPrice : smsUnitPrice);
-
-                log.setBillingUnit(apiType);
-
-            } else {
-
-                log.setBillingAmount(BigDecimal.ZERO);
-
-            }
-
+            logEntity.setResponseBody(result.getResponseBody());
+            logEntity.setResultCode(result.getResultCode());
+            logEntity.setResultMsg(result.getResultMsg());
+            logEntity.setSuccess(result.isSuccess() ? 1 : 0);
+            logEntity.setLimitHit(result.isLimitHit() ? 1 : 0);
+            logEntity.setLimitReason(result.getLimitReason());
+            logEntity.setCalleePhone(result.getCalleePhone());
+            logEntity.setCallerPhone(result.getCallerPhone());
+            logEntity.setGatewayId(result.getGatewayId());
+            applyBilling(logEntity, tenantId, apiType, result.isSuccess());
         } else {
         } else {
-
-            applyFallbackFields(log, calleePhone, callerPhone, gatewayId, "调用未正常返回结果");
-
+            applyFallbackFields(logEntity, calleePhone, callerPhone, gatewayId, "调用未正常返回结果");
         }
         }
 
 
-
-
-        if (StringUtils.isBlank(log.getCalleePhone()) && StringUtils.isNotBlank(calleePhone)) {
-
-            log.setCalleePhone(calleePhone);
-
+        if (StringUtils.isBlank(logEntity.getCalleePhone()) && StringUtils.isNotBlank(calleePhone)) {
+            logEntity.setCalleePhone(calleePhone);
         }
         }
-
-        if (StringUtils.isBlank(log.getCallerPhone()) && StringUtils.isNotBlank(callerPhone)) {
-
-            log.setCallerPhone(callerPhone);
-
+        if (StringUtils.isBlank(logEntity.getCallerPhone()) && StringUtils.isNotBlank(callerPhone)) {
+            logEntity.setCallerPhone(callerPhone);
         }
         }
-
-        if (log.getGatewayId() == null && gatewayId != null) {
-
-            log.setGatewayId(gatewayId);
-
+        if (logEntity.getGatewayId() == null && gatewayId != null) {
+            logEntity.setGatewayId(gatewayId);
         }
         }
-
-        return log;
-
+        return logEntity;
     }
     }
 
 
-
+    private void applyBilling(CommGatewayApiLog logEntity, Long tenantId, String apiType, boolean success) {
+        logEntity.setBillingUnit(apiType);
+        if (!success) {
+            logEntity.setBillingAmount(BigDecimal.ZERO);
+            logEntity.setCostPrice(BigDecimal.ZERO);
+            logEntity.setCalcPrice(BigDecimal.ZERO);
+            logEntity.setBillingQuantity(0);
+            return;
+        }
+        CommGatewayBillingQuote quote = commGatewayBillingService.resolveQuote(tenantId, apiType, 1);
+        logEntity.setCostPrice(quote.getCostPrice());
+        logEntity.setCalcPrice(quote.getCalcPrice());
+        logEntity.setBillingQuantity(quote.getBillingQuantity());
+        logEntity.setBillingAmount(quote.getBillingAmount());
+    }
 
 
     private void fillCompanyUserIdFromRequest(CommGatewayApiLog logEntity, Object request) {
     private void fillCompanyUserIdFromRequest(CommGatewayApiLog logEntity, Object request) {
-
         if (logEntity.getCompanyUserId() != null || request == null) {
         if (logEntity.getCompanyUserId() != null || request == null) {
-
             return;
             return;
-
         }
         }
-
         if (request instanceof CommCallSendRequest) {
         if (request instanceof CommCallSendRequest) {
-
             logEntity.setCompanyUserId(((CommCallSendRequest) request).getCompanyUserId());
             logEntity.setCompanyUserId(((CommCallSendRequest) request).getCompanyUserId());
-
         } else if (request instanceof CommSmsSendRequest) {
         } else if (request instanceof CommSmsSendRequest) {
-
             logEntity.setCompanyUserId(((CommSmsSendRequest) request).getCompanyUserId());
             logEntity.setCompanyUserId(((CommSmsSendRequest) request).getCompanyUserId());
-
         }
         }
-
     }
     }
 
 
-
-
     private void applyFallbackFields(CommGatewayApiLog logEntity, String calleePhone, String callerPhone,
     private void applyFallbackFields(CommGatewayApiLog logEntity, String calleePhone, String callerPhone,
-
                                      Long gatewayId, String message) {
                                      Long gatewayId, String message) {
-
         logEntity.setCalleePhone(calleePhone);
         logEntity.setCalleePhone(calleePhone);
-
         logEntity.setCallerPhone(callerPhone);
         logEntity.setCallerPhone(callerPhone);
-
         logEntity.setGatewayId(gatewayId);
         logEntity.setGatewayId(gatewayId);
-
         logEntity.setSuccess(0);
         logEntity.setSuccess(0);
-
         logEntity.setLimitHit(0);
         logEntity.setLimitHit(0);
-
         logEntity.setResultCode(500);
         logEntity.setResultCode(500);
-
         logEntity.setResultMsg(message);
         logEntity.setResultMsg(message);
-
         logEntity.setBillingAmount(BigDecimal.ZERO);
         logEntity.setBillingAmount(BigDecimal.ZERO);
-
+        logEntity.setCostPrice(BigDecimal.ZERO);
+        logEntity.setCalcPrice(BigDecimal.ZERO);
+        logEntity.setBillingQuantity(0);
         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);
-
         logEntity.setResponseBody(JSON.toJSONString(body));
         logEntity.setResponseBody(JSON.toJSONString(body));
-
     }
     }
 
 
-
-
     private void record(CommGatewayApiLog logEntity) {
     private void record(CommGatewayApiLog logEntity) {
-
         try {
         try {
-
             commGatewayApiLogService.saveLog(logEntity);
             commGatewayApiLogService.saveLog(logEntity);
-
         } catch (Exception ex) {
         } catch (Exception ex) {
-
             log.error("写入通讯网关调用日志失败", ex);
             log.error("写入通讯网关调用日志失败", ex);
-
         }
         }
-
     }
     }
 
 
-
-
     public CommApiRecordResult buildLimitFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
     public CommApiRecordResult buildLimitFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
-
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
-
         body.put("code", 500);
         body.put("code", 500);
-
         body.put("msg", ex.getMessage());
         body.put("msg", ex.getMessage());
-
         return CommApiRecordResult.builder()
         return CommApiRecordResult.builder()
-
                 .success(false)
                 .success(false)
-
                 .limitHit(true)
                 .limitHit(true)
-
                 .limitReason(ex.getMessage())
                 .limitReason(ex.getMessage())
-
                 .resultCode(500)
                 .resultCode(500)
-
                 .resultMsg(ex.getMessage())
                 .resultMsg(ex.getMessage())
-
                 .responseBody(JSON.toJSONString(body))
                 .responseBody(JSON.toJSONString(body))
-
                 .calleePhone(calleePhone)
                 .calleePhone(calleePhone)
-
                 .callerPhone(callerPhone)
                 .callerPhone(callerPhone)
-
                 .gatewayId(gatewayId)
                 .gatewayId(gatewayId)
-
                 .build();
                 .build();
-
     }
     }
 
 
-
-
     public CommApiRecordResult buildSuccess(Object data, String calleePhone, String callerPhone, Long gatewayId) {
     public CommApiRecordResult buildSuccess(Object data, String calleePhone, String callerPhone, Long gatewayId) {
-
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
-
         body.put("code", 200);
         body.put("code", 200);
-
         body.put("msg", "success");
         body.put("msg", "success");
-
         body.put("data", data);
         body.put("data", data);
-
         return CommApiRecordResult.builder()
         return CommApiRecordResult.builder()
-
                 .success(true)
                 .success(true)
-
                 .limitHit(false)
                 .limitHit(false)
-
                 .resultCode(200)
                 .resultCode(200)
-
                 .resultMsg("success")
                 .resultMsg("success")
-
                 .responseBody(JSON.toJSONString(body))
                 .responseBody(JSON.toJSONString(body))
-
                 .calleePhone(calleePhone)
                 .calleePhone(calleePhone)
-
                 .callerPhone(callerPhone)
                 .callerPhone(callerPhone)
-
                 .gatewayId(gatewayId)
                 .gatewayId(gatewayId)
-
                 .build();
                 .build();
-
     }
     }
 
 
-
-
     public CommApiRecordResult buildFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
     public CommApiRecordResult buildFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
-
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
-
         body.put("code", 500);
         body.put("code", 500);
-
         body.put("msg", ex.getMessage());
         body.put("msg", ex.getMessage());
-
         return CommApiRecordResult.builder()
         return CommApiRecordResult.builder()
-
                 .success(false)
                 .success(false)
-
                 .limitHit(false)
                 .limitHit(false)
-
                 .resultCode(500)
                 .resultCode(500)
-
                 .resultMsg(ex.getMessage())
                 .resultMsg(ex.getMessage())
-
                 .responseBody(JSON.toJSONString(body))
                 .responseBody(JSON.toJSONString(body))
-
                 .calleePhone(calleePhone)
                 .calleePhone(calleePhone)
-
                 .callerPhone(callerPhone)
                 .callerPhone(callerPhone)
-
                 .gatewayId(gatewayId)
                 .gatewayId(gatewayId)
-
                 .build();
                 .build();
-
     }
     }
 
 
-
-
     public static class CommApiRecordResult {
     public static class CommApiRecordResult {
-
         private boolean success;
         private boolean success;
-
         private boolean limitHit;
         private boolean limitHit;
-
         private Integer resultCode;
         private Integer resultCode;
-
         private String resultMsg;
         private String resultMsg;
-
         private String responseBody;
         private String responseBody;
-
         private String limitReason;
         private String limitReason;
-
         private String calleePhone;
         private String calleePhone;
-
         private String callerPhone;
         private String callerPhone;
-
         private Long gatewayId;
         private Long gatewayId;
 
 
-
-
         public static CommApiRecordResultBuilder builder() {
         public static CommApiRecordResultBuilder builder() {
-
             return new CommApiRecordResultBuilder();
             return new CommApiRecordResultBuilder();
-
         }
         }
 
 
-
-
         public boolean isSuccess() { return success; }
         public boolean isSuccess() { return success; }
-
         public boolean isLimitHit() { return limitHit; }
         public boolean isLimitHit() { return limitHit; }
-
         public Integer getResultCode() { return resultCode; }
         public Integer getResultCode() { return resultCode; }
-
         public String getResultMsg() { return resultMsg; }
         public String getResultMsg() { return resultMsg; }
-
         public String getResponseBody() { return responseBody; }
         public String getResponseBody() { return responseBody; }
-
         public String getLimitReason() { return limitReason; }
         public String getLimitReason() { return limitReason; }
-
         public String getCalleePhone() { return calleePhone; }
         public String getCalleePhone() { return calleePhone; }
-
         public String getCallerPhone() { return callerPhone; }
         public String getCallerPhone() { return callerPhone; }
-
         public Long getGatewayId() { return gatewayId; }
         public Long getGatewayId() { return gatewayId; }
 
 
-
-
         public static class CommApiRecordResultBuilder {
         public static class CommApiRecordResultBuilder {
-
             private final CommApiRecordResult target = new CommApiRecordResult();
             private final CommApiRecordResult target = new CommApiRecordResult();
 
 
             public CommApiRecordResultBuilder success(boolean success) { target.success = success; return this; }
             public CommApiRecordResultBuilder success(boolean success) { target.success = success; return this; }
-
             public CommApiRecordResultBuilder limitHit(boolean limitHit) { target.limitHit = limitHit; return this; }
             public CommApiRecordResultBuilder limitHit(boolean limitHit) { target.limitHit = limitHit; return this; }
-
             public CommApiRecordResultBuilder resultCode(Integer resultCode) { target.resultCode = resultCode; return this; }
             public CommApiRecordResultBuilder resultCode(Integer resultCode) { target.resultCode = resultCode; return this; }
-
             public CommApiRecordResultBuilder resultMsg(String resultMsg) { target.resultMsg = resultMsg; return this; }
             public CommApiRecordResultBuilder resultMsg(String resultMsg) { target.resultMsg = resultMsg; return this; }
-
             public CommApiRecordResultBuilder responseBody(String responseBody) { target.responseBody = responseBody; return this; }
             public CommApiRecordResultBuilder responseBody(String responseBody) { target.responseBody = responseBody; return this; }
-
             public CommApiRecordResultBuilder limitReason(String limitReason) { target.limitReason = limitReason; return this; }
             public CommApiRecordResultBuilder limitReason(String limitReason) { target.limitReason = limitReason; return this; }
-
             public CommApiRecordResultBuilder calleePhone(String calleePhone) { target.calleePhone = calleePhone; return this; }
             public CommApiRecordResultBuilder calleePhone(String calleePhone) { target.calleePhone = calleePhone; return this; }
-
             public CommApiRecordResultBuilder callerPhone(String callerPhone) { target.callerPhone = callerPhone; return this; }
             public CommApiRecordResultBuilder callerPhone(String callerPhone) { target.callerPhone = callerPhone; return this; }
-
             public CommApiRecordResultBuilder gatewayId(Long gatewayId) { target.gatewayId = gatewayId; return this; }
             public CommApiRecordResultBuilder gatewayId(Long gatewayId) { target.gatewayId = gatewayId; return this; }
-
             public CommApiRecordResult build() { return target; }
             public CommApiRecordResult build() { return target; }
-
         }
         }
-
     }
     }
-
 }
 }
-
-

+ 144 - 0
fs-comm-gateway/src/main/java/com/fs/comm/sms/MyCommSmsProvider.java

@@ -0,0 +1,144 @@
+package com.fs.comm.sms;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.vo.SmsSendItemVO;
+import com.fs.common.vo.SmsSendVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 迈远(my)短信 HTTP 发送实现。
+ * 对接 company_sms_api / company_sms_api_port 配置的 url、account、password、sign、extno。
+ */
+@Slf4j
+@Service
+public class MyCommSmsProvider implements CommSmsProvider {
+
+    private static final String PROVIDER = "my";
+
+    @Override
+    public String provider() {
+        return PROVIDER;
+    }
+
+    @Override
+    public CommSmsChannelResult send(CommSmsChannelRequest request) {
+        String validateError = validate(request);
+        if (validateError != null) {
+            return CommSmsChannelResult.fail(validateError, null);
+        }
+
+        String sendUrl;
+        try {
+            sendUrl = buildSendUrl(request);
+        } catch (UnsupportedEncodingException e) {
+            log.error("MyCommSmsProvider URL编码异常 phone={}", request.getPhone(), e);
+            return CommSmsChannelResult.fail("ENCODE_ERROR", null);
+        }
+
+        String responseBody;
+        try {
+            responseBody = HttpRequest.get(sendUrl).timeout(15000).execute().body();
+        } catch (Exception e) {
+            log.error("MyCommSmsProvider HTTP请求异常 phone={}, url={}", request.getPhone(), maskUrl(sendUrl), e);
+            return CommSmsChannelResult.fail("HTTP_ERROR", e.getMessage());
+        }
+
+        return parseResponse(responseBody, request.getPhone());
+    }
+
+    private String validate(CommSmsChannelRequest request) {
+        if (request == null) {
+            return "INVALID_REQUEST";
+        }
+        if (StringUtils.isBlank(request.getPhone())) {
+            return "INVALID_PHONE";
+        }
+        if (StringUtils.isBlank(request.getUrl())) {
+            return "INVALID_URL";
+        }
+        if (StringUtils.isBlank(request.getAccount()) || StringUtils.isBlank(request.getPassword())) {
+            return "INVALID_CREDENTIAL";
+        }
+        if (request.getTempType() == null) {
+            return "UNSUPPORTED_TEMP_TYPE";
+        }
+        if (!Integer.valueOf(1).equals(request.getTempType()) && !Integer.valueOf(2).equals(request.getTempType())) {
+            return "UNSUPPORTED_TEMP_TYPE";
+        }
+        return null;
+    }
+
+    /**
+     * 迈远发送 URL:{url}sms?action=send&account=...&password=...&mobile=...&content=...&extno=...&rt=json
+     */
+    String buildSendUrl(CommSmsChannelRequest request) throws UnsupportedEncodingException {
+        String sign = StringUtils.defaultString(request.getSign());
+        String body = request.getContent();
+        if (Integer.valueOf(2).equals(request.getTempType())) {
+            body = body + "拒收请回复R";
+        }
+        String encodedContent = URLEncoder.encode(sign + body, StandardCharsets.UTF_8.name());
+        String baseUrl = normalizeBaseUrl(request.getUrl());
+        return baseUrl + "sms?action=send"
+                + "&account=" + request.getAccount()
+                + "&password=" + request.getPassword()
+                + "&mobile=" + request.getPhone()
+                + "&content=" + encodedContent
+                + "&extno=" + StringUtils.defaultString(request.getExtno())
+                + "&rt=json";
+    }
+
+    private CommSmsChannelResult parseResponse(String responseBody, String phone) {
+        if (StringUtils.isBlank(responseBody)) {
+            return CommSmsChannelResult.fail("EMPTY_RESPONSE", null);
+        }
+        try {
+            SmsSendVO vo = JSONUtil.toBean(responseBody, SmsSendVO.class);
+            if (vo == null || vo.getStatus() == null || !Integer.valueOf(0).equals(vo.getStatus())) {
+                log.warn("MyCommSmsProvider 发送失败 phone={}, response={}", phone, abbreviate(responseBody));
+                return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+            }
+            if (vo.getList() == null) {
+                return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+            }
+            for (SmsSendItemVO item : vo.getList()) {
+                if (item != null && "0".equals(item.getResult())) {
+                    return CommSmsChannelResult.ok(item.getMid());
+                }
+            }
+            log.warn("MyCommSmsProvider 无成功条目 phone={}, response={}", phone, abbreviate(responseBody));
+            return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+        } catch (Exception e) {
+            log.error("MyCommSmsProvider 响应解析异常 phone={}, response={}", phone, abbreviate(responseBody), e);
+            return CommSmsChannelResult.fail("PARSE_ERROR", abbreviate(responseBody));
+        }
+    }
+
+    private String normalizeBaseUrl(String url) {
+        if (StringUtils.isBlank(url)) {
+            return "";
+        }
+        return url.endsWith("/") ? url : url + "/";
+    }
+
+    private String maskUrl(String url) {
+        if (StringUtils.isBlank(url)) {
+            return url;
+        }
+        return url.replaceAll("password=[^&]*", "password=***");
+    }
+
+    private String abbreviate(String text) {
+        if (text == null) {
+            return null;
+        }
+        return text.length() > 500 ? text.substring(0, 500) : text;
+    }
+}

+ 22 - 0
fs-comm-gateway/对接文档.md

@@ -302,6 +302,28 @@ Content-Type: application/json
 
 
 发送结果异步写入租户库 `company_voice_robotic_call_log_sendmsg`(status:1 进行中 / 2 成功 / 3 失败)。
 发送结果异步写入租户库 `company_voice_robotic_call_log_sendmsg`(status:1 进行中 / 2 成功 / 3 失败)。
 
 
+**迈远(provider=my)发送说明:**
+
+网关进程内由 `MyCommSmsProvider` 负责实际 HTTP 下发,不再读取旧的 `his.sms` 全局配置,而是按租户在 Admin 配置的接口表路由:
+
+| 配置表 | 字段 | 说明 |
+|--------|------|------|
+| `company_sms_api` | `provider=my` | 固定为迈远 |
+| | `url` | 接口根地址 |
+| | `account` / `password` | 账户密码 |
+| | `sign` | 短信签名 |
+| `company_sms_api_port` | `port_no` | 迈远扩展码 `extno` |
+| | `account` / `password` / `sign` | 可选,覆盖接口级配置 |
+
+请求 URL 格式(与旧版 `sendCaptcha` 一致):
+
+```
+{url}sms?action=send&account={account}&password={password}&mobile={phone}&content={URLEncode(sign+content)}&extno={extno}&rt=json
+```
+
+- 模板类型 `tempType=1`(行业通知):内容为 `sign + content`
+- 模板类型 `tempType=2`(营销):内容为 `sign + content + 拒收请回复R`
+
 ---
 ---
 
 
 ### 5.3 查询外呼记录
 ### 5.3 查询外呼记录

+ 6 - 0
fs-service/src/main/java/com/fs/comm/domain/CommGatewayApiLog.java

@@ -38,6 +38,12 @@ public class CommGatewayApiLog extends BaseEntity {
     private String callerPhone;
     private String callerPhone;
     private Long gatewayId;
     private Long gatewayId;
     private BigDecimal billingAmount;
     private BigDecimal billingAmount;
+    /** 成本价(单价) */
+    private BigDecimal costPrice;
+    /** 计算价/售价(单价) */
+    private BigDecimal calcPrice;
+    /** 计费数量 */
+    private Integer billingQuantity;
     private String billingUnit;
     private String billingUnit;
     private String clientIp;
     private String clientIp;
     private String authScope;
     private String authScope;

+ 26 - 0
fs-service/src/main/java/com/fs/comm/model/CommGatewayBillingQuote.java

@@ -0,0 +1,26 @@
+package com.fs.comm.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 通讯网关单次调用计费报价
+ */
+@Data
+@Builder
+public class CommGatewayBillingQuote {
+
+    /** 成本价(单价,元) */
+    private BigDecimal costPrice;
+
+    /** 计算价/售价(单价,元) */
+    private BigDecimal calcPrice;
+
+    /** 计费数量 */
+    private Integer billingQuantity;
+
+    /** 计费总额 = calcPrice × billingQuantity */
+    private BigDecimal billingAmount;
+}

+ 156 - 0
fs-service/src/main/java/com/fs/comm/service/CommGatewayBillingService.java

@@ -0,0 +1,156 @@
+package com.fs.comm.service;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.comm.model.CommGatewayBillingQuote;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
+import com.fs.proxy.domain.CompanySmsApiTenant;
+import com.fs.proxy.domain.ServiceFeeConfig;
+import com.fs.proxy.domain.TenantTrafficPricing;
+import com.fs.proxy.enums.ConsumeTypeEnum;
+import com.fs.proxy.mapper.CompanySmsApiTenantMapper;
+import com.fs.proxy.mapper.TenantTrafficPricingMapper;
+import com.fs.proxy.service.BalanceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 通讯网关计费报价(对齐 BalanceServiceImpl 定价规则)
+ */
+@Slf4j
+@Service
+public class CommGatewayBillingService {
+
+    @Value("${comm.gateway.call-unit-price:0.12}")
+    private BigDecimal defaultCallCalcPrice;
+
+    @Value("${comm.gateway.sms-unit-price:0.05}")
+    private BigDecimal defaultSmsCalcPrice;
+
+    @Autowired
+    private BalanceService balanceService;
+
+    @Autowired
+    private CompanySmsApiTenantMapper smsApiTenantMapper;
+
+    @Autowired
+    private CompanyVoiceApiTenantMapper voiceApiTenantMapper;
+
+    @Autowired
+    private TenantTrafficPricingMapper trafficPricingMapper;
+
+    @DataSource(DataSourceType.MASTER)
+    public CommGatewayBillingQuote resolveQuote(Long tenantId, String apiType, int quantity) {
+        if (quantity <= 0) {
+            quantity = 1;
+        }
+        if (CommGatewayApiLog.API_TYPE_SMS.equals(apiType)) {
+            return buildQuote(resolveSmsUnitPrice(tenantId), quantity);
+        }
+        if (CommGatewayApiLog.API_TYPE_CALL.equals(apiType)) {
+            return buildQuote(resolveCallUnitPrice(tenantId), quantity);
+        }
+        return zeroQuote();
+    }
+
+    private CommGatewayBillingQuote buildQuote(UnitPricePair pair, int quantity) {
+        BigDecimal calcPrice = pair.calcPrice != null ? pair.calcPrice : BigDecimal.ZERO;
+        BigDecimal costPrice = pair.costPrice != null ? pair.costPrice : BigDecimal.ZERO;
+        return CommGatewayBillingQuote.builder()
+                .costPrice(costPrice)
+                .calcPrice(calcPrice)
+                .billingQuantity(quantity)
+                .billingAmount(calcPrice.multiply(BigDecimal.valueOf(quantity)))
+                .build();
+    }
+
+    private CommGatewayBillingQuote zeroQuote() {
+        return CommGatewayBillingQuote.builder()
+                .costPrice(BigDecimal.ZERO)
+                .calcPrice(BigDecimal.ZERO)
+                .billingQuantity(0)
+                .billingAmount(BigDecimal.ZERO)
+                .build();
+    }
+
+    private UnitPricePair resolveSmsUnitPrice(Long tenantId) {
+        ServiceFeeConfig config = balanceService.getFeeConfig(ConsumeTypeEnum.SMS_SEND.getCode());
+        BigDecimal calcPrice = config != null && config.getFeeStandard() != null
+                ? config.getFeeStandard() : defaultSmsCalcPrice;
+        BigDecimal costPrice = config != null && config.getPlatformCost() != null
+                ? config.getPlatformCost() : BigDecimal.ZERO;
+
+        if (tenantId != null) {
+            List<CompanySmsApiTenant> bindings = smsApiTenantMapper.selectByCompanyId(tenantId);
+            if (bindings != null) {
+                for (CompanySmsApiTenant binding : bindings) {
+                    if (!Integer.valueOf(1).equals(binding.getStatus())) {
+                        continue;
+                    }
+                    if (binding.getPrice() != null && binding.getPrice().compareTo(BigDecimal.ZERO) > 0) {
+                        calcPrice = binding.getPrice();
+                    }
+                    if (binding.getCostPrice() != null) {
+                        costPrice = binding.getCostPrice();
+                    }
+                    break;
+                }
+            }
+        }
+        return new UnitPricePair(costPrice, calcPrice);
+    }
+
+    private UnitPricePair resolveCallUnitPrice(Long tenantId) {
+        ServiceFeeConfig config = balanceService.getFeeConfig(ConsumeTypeEnum.AI_CALL.getCode());
+        BigDecimal calcPrice = config != null && config.getFeeStandard() != null
+                ? config.getFeeStandard() : defaultCallCalcPrice;
+        BigDecimal costPrice = config != null && config.getPlatformCost() != null
+                ? config.getPlatformCost() : BigDecimal.ZERO;
+
+        if (tenantId != null) {
+            List<CompanyVoiceApiTenant> voiceBindings = voiceApiTenantMapper.selectEnabledApisByTenantId(tenantId);
+            if (voiceBindings != null && !voiceBindings.isEmpty()) {
+                CompanyVoiceApiTenant voiceBinding = voiceBindings.get(0);
+                if (voiceBinding.getSalePrice() != null && voiceBinding.getSalePrice().compareTo(BigDecimal.ZERO) > 0) {
+                    BigDecimal voicePrice = voiceBinding.getSalePrice();
+                    BigDecimal voiceCost = voiceBinding.getCostPrice() != null ? voiceBinding.getCostPrice() : BigDecimal.ZERO;
+
+                    BigDecimal aiSurcharge = config != null && config.getFeeStandard() != null
+                            ? config.getFeeStandard() : defaultCallCalcPrice;
+                    BigDecimal aiCost = config != null && config.getPlatformCost() != null
+                            ? config.getPlatformCost() : BigDecimal.ZERO;
+
+                    TenantTrafficPricing aiTrafficPricing = trafficPricingMapper
+                            .selectByTenantAndType(tenantId, ConsumeTypeEnum.AI_CALL.getCode());
+                    if (aiTrafficPricing != null && aiTrafficPricing.getPrice() != null
+                            && aiTrafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
+                        aiSurcharge = aiTrafficPricing.getPrice();
+                        if (aiTrafficPricing.getCostPrice() != null) {
+                            aiCost = aiTrafficPricing.getCostPrice();
+                        }
+                    }
+                    calcPrice = voicePrice.add(aiSurcharge);
+                    costPrice = voiceCost.add(aiCost);
+                }
+            }
+        }
+        return new UnitPricePair(costPrice, calcPrice);
+    }
+
+    private static class UnitPricePair {
+        private final BigDecimal costPrice;
+        private final BigDecimal calcPrice;
+
+        private UnitPricePair(BigDecimal costPrice, BigDecimal calcPrice) {
+            this.costPrice = costPrice;
+            this.calcPrice = calcPrice;
+        }
+    }
+}

+ 36 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelRequest.java

@@ -0,0 +1,36 @@
+package com.fs.comm.sms;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 短信通道发送请求(迈远等 HTTP 通道共用)
+ */
+@Data
+@Builder
+public class CommSmsChannelRequest {
+
+    /** 目标手机号 */
+    private String phone;
+
+    /** 短信正文(不含签名) */
+    private String content;
+
+    /** 模板类型: 1=行业通知 2=营销短信 */
+    private Integer tempType;
+
+    /** 账户 */
+    private String account;
+
+    /** 密码 */
+    private String password;
+
+    /** 短信签名 */
+    private String sign;
+
+    /** 接口根地址(迈远,需以 / 结尾或不含路径) */
+    private String url;
+
+    /** 扩展码 extno */
+    private String extno;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelResult.java

@@ -0,0 +1,40 @@
+package com.fs.comm.sms;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 短信通道发送结果
+ */
+@Data
+@Builder
+public class CommSmsChannelResult {
+
+    /** 是否发送成功 */
+    private boolean success;
+
+    /** 平台消息 ID(迈远 mid) */
+    private String mid;
+
+    /** 失败原因码,成功时为 OK */
+    private String errorCode;
+
+    /** 原始响应摘要(便于排查) */
+    private String rawResponse;
+
+    public static CommSmsChannelResult ok(String mid) {
+        return CommSmsChannelResult.builder()
+                .success(true)
+                .mid(mid)
+                .errorCode("OK")
+                .build();
+    }
+
+    public static CommSmsChannelResult fail(String errorCode, String rawResponse) {
+        return CommSmsChannelResult.builder()
+                .success(false)
+                .errorCode(errorCode)
+                .rawResponse(rawResponse)
+                .build();
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsProvider.java

@@ -0,0 +1,12 @@
+package com.fs.comm.sms;
+
+/**
+ * 短信通道发送 SPI(由 fs-comm-gateway 等模块提供具体实现)
+ */
+public interface CommSmsProvider {
+
+    /** 服务商标识,如 my / card */
+    String provider();
+
+    CommSmsChannelResult send(CommSmsChannelRequest request);
+}

+ 44 - 3
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -7,6 +7,9 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.proxy.enums.ConsumeTypeEnum;
 import com.fs.proxy.enums.ConsumeTypeEnum;
 import com.fs.proxy.service.BalanceService;
 import com.fs.proxy.service.BalanceService;
+import com.fs.comm.sms.CommSmsChannelRequest;
+import com.fs.comm.sms.CommSmsChannelResult;
+import com.fs.comm.sms.CommSmsProvider;
 import com.fs.common.service.ISmsService;
 import com.fs.common.service.ISmsService;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsNotifyVO;
@@ -52,6 +55,7 @@ import org.springframework.transaction.annotation.Transactional;
 import java.io.UnsupportedEncodingException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.text.SimpleDateFormat;
+import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
@@ -113,6 +117,9 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     @Autowired
     private com.fs.proxy.mapper.CompanySmsCardMapper smsCardMapper;
     private com.fs.proxy.mapper.CompanySmsCardMapper smsCardMapper;
 
 
+    @Autowired(required = false)
+    private List<CommSmsProvider> commSmsProviders = Collections.emptyList();
+
     /**
     /**
      * 统一发送方法 - 替代原来6处硬编码的 his.sms 配置读取
      * 统一发送方法 - 替代原来6处硬编码的 his.sms 配置读取
      * 
      * 
@@ -167,10 +174,45 @@ public class SmsServiceImpl implements ISmsService
         }
         }
     }
     }
 
 
-    /** 迈远发送 */
+    /** 迈远发送(优先走 fs-comm-gateway 注册的 CommSmsProvider) */
     private String sendByRf(String phone, String content, Integer tempType,
     private String sendByRf(String phone, String content, Integer tempType,
                              String account, String password, String sign, String url, String extno,
                              String account, String password, String sign, String url, String extno,
                              Long tenantId, Long apiId, Long portId) {
                              Long tenantId, Long apiId, Long portId) {
+        CommSmsProvider myProvider = findCommSmsProvider("my");
+        if (myProvider != null) {
+            CommSmsChannelResult result = myProvider.send(CommSmsChannelRequest.builder()
+                    .phone(phone)
+                    .content(content)
+                    .tempType(tempType)
+                    .account(account)
+                    .password(password)
+                    .sign(sign)
+                    .url(url)
+                    .extno(extno)
+                    .build());
+            if (result.isSuccess()) {
+                return "OK";
+            }
+            return StringUtils.defaultIfBlank(result.getErrorCode(), "SEND_FAILED");
+        }
+        return sendByRfFallback(phone, content, tempType, account, password, sign, url, extno);
+    }
+
+    private CommSmsProvider findCommSmsProvider(String provider) {
+        if (commSmsProviders == null || commSmsProviders.isEmpty()) {
+            return null;
+        }
+        for (CommSmsProvider commSmsProvider : commSmsProviders) {
+            if (provider.equals(commSmsProvider.provider())) {
+                return commSmsProvider;
+            }
+        }
+        return null;
+    }
+
+    /** 非网关进程兜底:本地直连迈远 HTTP */
+    private String sendByRfFallback(String phone, String content, Integer tempType,
+                                    String account, String password, String sign, String url, String extno) {
         String urls;
         String urls;
         try {
         try {
             if (tempType.equals(1)) {
             if (tempType.equals(1)) {
@@ -185,7 +227,7 @@ public class SmsServiceImpl implements ISmsService
                 return "UNSUPPORTED_TEMP_TYPE";
                 return "UNSUPPORTED_TEMP_TYPE";
             }
             }
         } catch (UnsupportedEncodingException e) {
         } catch (UnsupportedEncodingException e) {
-            log.error("sendByRf: URL编码异常", e);
+            log.error("sendByRfFallback: URL编码异常", e);
             return "ENCODE_ERROR";
             return "ENCODE_ERROR";
         }
         }
 
 
@@ -194,7 +236,6 @@ public class SmsServiceImpl implements ISmsService
         if (vo.getStatus().equals(0)) {
         if (vo.getStatus().equals(0)) {
             for (SmsSendItemVO itemVO : vo.getList()) {
             for (SmsSendItemVO itemVO : vo.getList()) {
                 if (itemVO.getResult().equals("0")) {
                 if (itemVO.getResult().equals("0")) {
-                    // 发送成功, 返回OK (调用方负责写日志)
                     return "OK";
                     return "OK";
                 }
                 }
             }
             }

+ 36 - 0
fs-service/src/main/resources/db/20250603-comm-gateway-api-log-billing-price.sql

@@ -0,0 +1,36 @@
+-- 通讯网关 API 调用日志:补充成本价、计算价字段(主库存量库执行,幂等)
+SET @dbname = DATABASE();
+SET @tablename = 'comm_gateway_api_log';
+
+SET @columnname = 'cost_price';
+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 comm_gateway_api_log ADD COLUMN `cost_price` DECIMAL(12, 4) DEFAULT 0.0000 COMMENT \"成本价(单价)\" AFTER `billing_amount`'
+));
+PREPARE alterIfNotExists FROM @preparedStatement;
+EXECUTE alterIfNotExists;
+DEALLOCATE PREPARE alterIfNotExists;
+
+SET @columnname = 'calc_price';
+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 comm_gateway_api_log ADD COLUMN `calc_price` DECIMAL(12, 4) DEFAULT 0.0000 COMMENT \"计算价/售价(单价)\" AFTER `cost_price`'
+));
+PREPARE alterIfNotExists FROM @preparedStatement;
+EXECUTE alterIfNotExists;
+DEALLOCATE PREPARE alterIfNotExists;
+
+SET @columnname = 'billing_quantity';
+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 comm_gateway_api_log ADD COLUMN `billing_quantity` INT DEFAULT 1 COMMENT \"计费数量\" AFTER `calc_price`'
+));
+PREPARE alterIfNotExists FROM @preparedStatement;
+EXECUTE alterIfNotExists;
+DEALLOCATE PREPARE alterIfNotExists;

+ 3 - 0
fs-service/src/main/resources/db/20250603-comm-gateway-api-log.sql

@@ -18,6 +18,9 @@ CREATE TABLE IF NOT EXISTS `comm_gateway_api_log` (
     `caller_phone`     VARCHAR(32)    DEFAULT NULL COMMENT '主叫号码/线路标识',
     `caller_phone`     VARCHAR(32)    DEFAULT NULL COMMENT '主叫号码/线路标识',
     `gateway_id`       BIGINT         DEFAULT NULL COMMENT '外呼线路ID',
     `gateway_id`       BIGINT         DEFAULT NULL COMMENT '外呼线路ID',
     `billing_amount`   DECIMAL(12, 4) DEFAULT 0.0000 COMMENT '计费金额',
     `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',
     `billing_unit`     VARCHAR(20)    DEFAULT NULL COMMENT '计费单位 call/sms',
     `client_ip`        VARCHAR(64)    DEFAULT NULL COMMENT '客户端IP',
     `client_ip`        VARCHAR(64)    DEFAULT NULL COMMENT '客户端IP',
     `auth_scope`       VARCHAR(20)    DEFAULT NULL COMMENT '鉴权范围 external/internal',
     `auth_scope`       VARCHAR(20)    DEFAULT NULL COMMENT '鉴权范围 external/internal',

+ 9 - 5
fs-service/src/main/resources/mapper/comm/CommGatewayApiLogMapper.xml

@@ -21,6 +21,9 @@
         <result property="callerPhone" column="caller_phone"/>
         <result property="callerPhone" column="caller_phone"/>
         <result property="gatewayId" column="gateway_id"/>
         <result property="gatewayId" column="gateway_id"/>
         <result property="billingAmount" column="billing_amount"/>
         <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="billingUnit" column="billing_unit"/>
         <result property="clientIp" column="client_ip"/>
         <result property="clientIp" column="client_ip"/>
         <result property="authScope" column="auth_scope"/>
         <result property="authScope" column="auth_scope"/>
@@ -33,7 +36,8 @@
         select l.log_id, l.tenant_id, l.company_id, l.company_user_id, l.caller_account,
         select l.log_id, l.tenant_id, l.company_id, l.company_user_id, l.caller_account,
                l.api_type, l.api_path, l.request_body, l.response_body, l.result_code, l.result_msg,
                l.api_type, l.api_path, l.request_body, l.response_body, l.result_code, l.result_msg,
                l.success, l.limit_hit, l.limit_reason, l.callee_phone, l.caller_phone, l.gateway_id,
                l.success, l.limit_hit, l.limit_reason, l.callee_phone, l.caller_phone, l.gateway_id,
-               l.billing_amount, l.billing_unit, l.client_ip, l.auth_scope, l.duration_ms, l.create_time,
+               l.billing_amount, l.cost_price, l.calc_price, l.billing_quantity, l.billing_unit,
+               l.client_ip, l.auth_scope, l.duration_ms, l.create_time,
                c.company_name
                c.company_name
         from comm_gateway_api_log l
         from comm_gateway_api_log l
         left join company c on c.company_id = l.company_id
         left join company c on c.company_id = l.company_id
@@ -68,13 +72,13 @@
         insert into comm_gateway_api_log
         insert into comm_gateway_api_log
         (tenant_id, company_id, company_user_id, caller_account, api_type, api_path,
         (tenant_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,
          request_body, response_body, result_code, result_msg, success, limit_hit, limit_reason,
-         callee_phone, caller_phone, gateway_id, billing_amount, billing_unit, client_ip,
-         auth_scope, duration_ms, create_time)
+         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
         values
         (#{tenantId}, #{companyId}, #{companyUserId}, #{callerAccount}, #{apiType}, #{apiPath},
         (#{tenantId}, #{companyId}, #{companyUserId}, #{callerAccount}, #{apiType}, #{apiPath},
          #{requestBody}, #{responseBody}, #{resultCode}, #{resultMsg}, #{success}, #{limitHit}, #{limitReason},
          #{requestBody}, #{responseBody}, #{resultCode}, #{resultMsg}, #{success}, #{limitHit}, #{limitReason},
-         #{calleePhone}, #{callerPhone}, #{gatewayId}, #{billingAmount}, #{billingUnit}, #{clientIp},
-         #{authScope}, #{durationMs}, #{createTime})
+         #{calleePhone}, #{callerPhone}, #{gatewayId}, #{billingAmount}, #{costPrice}, #{calcPrice}, #{billingQuantity},
+         #{billingUnit}, #{clientIp}, #{authScope}, #{durationMs}, #{createTime})
     </insert>
     </insert>
 
 
     <select id="countByCalleePhone" resultType="java.lang.Integer">
     <select id="countByCalleePhone" resultType="java.lang.Integer">