接入 Messaging
Messaging 用平台账号按人查发送地址、检查开通、回写投递回执,并按事件清理缓存。
| 环境 | 登录地址 | 带证书调用的接口地址 |
|---|---|---|
local | https://id.local.globalbes.sg | https://id.local.globalbes.sg:19443 |
test | https://id.test.globalbes.sg | https://id.test.globalbes.sg:9443 |
staging | 待定 | 待定 |
prod | 待定 | 待定 |
Sign in to send requests to see which of these steps are done. Sign in
生成私钥和证书请求
在哪里做:Messaging 服务器
生成 Messaging 私钥和证书请求,把证书请求送到客户端 CA 签发。
CERT_DIR="$HOME/bes-certs/messaging" mkdir -p "$CERT_DIR" SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z) openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ -subj "/CN=Messaging" \ -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature" \ -addext "extendedKeyUsage=clientAuth" \ -keyout "$CERT_DIR/messaging-service-key.pem" -out "$CERT_DIR/messaging-service.csr"
SERVICE_ACCOUNT_ID每次新生成,写进证书唯一的 URI SANurn:bes:service:<uuid>;登记平台账号后它就是账号的service_account_id。- 私钥
messaging-service-key.pem放进 Messaging 部署的密钥存储。
签发证书
在哪里做:客户端 CA 所在的机器
用客户端 CA 给证书请求签名,得到证书 messaging-service.pem。证书拷回 Messaging 服务器,和私钥一起调 Control;登记平台账号时也要用它。
前提:OpenSSL 3.0 及以上,openssl version 显示 OpenSSL 3.x。Mac 自带的 /usr/bin/openssl 是 LibreSSL,不认 -copy_extensions:先执行 brew install openssl@3,再执行 export PATH="$(brew --prefix openssl@3)/bin:$PATH"。
本环境(test)
CA_DIR:待定,test 环境的客户端 CA 位置未定。- 签发前把
messaging-service.csr拷到客户端 CA 所在机器的~/bes-certs/messaging/;签发后把messaging-service.pem拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/messaging-service.pem"
其他环境
local
本地的客户端 CA 在本机,证书请求不用拷,在本机执行:
CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \
-CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
-CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \
-out "$CERT_DIR/messaging-service.pem"
staging
CA_DIR:待定,staging 环境的客户端 CA 位置未定。- 签发前把
messaging-service.csr拷到客户端 CA 所在机器的~/bes-certs/messaging/;签发后把messaging-service.pem拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/messaging-service.pem"
prod
CA_DIR:待定,prod 环境的客户端 CA 位置未定。- 签发前把
messaging-service.csr拷到客户端 CA 所在机器的~/bes-certs/messaging/;签发后把messaging-service.pem拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging" mkdir -p "$CERT_DIR" CA_DIR="待定" openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \ -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \ -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \ -out "$CERT_DIR/messaging-service.pem"
登记平台账号
在哪里做:Control 服务器
执行 bes-control seed-trusted-caller,得到 Messaging 的平台账号,状态 active。之后 Messaging 带这张证书调查询接口,Control 按这个账号放行。
- 账号带 Control 的
channel-gateway角色(role),命令输出应为role=channel-gateway;列表不返回角色码,自动检查按显示名、product_id: null和状态判断。 --display-name写Messaging:测试台按这个名字找 Messaging 的平台账号。- 重复执行保留同一个账号。
本环境(test)
- 在 Control 服务器上执行
bes-control的方式:待定,test 环境的 Control 部署方式未定。 messaging-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging
其他环境
local
本地 Control 在容器 control-api 里,先把证书拷进 ~/.local/share/bes/local/stack/secrets/control/,每次执行时它被复制成容器里的 /run/bes/control/messaging-service.pem。在本机执行:
BES_INFRA="$HOME/Projects/bes-infra" # bes-infra 仓的路径
cp "$HOME/bes-certs/messaging/messaging-service.pem" "${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control/messaging-service.pem"
"$BES_INFRA/envs/local/dev" stack compose exec -T control-api /bin/sh /opt/bes/stack-entrypoint.sh \
bes-control seed-trusted-caller --role channel-gateway --certificate /run/bes/control/messaging-service.pem --display-name Messaging
staging
- 在 Control 服务器上执行
bes-control的方式:待定,staging 环境的 Control 部署方式未定。 messaging-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging
prod
- 在 Control 服务器上执行
bes-control的方式:待定,prod 环境的 Control 部署方式未定。 messaging-service.pem拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging --confirm-production
做完这一部分,手里有:Messaging 的证书、私钥和平台账号。查询时带证书和私钥。
查一个人的地址
在哪里做:Messaging 代码
发通知前按 person_id 查此人在用的地址,得到地址列表。用其中的地址发送。
程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
GET https://id.test.globalbes.sg:9443/internal/control/v1/people/5a9d3c1b-7e24-4f60-b8a1-c03e6d2f9475/channel-identities/resolve?channel_type=email
返回:
HTTP/1.1 200 OK
{
"person_id": "5a9d3c1b-7e24-4f60-b8a1-c03e6d2f9475",
"display_name": "Marcus Lim",
"channel_identities": [
{
"channel_identity_id": "6b1f4e9a-8c30-4d72-a5e6-f09c3b7d1e24",
"channel_type": "email",
"channel_identifier": "marcus.lim@topaz.example.com",
"status": "active",
"verified_at": null
}
]
}
按 person_id 查询 active 人员的全部 active 地址,任选符合消息渠道的地址发普通通知。verified_at: null 表示未验证,该地址同样可以发通知。凭据和邀请消息使用 Control 指定的投递地址。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 404 | PERSON_NOT_FOUND |
不发送:没有这个人。 |
查开通
在哪里做:Messaging 代码
发通知前检查所选租户、项目的 whatsapp-channel 开通状态。
GETEntitlement Resolve程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
GET https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve?tenant_id=7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11&project_id=e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903&product_id=b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614
返回:
HTTP/1.1 200 OK null
从环境配置取得 whatsapp-channel 的 product_id(值待定),按目标租户、项目查询,租户级产品省略 project_id。返回 null 时提示开通;有效时按 ends_at 控制缓存期限。
本环境(test)
Control 网关:https://id.test.globalbes.sg:9443;证书目录 ~/bes-certs/assistant/。
其他环境
local
Control 网关:https://id.local.globalbes.sg:19443;证书目录 ~/bes-certs/assistant/。
staging
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
prod
Control 网关:待定;证书目录 ~/bes-certs/assistant/。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 404 | PRODUCT_NOT_FOUND |
核对环境中登记的产品 ID。 |
| 403 | ACCESS_DENIED |
核对平台账号的授权解析权限。 |
请求示例
curl --fail-with-body --cert "$HOME/bes-certs/assistant/assistant-service.pem" --key "$HOME/bes-certs/assistant/assistant-service-key.pem" --get "https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve" --data-urlencode 'tenant_id=7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11' --data-urlencode 'project_id=e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903' --data-urlencode 'product_id=b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614'
回写回执
在哪里做:Messaging 代码
收到通道回执后把投递状态写回 Control,同一回执重试沿用 request_ref。
POSTDelivery Status Record程序里发这个请求(id 用前面步骤返回的):
带:证书
请求:
POST https://id.test.globalbes.sg:9443/internal/messaging/v1/delivery-status
Content-Type: application/json
{
"request_ref": "7804c8bc-e978-45e3-95b3-b36f405d5594",
"message_id": "2c0598e5-d12a-4f71-b65b-65f85c4d7b31",
"status": "delivered",
"occurred_at": "2026-10-06T08:00:00+00:00",
"batch_id": "1e29bb58-b018-4ee1-9c1d-53261eecf87f",
"recipient_ref": "first",
"reason": null,
"provider_error_code": null
}
返回:
HTTP/1.1 200 OK
{
"updated": true
}
message_id 使用发送受理返回的标识。每个状态变化用独立 request_ref,重试保持原值;把通道时间放入 occurred_at。
错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| 409 | IDEMPOTENCY_CONFLICT |
同一回执重试保留原参数;后续状态使用新的 request_ref。 |
接收事件并清缓存
在哪里做:Messaging 代码
接收 Control 事件,持久化去重即返回 2xx,再本地应用与重试。
按事件契约验证签名与 audience,使用 event_id 去重,持久化即 2xx,应用失败本地重试;发送方仅在未持久化时重试投递。
channel_access.revoked:丢掉该 channel_identity_id 的地址、认人和会话身份缓存;按 person_id 清除关联范围。person.status_changed:丢掉该 person_id 的地址、认人、授权和会话身份缓存。entitlement.changed:丢掉对应产品、租户、项目的开通结果和包含该范围的 contexts。
本环境(test)
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
其他环境
local
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
staging
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
prod
在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。
订阅配置
{
"messaging": {
"endpoint": "待定",
"audience": "messaging-api",
"event_types": [
"channel_access.revoked",
"person.status_changed",
"entitlement.changed"
]
}
}
做完这一部分,手里有:按人取得地址,检查开通并记录投递结果,订阅三种事件维护缓存。
展示上下文名称
在哪里做:Messaging 代码
显示 Control 返回的租户与项目名称;租户级项目名为 null。
身份上下文的显示名称来自 identity.resolve:每项包含 tenant_name 与 project_name;租户级 project_name 为 null。示例:{product_id: "<产品 UUID>", tenant_id: "<租户 UUID>", tenant_name: "Acme Construction", project_name: null}。
做完这一部分,手里有:按 Control 当前上下文展示租户与项目名称。