fs-comm-gateway 是 SaaS 平台统一的外呼 / 短信通讯网关,对外提供标准化 HTTP API,对内供工作流引擎、业务服务通过 CommGatewayClient 调用。
主要能力:
| 能力 | 说明 |
|---|---|
| AI 外呼 | 对接 EasyCallCenter365,创建任务、追加名单、启动外呼 |
| 短信发送 | 基于租户短信模板与余额,批量发送 AI 短信 |
| 工作流回调 | 接收 EasyCall / 短信平台回调,续跑阻塞节点 |
| 外呼记录查询 | 按 callBackUuid 查询外呼日志 |
默认服务端口:8010
建议通过 Nginx 反向代理暴露:/comm/ → http://127.0.0.1:8010/comm/
网关运行依赖以下基础设施(由运维按环境配置):
| 组件 | 用途 |
|---|---|
| MySQL 主库 | 租户信息、公司账号鉴权 |
| MySQL 租户库 | 业务数据(被叫人、模板、日志等) |
| Redis | Token 会话、限流、工作流回调上下文 |
| EasyCallCenter365 | 实际外呼平台,easycall.base-url 配置 |
网关支持两种调用身份,二选一即可访问业务接口(除白名单路径外)。
POST /comm/auth/token
Content-Type: application/json
请求体:
{
"tenantCode": "your_tenant_code",
"account": "company_user_account",
"password": "plain_password"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| tenantCode | string | 是 | 租户编码 |
| account | string | 是 | 公司端登录账号 |
| password | string | 是 | 登录密码(BCrypt 校验) |
成功响应:
{
"code": 200,
"msg": "success",
"data": {
"accessToken": "eyJhbGciOiJIUzUxMiJ9...",
"expiresIn": 7200,
"tokenType": "Bearer",
"tenantId": 33,
"companyId": 1001,
"companyUserId": 2001
}
}
| 字段 | 说明 |
|---|---|
| accessToken | JWT,后续请求携带 |
| expiresIn | 有效秒数(默认 120 分钟,见 token.expireTime) |
| tokenType | 固定 Bearer |
| tenantId / companyId / companyUserId | 当前会话绑定的租户与公司上下文 |
Authorization: Bearer {accessToken}
Content-Type: application/json
POST /comm/auth/refresh
Authorization: Bearer {accessToken}
POST /comm/auth/logout
Authorization: Bearer {accessToken}
平台内部服务(如工作流引擎)通过 CommGatewayClient 调用,无需 JWT,使用共享密钥 + 租户/公司头:
| 请求头 | 必填 | 说明 |
|---|---|---|
| X-Comm-Internal-Secret | 是 | 与配置 comm.gateway.internal-secret 一致 |
| X-Comm-Tenant-Id | 是 | 租户 ID |
| X-Comm-Company-Id | 是 | 公司 ID |
| X-Comm-Company-User-Id | 否 | 公司用户 ID |
配置项(调用方 application.yml):
comm:
gateway:
base-url: http://127.0.0.1:8010
internal-secret: ${COMM_INTERNAL_SECRET}
enabled: true
fallback-local: false # 网关失败时是否降级本地直连
以下路径不需要 Token 或 Internal Secret:
| 路径 | 说明 |
|---|---|
POST /comm/auth/token |
登录换 Token |
POST /comm/callback/easycall |
EasyCall 外呼结果回调(含 IP 白名单校验) |
POST /comm/callback/sms |
短信回执回调 |
GET /comm/softphone/ws |
软电话 ccPhoneBar WebSocket 握手(HTTP 层免 JWT,见 3.4) |
ccPhoneBar 控制通道可走 comm-gateway WebSocket 代理(开发环境 ws-proxy-url 配置)。采用双层鉴权:
| 层级 | 凭证 | 说明 |
|---|---|---|
| 网关层(前端携带) | wsTicket |
8006 调用 8010 内部接口申请,Redis 短时有效(默认 60s),握手一次性消费 |
| 上游 IPCC(网关内部) | loginToken |
出票时由 8006 写入票据,前端不感知;8010 验票后透传 IPCC |
POST /comm/softphone/ws-ticket
Content-Type: application/json
X-Comm-Internal-Secret: {internal-secret}
X-Comm-Tenant-Id: {tenantId}
X-Comm-Company-Id: {companyId}
X-Comm-Company-User-Id: {companyUserId}
请求体:
{
"loginToken": "easycall_login_token",
"extNum": "1001"
}
成功响应:
{
"code": 200,
"msg": "success",
"data": {
"wsTicket": "uuid",
"expiresIn": 60
}
}
业务服务在 getToolbarBasicParam 合并配置时自动调用(CommGatewayClient.issueSoftPhoneWsTicket),前端从接口响应读取 wsTicket(不返回 loginToken)。
ws://{gateway}/comm/softphone/ws?wsTicket={wsTicket}
wsTicket 或票据过期/已使用时,握手被拒绝。loginToken 仅存在于服务端票据中,由网关连接上游 IPCC 时自动附带。Unexpected response code: 200(响应头含 Vary: Origin),根因是 fs-framework 的 ResourcesConfig 对 /** 注册了全局 CorsFilter,阻断了 WebSocket Upgrade。comm-gateway 启动类已排除 ResourcesConfig,并由 CommGatewayCorsFilter 对 /comm/softphone/ws 强制绕过 CORS。修改后须重启 8010。comm:
gateway:
softphone:
ws-ticket-ttl-seconds: 60 # 8010 票据 TTL
ai-sip-call:
softphone:
ws-proxy-url: ws://127.0.0.1:8010/comm/softphone/ws # 8006 下发给前端;为空则直连 IPCC
业务 Controller 统一返回 CommApiResult:
{
"code": 200,
"msg": "success",
"data": { }
}
| code | 含义 |
|---|---|
| 200 | 成功 |
| 401 | 未认证(如查询接口未带 Token) |
| 404 | 资源不存在 |
| 500 | 业务失败或系统异常 |
判定规则:
code 字段 判断业务成败。code != 200 时,msg 为失败原因,对接方应记录并向上游返回失败。常见失败 msg 示例:
| msg | 场景 |
|---|---|
| 被叫人手机号解密失败或号码无效 | 被叫号码为空或解密失败 |
| 被叫人命中外呼黑名单 | 黑名单拦截 |
| 外呼名单追加失败或线路限流 | EasyCall 未追加名单或线路限流 |
| 成功追加0个名单 | EasyCall 返回 0 条(号码无效等) |
| 无权使用该外呼线路 | gatewayId 不在公司可用线路内 |
| 租户请求频率超限,请稍后重试 | 触发 QPS 限流 |
| 剩余短信数量不足,请充值 | 短信余额不足 |
| 短信模板不存在或未审核 | 模板无效 |
注意:Filter 层未授权时返回
AjaxResult格式(code: 401),与CommApiResult字段结构一致。
POST /comm/call/send
Authorization: Bearer {accessToken}
Content-Type: application/json
请求体:
{
"calleeId": 25,
"roboticId": 174,
"gatewayId": 5,
"businessId": null,
"nodeKey": "call_node_1",
"workflowInstanceId": "wf-instance-uuid",
"callbackUrl": "",
"phone": null,
"llmAccountId": 1,
"voiceCode": "xiaoyun",
"voiceSource": "ali",
"busiGroupId": 10,
"maxConcurrency": 1,
"bizParams": {}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| calleeId | long | 是 | 被叫人 ID(company_voice_robotic_callees.id) |
| roboticId | long | 是 | AI 外呼任务 ID |
| gatewayId | long | 是 | EasyCall 外呼线路 ID |
| businessId | long | 否 | 商机 ID,传入时校验当日拨打次数上限 |
| nodeKey | string | 工作流场景必填 | 工作流节点 Key |
| workflowInstanceId | string | 工作流场景必填 | 工作流实例 ID |
| callbackUrl | string | 否 | 自定义 EasyCall 回调地址,空则读租户/公司配置 |
| phone | string | 否 | 指定被叫号码(明文或 AES 密文);为空则从 calleeId 解析 |
| llmAccountId / voiceCode / voiceSource / busiGroupId / maxConcurrency | - | 否 | AI 外呼扩展参数(工作流节点配置透传) |
| bizParams | object | 否 | 追加到 EasyCall bizJson 的自定义字段 |
成功响应 data:
{
"callBackUuid": "4ca54a59-fcdf-4c55-8783-1112dd3405cf",
"batchId": 159576,
"phone": "13800138000"
}
| 字段 | 说明 |
|---|---|
| callBackUuid | 本次外呼唯一标识,用于查询与回调关联 |
| batchId | EasyCall 任务批次 ID |
| phone | 实际外呼号码(明文) |
处理流程简述:
easycall:workflow:callback:{callBackUuid})addCallList + startTaskcompany_voice_robotic_call_log_callphonePOST /comm/sms/send
Authorization: Bearer {accessToken}
Content-Type: application/json
请求体:
{
"roboticId": 174,
"calleeId": 25,
"smsTempId": 88,
"nodeKey": "sms_node_1",
"workflowInstanceId": "wf-instance-uuid",
"phone": null,
"customerId": null,
"companyUserId": null,
"senderName": null,
"cardUrl": null,
"templateParams": {}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| roboticId | long | 是 | AI 任务 ID |
| calleeId | long | 是 | 被叫人 ID |
| smsTempId | long | 是 | 短信模板 ID(须已审核且启用) |
| nodeKey | string | 工作流场景建议填 | 节点 Key |
| workflowInstanceId | string | 工作流场景建议填 | 工作流实例 ID |
| phone | string | 否 | 指定手机号,默认取客户 mobile |
| customerId | long | 否 | 客户 ID,默认取 callee 关联 userId |
| companyUserId / senderName | - | 否 | 发送销售信息,空则自动从微信绑定关系解析 |
| cardUrl | string | 否 | 卡片链接 |
| templateParams | map | 否 | 模板变量(预留) |
成功响应 data:
{
"callbackUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customerId": 10001,
"phone": "13800138000"
}
发送结果异步写入租户库 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 + contenttempType=2(营销):内容为 sign + content + 拒收请回复RGET /comm/query/call/{callBackUuid}
Authorization: Bearer {accessToken}
成功响应 data: 外呼日志对象(company_voice_robotic_call_log_callphone 表字段 JSON 化),含 status、result、callTime、intention 等。
失败示例:
{
"code": 404,
"msg": "未找到外呼记录",
"data": null
}
POST /comm/callback/easycall
Content-Type: application/json
"success",非法 IP 返回 "illegal IP"@CallbackIpCheck 校验来源 IP 是否在租户 cId.config.legalIPs 白名单内bizJson.tenantId 切换租户库,更新外呼日志并续跑工作流阻塞节点EasyCall 侧需配置的回调地址示例:
https://{your-domain}/comm/callback/easycall
bizJson 中需包含(网关外呼时自动写入):
{
"tenantId": 33,
"callBackUuid": "4ca54a59-fcdf-4c55-8783-1112dd3405cf",
"callBackUrl": "",
"custName": "张三"
}
POST /comm/callback/sms
Content-Type: application/json
tenantId 字段以便切库ISmsService.smsNotify 返回(通常为 "success" 或平台约定字符串)sequenceDiagram
participant Client as 调用方
participant GW as fs-comm-gateway
participant EC as EasyCall
participant WF as 工作流引擎
Client->>GW: POST /comm/call/send
GW->>GW: 校验鉴权/线路/号码/黑名单
GW->>EC: addCallList + startTask
GW-->>Client: callBackUuid, batchId, phone
EC->>GW: POST /comm/callback/easycall
GW->>GW: 切租户库、更新 call_log
GW->>WF: resumeFromBlockingNode
/comm/auth/token 获取 accessTokenroboticId、calleeId、gatewayId(或 smsTempId)/comm/call/send 或 /comm/sms/sendcode === 200,并保存 callBackUuid / callbackUuidGET /comm/query/call/{callBackUuid} 或等待 EasyCall 回调触发后续流程comm.gateway.tenant-qps-limit(默认 200)租户请求频率超限,请稍后重试gatewayId 必须属于当前公司可用线路,校验顺序:
getGatewayList(companyId) 返回列表gateWayList 配置cId.config.showGatewayIdsapplication.yml 核心项(生产环境请通过环境变量或配置中心注入,勿提交明文密钥):
server:
port: 8010
token:
header: Authorization
secret: ${COMM_TOKEN_SECRET}
expireTime: 120 # Token 有效分钟数
comm:
gateway:
internal-secret: ${COMM_INTERNAL_SECRET}
tenant-qps-limit: 200
executor:
core-pool-size: 20
max-pool-size: 100
queue-capacity: 2000
easycall:
base-url: http://{easycall-host}:{port}
# 1. 获取 Token
TOKEN_RESP=$(curl -s -X POST "http://127.0.0.1:8010/comm/auth/token" \
-H "Content-Type: application/json" \
-d '{"tenantCode":"demo","account":"admin","password":"your_password"}')
ACCESS_TOKEN=$(echo $TOKEN_RESP | jq -r '.data.accessToken')
# 2. 发起外呼
curl -s -X POST "http://127.0.0.1:8010/comm/call/send" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"calleeId": 25,
"roboticId": 174,
"gatewayId": 5,
"nodeKey": "node_call_1",
"workflowInstanceId": "your-workflow-instance-id"
}'
curl -s -X POST "http://127.0.0.1:8010/comm/call/send" \
-H "Content-Type: application/json" \
-H "X-Comm-Internal-Secret: your_internal_secret" \
-H "X-Comm-Tenant-Id: 33" \
-H "X-Comm-Company-Id: 1001" \
-d '{
"calleeId": 25,
"roboticId": 174,
"gatewayId": 5,
"nodeKey": "node_call_1",
"workflowInstanceId": "your-workflow-instance-id"
}'
Map<String, Object> body = new HashMap<>();
body.put("calleeId", 25L);
body.put("roboticId", 174L);
body.put("gatewayId", 5L);
body.put("nodeKey", "node_call_1");
body.put("workflowInstanceId", workflowInstanceId);
JSONObject result = commGatewayClient.sendCall(tenantId, companyId, companyUserId, body);
String callBackUuid = result.getString("callBackUuid");
String phone = result.getString("phone");
cd ylrz_saas_his_scrm
mvn clean package -pl fs-comm-gateway -am -DskipTests
docker build -t fs-comm-gateway:latest fs-comm-gateway/
docker run -d -p 8010:8010 \
-e SPRING_PROFILES_ACTIVE=prod \
fs-comm-gateway:latest
参考模块内 nginx.conf:
location /comm/ {
proxy_pass http://127.0.0.1:8010/comm/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 120s;
}
# 软电话 WebSocket 代理需额外升级头
location /comm/softphone/ws {
proxy_pass http://127.0.0.1:8010/comm/softphone/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 600s;
}
| 表名 | 库 | 说明 |
|---|---|---|
| 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
| 项 | 值 |
|---|---|
| 模块 | fs-comm-gateway |
| 默认端口 | 8010 |
| API 前缀 | /comm |
| 文档更新 | 2026-06-03 |
如有对接问题,请提供:tenantId、callBackUuid、请求体、完整响应 JSON 及服务端日志时间点,便于排查。