# fs-comm-gateway 通讯中间件对接文档 ## 1. 概述 `fs-comm-gateway` 是 SaaS 平台统一的**外呼 / 短信通讯网关**,对外提供标准化 HTTP API,对内供工作流引擎、业务服务通过 `CommGatewayClient` 调用。 主要能力: | 能力 | 说明 | |------|------| | AI 外呼 | 对接 EasyCallCenter365,创建任务、追加名单、启动外呼 | | 短信发送 | 基于租户短信模板与余额,批量发送 AI 短信 | | 工作流回调 | 接收 EasyCall / 短信平台回调,续跑阻塞节点 | | 外呼记录查询 | 按 `callBackUuid` 查询外呼日志 | 默认服务端口:**8010** 建议通过 Nginx 反向代理暴露:`/comm/` → `http://127.0.0.1:8010/comm/` --- ## 2. 环境与依赖 网关运行依赖以下基础设施(由运维按环境配置): | 组件 | 用途 | |------|------| | MySQL 主库 | 租户信息、公司账号鉴权 | | MySQL 租户库 | 业务数据(被叫人、模板、日志等) | | Redis | Token 会话、限流、工作流回调上下文 | | EasyCallCenter365 | 实际外呼平台,`easycall.base-url` 配置 | --- ## 3. 鉴权方式 网关支持两种调用身份,**二选一**即可访问业务接口(除白名单路径外)。 ### 3.1 外部对接:JWT Token(推荐三方系统使用) #### 3.1.1 获取 Token ```http POST /comm/auth/token Content-Type: application/json ``` **请求体:** ```json { "tenantCode": "your_tenant_code", "account": "company_user_account", "password": "plain_password" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | tenantCode | string | 是 | 租户编码 | | account | string | 是 | 公司端登录账号 | | password | string | 是 | 登录密码(BCrypt 校验) | **成功响应:** ```json { "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 | 当前会话绑定的租户与公司上下文 | #### 3.1.2 携带 Token 调用业务接口 ```http Authorization: Bearer {accessToken} Content-Type: application/json ``` #### 3.1.3 刷新 / 注销 Token ```http POST /comm/auth/refresh Authorization: Bearer {accessToken} ``` ```http POST /comm/auth/logout Authorization: Bearer {accessToken} ``` --- ### 3.2 内部服务调用:Internal Secret 请求头 平台内部服务(如工作流引擎)通过 `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`): ```yaml comm: gateway: base-url: http://127.0.0.1:8010 internal-secret: ${COMM_INTERNAL_SECRET} enabled: true fallback-local: false # 网关失败时是否降级本地直连 ``` --- ### 3.3 免鉴权路径 以下路径**不需要** 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) | --- ### 3.4 软电话 WebSocket 网关鉴权(方案 A) ccPhoneBar 控制通道可走 comm-gateway WebSocket 代理(开发环境 `ws-proxy-url` 配置)。采用**双层鉴权**: | 层级 | 凭证 | 说明 | |------|------|------| | 网关层(前端携带) | `wsTicket` | 8006 调用 8010 内部接口申请,Redis 短时有效(默认 60s),握手一次性消费 | | 上游 IPCC(网关内部) | `loginToken` | 出票时由 8006 写入票据,前端不感知;8010 验票后透传 IPCC | #### 3.4.1 内部申请 wsTicket ```http 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} ``` **请求体:** ```json { "loginToken": "easycall_login_token", "extNum": "1001" } ``` **成功响应:** ```json { "code": 200, "msg": "success", "data": { "wsTicket": "uuid", "expiresIn": 60 } } ``` 业务服务在 `getToolbarBasicParam` 合并配置时自动调用(`CommGatewayClient.issueSoftPhoneWsTicket`),前端从接口响应读取 `wsTicket`(**不返回** `loginToken`)。 #### 3.4.2 前端 WebSocket 连接 ```text ws://{gateway}/comm/softphone/ws?wsTicket={wsTicket} ``` - 缺少 `wsTicket` 或票据过期/已使用时,握手被拒绝。 - `loginToken` 仅存在于服务端票据中,由网关连接上游 IPCC 时自动附带。 - JsSIP 通话 WSS(8443)仍直连 IPCC,不走本代理。 - 若浏览器报 `Unexpected response code: 200`(响应头含 `Vary: Origin`),根因是 `fs-framework` 的 `ResourcesConfig` 对 `/**` 注册了全局 `CorsFilter`,阻断了 WebSocket Upgrade。comm-gateway 启动类已**排除** `ResourcesConfig`,并由 `CommGatewayCorsFilter` 对 `/comm/softphone/ws` 强制绕过 CORS。修改后须**重启 8010**。 #### 3.4.3 配置项 ```yaml 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 ``` --- ## 4. 统一响应格式 业务 Controller 统一返回 `CommApiResult`: ```json { "code": 200, "msg": "success", "data": { } } ``` | code | 含义 | |------|------| | 200 | 成功 | | 401 | 未认证(如查询接口未带 Token) | | 404 | 资源不存在 | | 500 | 业务失败或系统异常 | **判定规则:** - HTTP 状态码通常为 **200**,请以响应体 **`code` 字段** 判断业务成败。 - `code != 200` 时,`msg` 为失败原因,对接方应记录并向上游返回失败。 **常见失败 msg 示例:** | msg | 场景 | |-----|------| | 被叫人手机号解密失败或号码无效 | 被叫号码为空或解密失败 | | 被叫人命中外呼黑名单 | 黑名单拦截 | | 外呼名单追加失败或线路限流 | EasyCall 未追加名单或线路限流 | | 成功追加0个名单 | EasyCall 返回 0 条(号码无效等) | | 无权使用该外呼线路 | gatewayId 不在公司可用线路内 | | 租户请求频率超限,请稍后重试 | 触发 QPS 限流 | | 剩余短信数量不足,请充值 | 短信余额不足 | | 短信模板不存在或未审核 | 模板无效 | > 注意:Filter 层未授权时返回 `AjaxResult` 格式(`code: 401`),与 `CommApiResult` 字段结构一致。 --- ## 5. 接口明细 ### 5.1 发起外呼 ```http POST /comm/call/send Authorization: Bearer {accessToken} Content-Type: application/json ``` **请求体:** ```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:** ```json { "callBackUuid": "4ca54a59-fcdf-4c55-8783-1112dd3405cf", "batchId": 159576, "phone": "13800138000" } ``` | 字段 | 说明 | |------|------| | callBackUuid | 本次外呼唯一标识,用于查询与回调关联 | | batchId | EasyCall 任务批次 ID | | phone | 实际外呼号码(明文) | **处理流程简述:** 1. 校验线路归属、黑名单、号码有效性 2. 写入 Redis 工作流回调上下文(`easycall:workflow:callback:{callBackUuid}`) 3. 调用 EasyCall `addCallList` + `startTask` 4. 异步写入租户库 `company_voice_robotic_call_log_callphone` --- ### 5.2 发送短信 ```http POST /comm/sms/send Authorization: Bearer {accessToken} Content-Type: application/json ``` **请求体:** ```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:** ```json { "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 + content` - 模板类型 `tempType=2`(营销):内容为 `sign + content + 拒收请回复R` --- ### 5.3 查询外呼记录 ```http GET /comm/query/call/{callBackUuid} Authorization: Bearer {accessToken} ``` **成功响应 data:** 外呼日志对象(`company_voice_robotic_call_log_callphone` 表字段 JSON 化),含 `status`、`result`、`callTime`、`intention` 等。 **失败示例:** ```json { "code": 404, "msg": "未找到外呼记录", "data": null } ``` --- ### 5.4 平台回调接口(运维 / EasyCall 配置) #### EasyCall 外呼回调 ```http POST /comm/callback/easycall Content-Type: application/json ``` - 请求体:EasyCall CDR JSON 字符串(原样 POST) - 响应:成功返回 `"success"`,非法 IP 返回 `"illegal IP"` - 安全:`@CallbackIpCheck` 校验来源 IP 是否在租户 `cId.config.legalIPs` 白名单内 - 处理:解析 `bizJson.tenantId` 切换租户库,更新外呼日志并续跑工作流阻塞节点 **EasyCall 侧需配置的回调地址示例:** ``` https://{your-domain}/comm/callback/easycall ``` `bizJson` 中需包含(网关外呼时自动写入): ```json { "tenantId": 33, "callBackUuid": "4ca54a59-fcdf-4c55-8783-1112dd3405cf", "callBackUrl": "", "custName": "张三" } ``` #### 短信回执回调 ```http POST /comm/callback/sms Content-Type: application/json ``` - 请求体:短信平台回执 JSON,需包含 `tenantId` 字段以便切库 - 响应:由 `ISmsService.smsNotify` 返回(通常为 `"success"` 或平台约定字符串) --- ## 6. 对接时序 ### 6.1 外呼 + 工作流续跑 ```mermaid 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 ``` ### 6.2 三方系统最小对接步骤 1. 调用 `/comm/auth/token` 获取 `accessToken` 2. 准备业务数据:`roboticId`、`calleeId`、`gatewayId`(或 `smsTempId`) 3. 调用 `/comm/call/send` 或 `/comm/sms/send` 4. **必须检查响应 `code === 200`**,并保存 `callBackUuid` / `callbackUuid` 5. 轮询 `GET /comm/query/call/{callBackUuid}` 或等待 EasyCall 回调触发后续流程 --- ## 7. 限流与线路鉴权 ### 7.1 租户 QPS 限流 - 配置项:`comm.gateway.tenant-qps-limit`(默认 200) - 外呼、短信发送前均校验 - 超限返回:`租户请求频率超限,请稍后重试` ### 7.2 外呼线路鉴权 `gatewayId` 必须属于当前公司可用线路,校验顺序: 1. EasyCall `getGatewayList(companyId)` 返回列表 2. 否则读公司 `gateWayList` 配置 3. 否则读全局 `cId.config.showGatewayIds` --- ## 8. 配置参考 `application.yml` 核心项(生产环境请通过环境变量或配置中心注入,**勿提交明文密钥**): ```yaml 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} ``` --- ## 9. 调用示例 ### 9.1 cURL:登录 + 外呼 ```bash # 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" }' ``` ### 9.2 cURL:内部服务调用 ```bash 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" }' ``` ### 9.3 Java(内部 CommGatewayClient) ```java Map 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"); ``` --- ## 10. 部署说明 ### 10.1 构建 ```bash cd ylrz_saas_his_scrm mvn clean package -pl fs-comm-gateway -am -DskipTests ``` ### 10.2 Docker ```bash 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 ``` ### 10.3 Nginx 反代 参考模块内 `nginx.conf`: ```nginx 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; } ``` --- ## 11. 附录:相关数据表 | 表名 | 库 | 说明 | |------|-----|------| | 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` --- ## 12. 版本与联系 | 项 | 值 | |----|-----| | 模块 | fs-comm-gateway | | 默认端口 | 8010 | | API 前缀 | `/comm` | | 文档更新 | 2026-06-03 | 如有对接问题,请提供:`tenantId`、`callBackUuid`、请求体、完整响应 JSON 及服务端日志时间点,便于排查。