本文件是 aile-service-platform-integration 服務的最終設計規格,作為開發團隊的實施指導文件。文件自包含,覆蓋服務邊界、領域模型、安裝握手協議、閘道器層、事件三段鏈路、簽名規則、管理面介面與資料遷移等全部交付範圍。
[!NOTE]
本文件定位為開發參考用的臨時設計稿,協助團隊在正式規格文件發布前對齊服務設計與實作邊界;待正式規格建立後,可由正式文件取代並移除此參考稿。
閱讀物件:後端開發、測試、運維。
閱讀前提:已瞭解 Aile 多租戶生態(Aile、AIPower 等)與現有 aile-service-job 中 TenantAppModel / WebhookEventServiceImpl 的歷史實現。
aile-service-platform-integration 是 Aile 生態中負責「平臺級整合底座」的獨立微服務,定位為 Aile Open Integration Layer 的工程實體——為所有外部生態應用(AIPower 及未來擴充套件)提供統一的安裝、鑑權、路由與事件出口,自身不持有任何業務領域邏輯。
核心目標:
IntegrationApp / TenantIntegration / TenantMapping 三張核心模型,實現「平臺級應用定義」與「租戶級安裝例項」的分離/openapi/v1/* 閘道器層:統一路由、簽名驗籤、鑑權切面,內部代理回各領域子服務,不持有任何業務領域邏輯EventEnvelope 結構,封裝業務事件並推入 Pub/Sub,供獨立的 webhook 服務消費/install / /update / /uninstall / /rotate-secret),完成租戶安裝例項的全生命週期管理TenantAppModel / WebhookEventServiceImpl本服務採用「Open Integration Layer」邊界,本服務自有與僅代理兩類能力的劃分如下:
| 能力 | 本服務定位 | 說明 |
|---|---|---|
| IntegrationApp / TenantIntegration / TenantMapping | 自有領域 | 本服務獨佔的核心模型與狀態機 |
| 安裝握手協議(/install /update /uninstall /rotate-secret) | 自有介面 | 由本服務主導呼叫 AIPower 安裝入口 |
| 統一簽名 SDK / 驗籤 Filter | 自有能力 | 本服務暴露給所有 Aile 子服務複用 |
| /openapi/v1/* 閘道器 | 自有入口 + 代理轉發 | 路由 + 鑑權 + 簽名驗籤,領域邏輯回原服務 |
| 業務事件封裝 + Pub/Sub 投遞 + EventLog | 自有職責 | 本服務取代舊 WebhookEventServiceImpl 的「事件封裝+釋出」環節 |
| Webhook 出站投遞 / 重試 / DLQ | 不在本服務 | 由獨立 webhook 服務訂閱 Pub/Sub 完成 |
| 通知狀態事件(notice.*)封裝 + 釋出 | 自有職責 | message 服務透過 EventPublishClient 推入本服務,統一封裝為 EventEnvelope 後入 Pub/Sub,與業務事件共用同一 webhookUrl 投遞 |
| AIPower 反向能力呼叫(Aile→AIPower 執行時 API) | 本期不實現 | apiBaseUrl 欄位僅佔位,後期再設計 |
| AIFF / 業務物件 / 同步資源等所有領域邏輯 | 不在本服務 | 領域程式碼與儲存保留在 auth/job/tenant/account/room 等原服務,本服務僅作為閘道器代理 |
| SystemAppModel(內部服務間簽名) | 不動 | 保留在 aile-service-job,與 IntegrationApp 互不干涉 |
架構定點陣圖:
以下決策為本期實施的最終口徑,開發實施時直接遵循,不再做二次討論。
| 決策項 | 最終口徑 | 原因 / 說明 |
|---|---|---|
| 外部租戶欄位命名 | 統一使用 externalTenantId / externalSpaceId | 面向未來多生態應用擴充套件提前抽象,避免與具體應用(如 AIPower)耦合 |
| Webhook URL 設計 | 統一為單一 webhookUrl,業務事件與 notice.* 通知事件全部投遞到該地址 | 事件分發本質上是第三方接收側的職責(EventEnvelope 已攜帶 eventType);合併端點簡化握手協議、路由約定、遷移指令碼與接收側接入成本 |
| 通知 notice.* 事件 | 與業務事件完全同鏈路同通道:message 服務 → EventPublishClient → 本服務 → Pub/Sub → 獨立 webhook 服務 → 統一 webhookUrl | 走 Pub/Sub 解耦,避免增加 message 服務的同步出站壓力;複用統一簽名 / 重試 / DLQ;下游接收側按 EventEnvelope.eventType 自行分發 |
| 反向能力呼叫(Aile → 外部應用執行時 API) | 本期不實現,apiBaseUrl 欄位僅佔位 | P0 範圍聚焦核心閉環 |
| Scope 鑑權 | 本期不實現,supportedScopes / grantedScopes 預設 [""] | 簡化首版,後續再做細粒度 |
| PENDING_USER_CONFIRM 狀態 | 列舉保留,反向回撥介面不實現 | 本期外部應用不需要人工審批流程 |
| Nonce 防重放 | 本期不實現(沿用現狀) | nonce 僅參與簽名計算,不快取查重,後續如有安全需求再加 |
| Secret 儲存 | 明文儲存 Mongo | 沿用現狀,後續如啟用 KMS 再遷移 |
| 金鑰輪換策略 | 硬切換,舊金鑰立即失效 | 實現簡單,本期可接受短暫中斷 |
| 舊介面相容 | 不提供灰度相容期,遷移後直接下線 /tenantapp/v1/ | 降低長期維護成本 |
| EventLog 用途 | 僅作審計,不作重試源 | 重試由下游獨立 webhook 服務負責 |
採用三層命名以消除舊 appId 二義性(全域性應用定義、租戶安裝例項、租戶實體三者明確分離)。下表為本服務的正式命名口徑,所有程式碼、介面、文件須統一使用。
| 正式命名 | 含義 | 使用位置 |
|---|---|---|
integrationAppId | 全域性生態應用定義 ID,例如 aipower。全平臺唯一,不隨租戶變化 | IntegrationApp.appId、EventEnvelope.integration.appId |
tenantIntegrationId | 租戶安裝例項 ID,簽名鑑權與路由的主要依據 | 簽名 header、TenantIntegration.integrationId、EventEnvelope.integration.integrationId |
tenantIntegrationSecret | 租戶安裝例項簽名金鑰 | 簽名演算法輸入,僅本服務與該安裝例項持有 |
aileTenantId | Aile 側的租戶 ID | TenantIntegration.aileTenantId、TenantMapping.aileTenantId |
externalTenantId / externalSpaceId | 外部生態應用側的租戶/空間 ID(抽象命名,適配 AIPower 及未來其他生態應用) | TenantMapping.externalTenantId、EventEnvelope.tenant.mappedExternalTenantId |
ownerType / ownerId | 個人租戶隔離維度 | TenantMapping.ownerType / ownerId |
簽名 Header 的正式口徑:
Authorization = "AILE " + tenantIntegrationId + ":" + signatureaile-service/
aile-service-platform-integration/
src/main/java/com/aile/service/platformintegration/
PlatformIntegrationApplication.java
config/
gateway/ ← 网关层(新增,本服务核心入口)
OpenApiRoutingFilter.java ← /openapi/v1/* 路由
SignatureVerifyFilter.java ← 入站签名验签
TenantContextResolver.java ← 根据 tenantIntegrationId 解析租户上下文
ProxyController.java ← 代理转发
controller/ ← 自有接口层
InstallController.java ← /install /update /uninstall /rotate-secret
IntegrationAppController.java ← /admin/integrations/apps
TenantIntegrationController.java
application/ ← 应用服务层
InstallHandshakeAppService.java
IntegrationManagementAppService.java
EventPublishAppService.java
domain/ ← 领域层
integrationapp/
IntegrationApp.java
IntegrationAppRepository.java
tenantintegration/
TenantIntegration.java
TenantIntegrationRepository.java
TenantIntegrationStatus.java
TenantIntegrationAuditRepository.java
InstallHandshakeService.java
tenantmapping/
TenantMapping.java
TenantMappingRepository.java
event/
EventEnvelope.java
StandardEventType.java
EventPublisher.java ← 接口,实现走 Pub/Sub
EventLogRepository.java
signature/
HmacSignatureService.java
infrastructure/ ← 基础设施层
persistence/
IntegrationAppMongoRepo.java
TenantIntegrationMongoRepo.java
TenantIntegrationAuditMongoRepo.java
TenantMappingMongoRepo.java
EventLogMongoRepo.java
pubsub/
EventPubSubPublisher.java ← Pub/Sub 实现
http/
InstallHttpClient.java ← 调用 AIPower /install
proxy/
DownstreamRouteRegistry.java ← /openapi/v1/* 路由配置API 模型層(與其他服務共享):
aile-api/
aile-platform-integration-api/
src/main/java/com/aile/api/platformintegration/
model/
IntegrationAppModel.java
TenantIntegrationModel.java
TenantMappingModel.java
enums/
IntegrationAppStatus.java
TenantIntegrationStatus.java
StandardEventType.java
OwnerType.java
dto/
InstallRequestDto.java
InstallResponseDto.java
UpdateInstallRequestDto.java
UninstallRequestDto.java
RotateSecretRequestDto.java
EventEnvelopeDto.java平臺級全域性應用定義。一個生態應用在全平臺僅一條記錄。
@Document("aile.platform.integration.app")
@CompoundIndexes({
@CompoundIndex(name = "appId_1", def = "{'appId': 1}", unique = true)
})
public class IntegrationAppModel extends BaseModel {
private String appId; // integrationAppId,如 "aipower"
private String appName;
private String provider; // 如 "AIPOWER"
private String installBaseUrl; // 固定安装入口地址(控制面)
private List<TenantType> supportedTenantTypes; // 支持的租户类型,例如 [PERSONAL, TEAM]
private List<String> supportedScopes; // 占位,本期默认 ["*"]
private List<String> defaultScopes; // 占位,本期默认 ["*"]
private List<String> supportedEvents; // 支持的事件类型清单(资源域级,如 contact.*)
private IntegrationAppStatus status; // ACTIVE / DEPRECATED
}租戶安裝例項。同一 aileTenantId + appId 只能存在一個非 DELETED 記錄。
@Document("aile.platform.tenant.integration")
@CompoundIndexes({
@CompoundIndex(name = "integrationId_1", def = "{'integrationId': 1}", unique = true),
@CompoundIndex(name = "tenant_app_active",
def = "{'aileTenantId': 1, 'appId': 1, 'status': 1}")
})
public class TenantIntegrationModel extends BaseModel {
private String integrationId; // tenantIntegrationId
private String aileTenantId;
private TenantType aileTenantType; // PERSONAL / TEAM
private String appId; // 关联 IntegrationApp.appId
private String appSecret; // tenantIntegrationSecret,明文存储(本期沿用现状)
private IntegrationMode integrationMode; // PERSONAL / TEAM
private String apiBaseUrl; // 占位,本期不调用
private String webhookUrl; // 统一 Webhook 接收地址,业务事件与 notice.* 通知事件全部投递到此(由独立 webhook 服务消费 Pub/Sub 后投递);接收侧自行按 eventType 分发
private List<String> grantedScopes; // 占位,本期默认 ["*"]
private List<String> subscribedEvents; // 资源域级订阅,如 ["contact.*", "user.*"]
private TenantIntegrationStatus status;
private String createdBy;
// 审计日志不内嵌,使用独立 collection aile.platform.tenant.integration.audit
}狀態機:
| 狀態 | 含義 | 本期是否啟用 |
|---|---|---|
| PENDING | Aile 已建立安裝草稿,握手未完成 | 是 |
| PENDING_USER_CONFIRM | AIPower 側需人工確認 | 列舉保留,不會進入此狀態 |
| ACTIVE | 安裝完成,正常執行 | 是 |
| SUSPENDED | 臨時停用 | 是 |
| DISABLED | 租戶/平臺主動停用 | 是 |
| DELETED | 已解除安裝,終態 | 是 |
狀態遷移圖:
關鍵規則:
ACTIVE 狀態不投遞事件,不接受簽名 API 呼叫(解除安裝/輪轉等控制面動作除外)aileTenantId + appId 組合只能存在一個非 DELETED 記錄(應用層校驗 + 唯一索引)DELETED 為終態,清理期結束後可物理清除 appSecret 與端點資訊跨系統租戶對映。
@Document("aile.platform.tenant.mapping")
public class TenantMappingModel extends BaseModel {
private String mappingId;
private String aileTenantId;
private TenantType aileTenantType; // PERSONAL / TEAM
private String externalTenantId; // 外部生态应用侧租户 ID(如 AIPower 内部租户 ID)
private String externalSpaceId; // 团队模式空间ID,个人模式为 null
private OwnerType ownerType; // AILE_PERSONAL / AILE_TEAM
private String ownerId; // 个人模式必填,= ailePersonalTenantId
private String integrationId; // 关联 TenantIntegration
private MappingStatus status;
}OwnerType 列舉:
public enum OwnerType {
AILE_PERSONAL, // 个人租户模式
AILE_TEAM // 团队租户模式(预留,实际团队模式可不依赖 ownerId)
}個人租戶 / 團隊租戶業務前提:
對映規則:
externalTenantId(=publicTenantId)ownerType = AILE_PERSONALownerId = ailePersonalTenantId(= 該賬號 ID,三者一一對應),作為 AIPower 共享租戶內的資料隔離鍵externalTenantId,可選 externalSpaceIdContact ↔ ServiceNumber 關係約定(本服務不直接持有 contact / visitor / service_number 領域模型,這些保留在 auth / tenant 等下游服務,但其關係約束直接影響事件 scope 與 OpenAPI 路徑設計,在此明確口徑):
scope.serviceNumberId。scope.serviceNumberId;followed / unfollowed 表達客戶主動關注 / 取消關注該服務號的狀態變遷。/openapi/v1/service-numbers/{snId}/ 之下,因為外部生態應用看到的「客戶」始終是某個服務號下的檢視(詳見 §9.2)。@Document("aile.platform.tenant.integration.audit")
@CompoundIndexes({
@CompoundIndex(name = "integrationId_time",
def = "{'integrationId': 1, 'occurredAt': -1}")
})
public class TenantIntegrationAuditModel extends BaseModel {
private String integrationId;
private TenantIntegrationStatus fromStatus;
private TenantIntegrationStatus toStatus;
private String actor; // userId 或 system 标识
private String reason;
private Long occurredAt;
}僅追加寫入,不更新不刪除,保留完整狀態遷移歷史。
本節定義 Aile 主導、外部生態應用配合的安裝握手全流程,涵蓋建立、更新、解除安裝、金鑰輪換四個控制面動作。
/install 請求欄位請求:POST {installBaseUrl}/install
| 欄位 | 必填 | 說明 |
|---|---|---|
integrationAppId | 是 | 全域性應用 ID,如 aipower |
tenantIntegrationId | 是 | 本次安裝的租戶例項 ID |
tenantIntegrationSecret | 是 | 與上述 ID 配對的簽名金鑰,僅在安裝握手階段下發 |
aileTenantId | 是 | Aile 租戶 ID |
aileTenantType | 是 | PERSONAL / TEAM |
requestedScopes | 是 | 本期固定為 ["*"] |
aileApiBaseUrl | 是 | AIPower 後續呼叫 Aile Open API 的基礎地址 |
installNonce | 是 | 冪等鍵,AIPower 據此去重同一安裝請求 |
installedAt | 是 | ISO-8601 時間戳 |
安全說明:本期 /install 不攜帶簽名,僅依賴 HTTPS + AIPower 側 IP 白名單保障來源合法性。tenantIntegrationSecret 在請求體中明文下發(HTTPS 傳輸層已加密)。
/install 響應欄位| 欄位 | 必填 | 說明 |
|---|---|---|
integrationMode | 是 | PERSONAL / TEAM |
apiBaseUrl | 是 | 佔位,本期儲存但不呼叫 |
webhookUrl | 是 | 統一 Webhook 接收地址,業務事件與 notice.* 通知事件均投遞到此(由獨立 webhook 服務消費 Pub/Sub 後投遞),必須 HTTPS;第三方在自己 handler 內按 eventType 欄位分發到對應處理邏輯 |
externalTenantId | 是 | 外部生態應用側租戶 ID(如 AIPower 內部租戶 ID) |
externalSpaceId | 否 | 團隊模式可選,個人模式省略 |
ownerType | 個人租戶必填 | AILE_PERSONAL |
ownerId | 個人租戶必填 | ailePersonalTenantId |
acceptedScopes | 是 | 本期固定回傳 ["*"] |
installStatus | 是 | 本期僅接受 ACTIVE,返回其他值視為錯誤 |
| 錯誤碼 | HTTP | 說明 |
|---|---|---|
UNSUPPORTED_TENANT_TYPE | 400 | AIPower 不支援該 aileTenantType |
DUPLICATE_INSTALL | 409 | 同 installNonce 或同 (aileTenantId + integrationAppId) 已存在有效安裝 |
INTEGRATION_APP_NOT_FOUND | 404 | integrationAppId 未註冊或非 ACTIVE |
STATUS_TRANSITION_FORBIDDEN | 409 | 不允許的狀態遷移 |
SIGNATURE_INVALID | 401 | Open API 簽名驗籤失敗 |
TENANT_INTEGRATION_NOT_ACTIVE | 403 | 非 ACTIVE 狀態下呼叫需簽名的 Open API |
INVALID_WEBHOOK_URL | 400 | 非 HTTPS URL |
保留但本期不觸發:SCOPE_NOT_GRANTABLE(scope 未啟用)、REQUIRES_MANUAL_APPROVAL(人工審批未啟用)。
| 介面 | 說明 |
|---|---|
POST {installBaseUrl}/update | 更新已安裝租戶的 webhook 端點等 |
POST {installBaseUrl}/uninstall | 解除安裝,本服務側 TenantIntegration → DELETED |
POST {installBaseUrl}/rotate-secret | 金鑰輪換,硬切換,舊金鑰立即失效,無重疊期 |
raw = tenantIntegrationId + nonce + bodyString
signature = Base64(HmacSHA256(tenantIntegrationSecret, raw))
Authorization = "AILE " + tenantIntegrationId + ":" + signature統一適用範圍(消除當前 SignatureUtils 三套不一致):
/openapi/v1/* 入站驗籤/install 之後所有需鑑權呼叫不參與簽名的欄位:serviceNumberId 僅作為 OpenAPI 路徑上的資源標識與 EventEnvelope 的 scope 欄位使用,不進入簽名 raw 串;EventEnvelope 的 scope 子結構整體同樣不參與任何簽名計算。簽名 raw 串仍嚴格遵循 §8.1 第一段定義。
public interface HmacSignatureService {
String sign(String body, String tenantIntegrationId, String secret, String nonce);
boolean verify(String body, String authHeader, String nonce, String secret);
String buildAuthHeader(String tenantIntegrationId, String signature);
AuthHeaderParts parseAuthHeader(String authorizationHeader);
}本期不實現 nonce 防重放(沿用現狀)。nonce 僅作為簽名輸入引數參與計算,不做快取查重、不校驗時間戳視窗。後續如有安全需求再加 Redis 快取 + TTL。
tenantIntegrationSecret 明文儲存在 Mongo,與當前 TenantAppModel.secretKey 一致。已知風險點,本期不升級,後續如啟用 KMS 再做遷移。
/openapi/v1/* 閘道器層本服務承擔所有生態應用的入站 API 統一入口。
處理順序:
Authorization header,提取 tenantIntegrationId,查庫取 tenantIntegrationSecret,按§8.1 驗籤,失敗返回 401 SIGNATURE_INVALIDtenantIntegrationId 還原 aileTenantId / aileTenantType / externalTenantId / ownerType / ownerId,注入請求上下文DownstreamRouteRegistry,判斷應路由到哪個內部服務X-Aile-Tenant-Id / X-Aile-Owner-Id 等)DownstreamRouteRegistry 採用配置驅動 + 白名單:每條 Open API 必須在下表顯式註冊具體 method + path,不接受萬用字元字首;未登記路徑一律返回 404 ROUTE_NOT_FOUND。新增 API 時,本服務的路由配置與下游服務實現必須同步登記。
| 方法 + 路徑 | 下游服務 | 資源域 | 說明 |
|---|---|---|---|
GET /openapi/v1/tenants/me | aile-service-tenant | 租戶 | 查詢當前安裝例項所屬租戶資訊 |
GET /openapi/v1/users | aile-service-account | 使用者 | 列出租戶下使用者(分頁) |
GET /openapi/v1/users/{userId} | aile-service-account | 使用者 | 查詢單個使用者 |
GET /openapi/v1/service-numbers | aile-service-tenant | 服務號 | 列出當前 integration 可訪問的全部服務號 |
GET /openapi/v1/service-numbers/{snId} | aile-service-tenant | 服務號 | 查詢單個服務號詳情 |
GET /openapi/v1/groups | aile-service-room | 群組 | 列出租戶群組(分頁) |
GET /openapi/v1/groups/{groupId} | aile-service-room | 群組 | 查詢單個群組 |
GET /openapi/v1/addressbook | aile-service-auth | 通訊錄 | 查詢租戶通訊錄 |
POST /openapi/v1/aiff/configurations | aile-service-auth | AIFF 配置 | 註冊 AIFF 槽位 + endpoint URL |
GET /openapi/v1/aiff/configurations | aile-service-auth | AIFF 配置 | 查詢當前 integration 已註冊的 AIFF 配置清單 |
PUT /openapi/v1/aiff/configurations/{configId} | aile-service-auth | AIFF 配置 | 更新單個 AIFF 配置 |
DELETE /openapi/v1/aiff/configurations/{configId} | aile-service-auth | AIFF 配置 | 刪除單個 AIFF 配置 |
POST /openapi/v1/entry-sources | aile-service-room | 進線原因 | 建立預設進線來源 EntrySource;閘道器自動注入 owner.integrationAppId / owner.tenantIntegrationId 標記建立方;觸發事件按 owner 路由回建立方 webhook(詳 §9.4) |
GET /openapi/v1/entry-sources | aile-service-room | 進線原因 | 列表查詢(預設僅返回 owner.tenantIntegrationId = 當前 integration 建立的記錄;支援 status / channel / reason.type 過濾) |
GET /openapi/v1/entry-sources/{sourceId} | aile-service-room | 進線原因 | 查詢單個 EntrySource;owner 非當前 integration 時返回 403 ENTRY_SOURCE_FORBIDDEN |
PUT /openapi/v1/entry-sources/{sourceId} | aile-service-room | 進線原因 | 更新 EntrySource(name / reason.data / expiresAt / channels 等);owner 檢查同上 |
PUT /openapi/v1/entry-sources/{sourceId}/status | aile-service-room | 進線原因 | 停用 / 重啟 EntrySource(active ⇄ inactive);owner 檢查同上 |
GET /openapi/v1/business/objects | aile-service-job | 業務物件 | 列出業務物件型別(擴充套件點) |
GET /openapi/v1/sync/resources | aile-service-job | 同步資源 | 列出同步資源型別(擴充套件點) |
/openapi/v1/service-numbers/{snId}/...)| 方法 + 路徑 | 下游服務 | 資源域 | 說明 |
|---|---|---|---|
GET /openapi/v1/service-numbers/{snId}/contacts | aile-service-auth | 聯絡人 | 列出該服務號下的 contact(分頁 / 篩選) |
GET /openapi/v1/service-numbers/{snId}/contacts/{contactId} | aile-service-auth | 聯絡人 | 查詢單個 contact 在該服務號下的檢視 |
GET /openapi/v1/service-numbers/{snId}/contacts/labels | aile-service-auth | 聯絡人標籤 | 列出該服務號下可用的客戶標籤清單(供整合方篩選受眾) |
POST /openapi/v1/service-numbers/{snId}/contacts/labels:add | aile-service-auth | 聯絡人標籤 | 批次給 contact 打標籤(body:contactIds[] • labelIds[],冪等) |
POST /openapi/v1/service-numbers/{snId}/contacts/labels:remove | aile-service-auth | 聯絡人標籤 | 批次摘標籤(body:contactIds[] • labelIds[],冪等) |
GET /openapi/v1/service-numbers/{snId}/contacts/{contactId}/labels | aile-service-auth | 聯絡人標籤 | 查詢單個 contact 在該服務號下的全部標籤 |
GET /openapi/v1/service-numbers/{snId}/visitors | aile-service-auth | 訪客 | 列出該服務號下的 visitor |
GET /openapi/v1/service-numbers/{snId}/visitors/{visitorId} | aile-service-auth | 訪客 | 查詢單個 visitor 在該服務號下的檢視 |
POST /openapi/v1/service-numbers/{snId}/broadcasts | aile-service-message | 群發任務 | 建立服務號群發任務(複用 Aile 現行服務號群發能力,新增 OpenAPI 入口)。body:name / note / channels[] / messages[](1..5 則 NoticeContent 卡片 / `{type:"text" |
GET /openapi/v1/service-numbers/{snId}/broadcasts/{taskId} | aile-service-message | 群發任務 | 查詢任務狀態。返回 status(scheduled / sending / completed / cancelled / failed) / recipientCount / summary{sent,delivered,failed,read} / startedAt / finishedAt。MVP 階段以輪詢替代任務級 webhook |
POST /openapi/v1/service-numbers/{snId}/broadcasts/{taskId}:cancel | aile-service-message | 群發任務 | 取消任務(僅 scheduled 狀態有效) |
GET /openapi/v1/service-numbers/{snId}/notices/{noticeId} | aile-service-message | 通知 | 查詢單條通知詳情(狀態 / 渠道 / 接收方 / NoticeContent 快照) |
路徑設計約定:
/openapi/v1/service-numbers 提供「列出當前 integration 可訪問的全部服務號」能力,生態應用據此發現並下鑽。/openapi/v1/service-numbers/{snId}/... 巢狀路徑表達;閘道器從 path variable 解析 serviceNumberId,以 header X-Aile-Service-Number-Id 透傳給下游領域服務。serviceNumberId 僅作為路由 / 上下文引數,不參與簽名計算(詳見 §8.1)。/openapi/v1/service-numbers/{snId}/notices 直接建立通知,而是統一透過上表 POST .../broadcasts 介面提交群發任務(單條傳送視為 audience 長度為 1 的群發任務),Aile 群發流水線完成命中 / 排程 / 退訂 / 頻次 / 時間窗治理後下發;/notices/{noticeId} 僅保留只讀查詢語義。@ConfigurationProperties 載入,避免硬編碼。tenantIntegrationId 對應的 TenantIntegration.status 必須 = ACTIVE,否則返回 403 TENANT_INTEGRATION_NOT_ACTIVEsubscribedEvents 僅用於事件過濾,不參與入站 API 鑑權(本期不啟用 scope)/openapi/v1/service-numbers/{snId}/...),閘道器需額外校驗:tenantIntegrationId 必須有權訪問該 serviceNumberId(即該 service_number 已繫結到當前 integration),校驗不透過返回 403 SERVICE_NUMBER_FORBIDDEN/openapi/v1/entry-sources/{sourceId})的 GET / PUT / 狀態切換,閘道器在下游響應前由 aile-service-room 進行 owner 檢查:EntrySource.owner.tenantIntegrationId 必須 = 當前 tenantIntegrationId,否則返回 403 ENTRY_SOURCE_FORBIDDEN;列表查詢預設僅返回 owner 為當前 integration 的記錄ROUTE_NOT_FOUNDEntrySource(進線來源,詳見進線原因規格設計)是租戶級共享資源,但每條記錄歸屬唯一建立方(integration / Aile 後臺管理員)。本服務在閘道器層為 EntrySource 注入 owner 標記並據此完成事件迴流,使「誰建立,誰接收」成為整合契約的硬約束。
閘道器在轉發 POST /openapi/v1/entry-sources 時,自動在請求體中注入 owner 子結構(整合方不自行填寫,即便填寫也會被覆蓋):
{
"owner": {
"type": "integration",
"integrationAppId": "aireach",
"tenantIntegrationId": "ti_xxx"
}
}
| 欄位 | 取值 | 說明 |
|---|---|---|
owner.type | integration / aile_admin | 透過 /openapi/v1/entry-sources 建立的固定為 integration;Aile 後臺管理臺手工建立的為 aile_admin |
owner.integrationAppId | 當前 IntegrationApp.appId | 例 aireach;aile_admin 型別為 null |
owner.tenantIntegrationId | 當前 TenantIntegration.integrationId | 用於精確路由 webhook 回撥;aile_admin 型別為 null |
EntrySource 在建立後,owner 欄位不可修改;解除安裝安裝例項(TenantIntegration → DELETED)時,該 integration 名下所有 EntrySource 由本服務級聯呼叫 PUT .../status (inactive) 停用,但不刪除記錄(保留歷史 Session 的進線原因可讀性)。
當使用者透過某個 EntrySource 進線建立 Session,aile-service-room 按以下規則路由 session.* 事件:
核心約束:
integration.integrationId 來源於 EntrySource.owner,而非「觸發使用者當前所屬租戶的全部 integration」——多個 integration 安裝到同一租戶時,只有建立該 EntrySource 的 integration 會收到迴流,其它 integration 不感知。tenantIntegrationId 已不在 ACTIVE 狀態(SUSPENDED / DISABLED / DELETED),按 §6.2 狀態機規則不投遞,事件僅寫 EventLog 備查(publishStatus = FAILED,failureReason = "OWNER_INTEGRATION_NOT_ACTIVE")。owner.type = aile_admin 的 EntrySource 觸發時不發整合 webhook,僅在 Aile 內部產生 Session 流轉。session.* 資源域為支撐上述迴流鏈路,本期 §10.2 事件清單新增 session.* 資源域(由 aile-service-room 在 EntrySource 觸發 Session 時發出),清單詳見 §10.2。
POST /openapi/v1/entry-sources 建立 EntrySource 時,無需單獨配置 webhook URL —— 事件統一走該 integration 安裝時握手協議給出的 webhookUrl,無第二條投遞通道。TenantIntegration.subscribedEvents 中訂閱 session.* 資源域,否則即使 owner 匹配也不會收到迴流(本服務在釋出前按 subscribedEvents 過濾)。統一事件信封結構如下,所有 Aile 領域服務發出的事件都必須封裝為此結構後入 Pub/Sub:
{
"eventId": "evt_xxx",
"eventType": "contact.entered",
"eventVersion": "1.0",
"occurredAt": "2026-05-20T10:00:00Z",
"source": "aile-service-auth",
"integration": {
"appId": "aipower",
"integrationId": "ti_xxx"
},
"tenant": {
"aileTenantId": "t_xxx",
"aileTenantType": "PERSONAL",
"mappedExternalTenantId": "aip_xxx",
"mappedExternalSpaceId": null,
"ownerType": "AILE_PERSONAL",
"ownerId": "pt_xxx"
},
"scope": {
"serviceNumberId": "sn_xxx"
},
"data": { ... },
"metadata": { ... }
}關鍵設計點:
source 為實際發出事件的領域服務名(不再寫死為 aile)tenant 子結構同時包含 Aile 側與外部生態側兩個身份,供下游 webhook 服務路由到正確生態應用租戶scope 子結構承載「業務作用域」語義,與 tenant(身份維度)嚴格分離;本期只包含 serviceNumberId,後續可擴充套件 channelType 等橫切維度scope.serviceNumberId 在服務號級事件中必填,在租戶級事件中省略——具體分類見 §10.2eventVersion 事件 schema 變更時遞增scope 子結構及其內的 serviceNumberId 均不參與簽名計算(詳見 §8.1)本期本服務需支援封裝與釋出以下資源域:
| 資源域 | 中文說明 | 舉例事件型別 | 發出服務 | scope.serviceNumberId |
|---|---|---|---|---|
tenant.* | 租戶生命週期(Aile 側租戶本身的建立、修改、停用) | tenant.created / tenant.updated / tenant.disabled | aile-service-tenant | 不填(租戶級) |
user.* | 使用者 / 賬號(租戶下的成員,跨服務號共享) | user.created / user.updated / user.disabled | aile-service-account | 不填(租戶級) |
service_number.* | 服務號(ServiceNumber,租戶內的客戶進線入口 / 業務號;一個租戶可擁有 1~N 個) | service_number.created / service_number.updated / service_number.deleted | aile-service-tenant | 必填(= 主體 ID,自身即服務號) |
contact.* | 聯絡人 / 客戶(實名歸戶後的穩定主體,租戶範圍全域性唯一) | 主體生命週期(租戶級):contact.created / contact.updated / contact.deleted | ||
進線 / 關注行為(服務號級):contact.entered / contact.re_entered / contact.service_number_followed / contact.service_number_unfollowed | aile-service-auth | 主體生命週期類不填;進線 / 關注行為類必填 | ||
visitor.* | 訪客(未實名的臨時主體,可被合併 / 晉升為 contact) | 主體級:visitor.created / visitor.merged | ||
進線行為(服務號級):visitor.entered | aile-service-auth | 主體級不填;進線行為類必填 | ||
group.* | 群組 / 聊天室(租戶級,可跨服務號組織) | group.created / group.member_changed | aile-service-room | 不填(租戶級,群組跨服務號) |
addressbook.* | 通訊錄(租戶成員 / 聯絡人組織目錄同步) | addressbook.synced | aile-service-auth | 不填(租戶級) |
notice.* | 通知狀態(送達 / 失敗 / 已讀 / 點選 / 退回 / 投訴 / 任務完成 / 轉化 等狀態回撥) | 基礎(v1.0):notice.delivered / notice.failed / notice.read v1.3 推播場景閉環擴充套件:notice.clicked(連結點選) / notice.bounced(投遞失敗子類:號碼無效 / 拒收 / 通道故障) / notice.complained(使用者標記騷擾) / notice.task_completed(整批傳送任務終態) / notice.converted(Aile 側歸因轉化) | aile-service-message | 通知繫結具體服務號時必填;租戶級通知可省略 |
session.* | 會話生命週期(由 EntrySource 觸發的客戶會話)。按 EntrySource owner 精確路由回建立該來源的 integration,而非廣播給同租戶全部 integration(詳 §9.4) | session.created / session.closed / session.transferred | aile-service-room | EntrySource 繫結具體服務號時必填;租戶級 EntrySource 觸發時可省略 |
事件分類口徑:
scope.serviceNumberId 省略。包含 tenant / user / addressbook / group 全量,以及 contact / visitor 的主體生命週期事件。scope.serviceNumberId 必填。包含 contact / visitor 的進線、關注 / 取消關注事件,以及繫結到具體服務號的 notice。contact.entered / visitor.entered 等獨立事件型別,正確表達 contact ↔ service_number 的 N:M 關係——同一 contact 進入第二個服務號時,不觸發 contact.created,而是觸發 contact.entered。scope.serviceNumberId 是否符合上述約束;違反約束直接拋錯,避免下游接收側得到含義不明的事件。投遞約定:下游 webhook 服務統一投遞到 TenantIntegration.webhookUrl,不再按事件型別分流——本期合併 URL,所有資源域事件(含 notice.*)共用同一入口。接收側在自己 webhook handler 內根據 EventEnvelope 的 eventType 欄位分發到對應處理邏輯(業務異動 / 通知狀態 / 等)。
NoticeContent 訊息契約與 notice.clicked 觸發規則(v1.4 補充)
Aile 的「通知」是一種結構化富文字訊息格式(本服務不持有該領域邏輯,由 aile-service-message 承載),其傳送體(NoticeContent)由生態應用逐條組合後提交,Aile 不維護預審模板。為使 notice.clicked 事件能被正確路由回傳送應用,並與「進線」聯動,NoticeContent 與事件 payload 需遵以下約定:
NoticeContent 結構:title / image / describe / buttons (0..N)。每個 button 含 buttonId (傳送方給定、語義化) / label / actionType / payload。actionType 列舉:postback / url / aiff / action。點選捕獲路徑差異:postback: 客戶端 → aile-service-message 後端 postback 端點 → 解析 payload。url: aile-service-message 包一層跳轉代理(形如 /notice-click/{noticeId}/{buttonId})記錄點選 → 302 至目標 URL。aiff / action: 由 AileDesktop 客戶端攔截點選後埋點上報 aile-service-message。postback button.payload 契約(本服務與傳送應用雙方約束):appId (string, 必填):傳送方 IntegrationApp 的 integrationAppId,aile-service-message 據此反查該服務號下哪個 TenantIntegration 需被通知。clickId (string, 必填):傳送方自定義業務語義標識(例:campaign_42_cta_redeem)。所有其他 actionType 的 payload 僅供 Aile 本身使用,不作路由依據。extra (object, 可選):業務自定義欄位,原樣透傳回 webhook。notice.clicked event payload(EventEnvelope.data 子結構)必含:noticeId / buttonId / actionType / contactId / clickedAtpostbackPayload:原樣透傳傳送方在 NoticeContent 中給定的按鈕 payload(含 appId / clickId / extra)inbound (可選子結構,由 Aile 側進線規則決定是否攜帶):{ triggered: boolean, conversationId?: string, reason?: { code, label, source } }serviceNumberId / actionType / payload.clickId 等判據是否同時建立 / 複用 contact↔serviceNumber 進線) → 如命中,將 inbound 子結構填充入 event payload → EventPublishClient 封裝 EventEnvelope 交由本服務入 Pub/Sub。整合方接收一條 notice.clicked event 即可同時獲取「點選事實 + 進線事實 + 進線原因」。inbound.reason.code推薦格式:notice_click_<clickId> 或由 Aile 運營配置的對映表生成。本服務不新增領域邏輯,僅在「事件封裝 + 釋出」環節保證如上結構被原樣以
EventEnvelope.data透傳。NoticeContent結構的儲存 / 點選捕獲路由 / 進線規則引擎均由 aile-service-message + aile-service-room 實現。
職責邊界:
EventPublishClient(api 模組提供的 FeignClient)@Document("aile.platform.event.log")
@CompoundIndexes({
@CompoundIndex(name = "eventId_1", def = "{'eventId': 1}", unique = true),
@CompoundIndex(name = "tenant_time",
def = "{'aileTenantId': 1, 'occurredAt': -1}"),
@CompoundIndex(name = "ttl_idx",
def = "{'occurredAt': 1}",
expireAfterSeconds = 2592000) // 30 天 TTL
})
public class EventLogModel extends BaseModel {
private String eventId;
private String eventType;
private String integrationId;
private String aileTenantId;
private String envelopeJson; // 完整 EventEnvelope JSON 存档
private PublishStatus publishStatus; // PUBLISHED / FAILED
private String failureReason;
private Long occurredAt;
}publishStatus = FAILED 表示推入 Pub/Sub 本身失敗(僅本服務職責範圍),與下游 webhook 服務的重試狀態無關| 介面 | 說明 |
|---|---|
POST /admin/integrations/apps | 建立生態應用定義 |
GET /admin/integrations/apps | 列表查詢 |
GET /admin/integrations/apps/{appId} | 查詢單個 |
PUT /admin/integrations/apps/{appId} | 更新(僅名稱/描述/事件清單等可變欄位) |
POST /admin/integrations/apps/{appId}/deprecate | 下架,狀態 → DEPRECATED |
| 介面 | 說明 |
|---|---|
POST /admin/integrations/tenant-integrations | 啟動安裝握手(走§7 協議) |
GET /admin/integrations/tenant-integrations | 列表查詢(支援按 aileTenantId / appId / status 篩選) |
GET /admin/integrations/tenant-integrations/{integrationId} | 詳情 |
POST /admin/integrations/tenant-integrations/{integrationId}/suspend | ACTIVE → SUSPENDED |
POST /admin/integrations/tenant-integrations/{integrationId}/resume | SUSPENDED/DISABLED → ACTIVE |
POST /admin/integrations/tenant-integrations/{integrationId}/disable | → DISABLED |
POST /admin/integrations/tenant-integrations/{integrationId}/uninstall | → DELETED + 呼叫下游 /uninstall |
POST /admin/integrations/tenant-integrations/{integrationId}/rotate-secret | 硬切換金鑰 |
GET /admin/integrations/tenant-integrations/{integrationId}/audits | 查詢審計軌跡 |
/admin/integrations/apps/**(生態應用定義)/admin/integrations/tenant-integrations/**當前 aile-service-job 中的 TenantAppModel 是本服務未拆分前的簡化實現,由本服務上線後一次性遷移並下線。
| 舊實體/介面 | 新位置 | 處理方式 |
|---|---|---|
TenantAppModel(aile-service-job) | TenantIntegrationModel(本服務) | 一次性指令碼遷移存量資料 |
/tenantapp/v1/* 介面 | /admin/integrations/tenant-integrations • /openapi/v1/* | 舊介面直接下線(不提供相容期) |
WebhookEventServiceImpl(aile-service-job) | EventPublishAppService(本服務) | 原傳送點改為傳送到本服務 EventPublishClient |
| SignatureUtils 三套邏輯(各服務) | HmacSignatureService(本服務 api 模組) | 各服務按§8.1 統一呼叫 |
SystemAppModel(aile-service-job) | 不動,保留原位 | 內部服務間簽名繼續使用,與 IntegrationApp 互不干涉 |
// 偽程式碼
for each tenantApp in aile-service-job.TenantAppModel {
create IntegrationApp if not exists (provider=AIPOWER → appId=aipower)
create TenantIntegration {
integrationId = tenantApp.appId
tenantIntegrationSecret = tenantApp.secretKey // 明文沿用
aileTenantId = tenantApp.tenantId
appId = "aipower"
webhookUrl = tenantApp.webhookUrl // 旧 tenantApp.noticeUrl 不再使用,合并至唯一 webhookUrl;迁移后由租户管理员确认是否需改成 noticeUrl
status = ACTIVE
subscribedEvents = ["*"] // 存量按全订阅处理
}
create TenantMapping {
aileTenantId = tenantApp.tenantId
externalTenantId = tenantApp.aipowerTenantId
ownerType = AILE_PERSONAL // 存量推断
ownerId = tenantApp.tenantId
}
write audit log: actor=system, reason="migration from TenantAppModel"
}DownstreamRouteRegistry(各下游服務接入)/admin/integrations/apps 建立 aipower 應用定義TenantAppModel → TenantIntegration + TenantMapping/openapi/v1/*,各 Aile 子服務事件傳送點改用本服務 EventPublishClient/tenantapp/v1/* 介面與 TenantAppModel| 類別 | 交付物 |
|---|---|
| 新建服務 | aile-service-platform-integration(完整 DDD 分層) |
| 新建 api 模組 | aile-platform-integration-api(模型+列舉+DTO+EventPublishClient+HmacSignatureService) |
| 新建 Mongo 集合 | aile.platform.integration.app / aile.platform.tenant.integration / aile.platform.tenant.integration.audit / aile.platform.tenant.mapping / aile.platform.event.log |
| 核心介面 | /install /update /uninstall /rotate-secret(控制面) |
| /openapi/v1/(閘道器) | |
| /admin/integrations/(管理面) | |
| 統一簽名 | HmacSignatureService 替換三套 SignatureUtils |
| 事件鏈路 | EventPublishAppService + Pub/Sub publisher + EventLog |
| 遷移指令碼 | TenantAppModel → TenantIntegration / TenantMapping |
| 下線項 | aile-service-job 中的 TenantAppModel、/tenantapp/v1/、WebhookEventServiceImpl |
| 釋出範圍 | tenant. / user.* / service_number.* / contact.* / visitor.* / group.* / addressbook.* / notice.*(本期 notice 子事件含 delivered / failed / read / clicked / bounced / complained / task_completed / converted,詳 §10.2) |
| 不在本期 | scope 鑑權、nonce 防重放、KMS 加密、AIPower 反向能力呼叫、PENDING_USER_CONFIRM 反向回撥、域名白名單、灰度相容期 |
| 版本 | 日期 | 作者 | 修訂摘要 |
|---|---|---|---|
| v0.1 | — | chunlei zhu | 初版(已歸檔,不再維護) |
| v0.2 | 2026-05-20 | chunlei zhu | 系統性重寫:補齊安裝協議、閘道器層、事件三段鏈路;明確服務邊界為 Open Integration Layer(零業務領域邏輯);抽象外部租戶命名為 externalTenantId/externalSpaceId;鎖定本期不實現項 |
| v1.0 | 2026-05-21 | chunlei zhu | 定稿為開發實施版本:移除對其他設計文件的外部引用,將「與上位方案差異」改寫為本期最終設計決策,文件自包含,作為開發團隊實施指導依據 |
| v1.1 | 2026-05-21 | chunlei zhu | 引入服務號(serviceNumber)作為橫切上下文:§10.1 EventEnvelope 新增 scope 子結構(serviceNumberId);§10.2 事件清單按租戶級/服務號級分類並新增 contact.entered 等進線行為事件;§9.2 路由表新增 /openapi/v1/service-numbers/ 頂層入口與服務號巢狀資源路徑;§6.3 補充 contact ↔ service_number N:M 關係約定;§9.3 增加 SERVICE_NUMBER_FORBIDDEN 鑑權規則;明確 scope 與 serviceNumberId 均不參與簽名 |
| v1.2 | 2026-05-21 | chunlei zhu | 事件清單治理:統一服務號資源域命名為 service_number(ServiceNumber),取代原 service_account;§10.2 表格新增「中文說明」列,補齊每個資源域的業務含義;§6.3 / §9.3 / §十三 同步替換 service_account 表述;service_number.* 事件按服務號自身生命週期調整為 created / updated / deleted(原 bound / unbound 不屬於服務號自身動作,移除);contact 關注類事件 service_number_linked / unlinked 重新命名為 service_number_followed / unfollowed(語義為客戶是否關注該服務號),相應分類口徑同步調為「進線 / 關注行為」 |
| v1.3 | 2026-05-22 | chunlei zhu | 推播場景閉環擴充套件(為承接 AiReach 等推播類生態應用):§10.2 notice.* 資源域追加 5 項擴充套件事件 — notice.clicked(連結點選) / notice.bounced(投遞失敗子類:號碼無效 / 拒收 / 通道故障) / notice.complained(使用者標記騷擾) / notice.task_completed(整批傳送任務終態,可替代生態應用的輪詢) / notice.converted(Aile 側歸因轉化);§13 釋出範圍同步更新。本服務自身行為不變(仍按 EventEnvelope 封裝 → Pub/Sub → 獨立 webhook 服務投遞);由 aile-service-message 在對應業務時機發起 EventPublishClient 呼叫即可 |
| v1.4 | 2026-05-22 | chunlei zhu | NoticeContent 訊息契約 + notice.clicked 觸發規則固化(§10.2 補充段落):1) 明確 Aile 不維護預審模板,應用按 NoticeContent(title / image / describe + buttons[],四類 actionType)自行組合傳送;2) postback button payload 契約:appId(必填,路由接收方) + clickId(必填,業務語義) + extra(可選);3) notice.clicked payload 必含 postbackPayload(原樣透傳傳送方按鈕 payload)與可選 inbound 子結構(triggered / conversationId / reason);4) 點選 ↔ 進線聯動由 Aile 編排(aile-service-message 攔截點選 + aile-service-room 提供進線規則 → 填充 inbound 後發事件),生態應用收一條 event 即同時獲知點選 + 進線;5) 本服務僅在事件封裝環節保證上述結構透傳,NoticeContent / 點選捕獲路由 / 進線規則引擎實現在 aile-service-message + aile-service-room |
| v1.5 | 2026-05-26 | chunlei zhu | §9.2 路由清單具體化 + 群發主鏈替代 notices 寫入 + EntrySource owner 整合規範:1) §9.2 由萬用字元字首表(如 /openapi/v1/service-numbers/{snId}/contacts/**)重寫為具體 method + path 白名單清單,拆分為 §9.2.1 租戶級與 §9.2.2 服務號級兩張表,每條 API 必須顯式註冊,未登記一律 404;2) 群發主鏈固化為 /openapi/v1/service-numbers/{snId}/broadcasts(A1 建立 / A2 查詢 / A3 取消),取代原「透過 /service-numbers/{snId}/notices 寫入」的群發入口,服務號級 /notices/{noticeId} 僅保留只讀語義,沿用 Aile 現行服務號群發流水線;3) 新增聯絡人標籤整合介面(GET /contacts/labels / POST /contacts/labels:add / POST /contacts/labels:remove / GET /contacts/{contactId}/labels)對齊 AiReach D 類用例;4) 新增進線原因 OpenAPI(/openapi/v1/entry-sources CRUD + 狀態切換)允許整合方租戶級自助登記 EntrySource;新增 §9.4「EntrySource 整合規範」明確 owner.integrationAppId / tenantIntegrationId 由閘道器自動注入、不可篡改、解除安裝時級聯停用;觸發迴流按 owner 精確路由——僅建立該 EntrySource 的 integration 收到事件,與同租戶其他 integration 隔離;5) §10.2 事件清單新增 session.* 資源域(session.created / closed / transferred)承載 EntrySource 觸發迴流 |