aile-service-platform-integration 服務設計規格 v1.0
文件說明
本文件是 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 服務消費 - 實現 Aile 主導的安裝握手協議(
/install//update//uninstall//rotate-secret),完成租戶安裝例項的全生命週期管理 - 統一 HMAC-SHA256 簽名規則,消除當前 SignatureUtils 中三套不一致邏輯
- 替代當前 aile-service-job 中零散的
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 + ":" + signature
五、包結構設計(DDD 分層)
aile-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
六、領域模型設計
6.1 IntegrationApp(聚合根)
平臺級全域性應用定義。一個生態應用在全平臺僅一條記錄。
@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
}
6.2 TenantIntegration(聚合根,含狀態機)
租戶安裝例項。同一 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 呼叫(解除安裝/輪轉等控制面動作除外) - 所有狀態遷移須寫入獨立審計集合(見 6.4)
- 同一
aileTenantId + appId組合只能存在一個非DELETED記錄(應用層校驗 + 唯一索引) DELETED為終態,清理期結束後可物理清除appSecret與端點資訊
6.3 TenantMapping(實體)
跨系統租戶對映。
@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)
}
個人租戶 / 團隊租戶業務前提:
- 個人租戶在 Aile 側沿用團隊租戶的資料結構,前端按功能粒度做能力限制,以便未來一鍵升級為團隊租戶
- 一個賬號僅擁有且必擁有一個個人租戶——賬號↔個人租戶為一對一繫結關係,不存在多賬號共享同一個人租戶的場景
- 團隊租戶可包含多個賬號與多個空間(space)
對映規則:
- 個人租戶
- AIPower 側不為每個 Aile 個人租戶單獨建立外部租戶例項,所有 Aile 個人租戶共用同一
externalTenantId(=publicTenantId) ownerType = AILE_PERSONALownerId = ailePersonalTenantId(= 該賬號 ID,三者一一對應),作為 AIPower 共享租戶內的資料隔離鍵
- AIPower 側不為每個 Aile 個人租戶單獨建立外部租戶例項,所有 Aile 個人租戶共用同一
- 團隊租戶 → 每個團隊獨立
externalTenantId,可選externalSpaceId - 對映在安裝握手成功後根據 AIPower 返回資訊建立
- 對映表是事件路由與資料隔離的核心依據
Contact ↔ ServiceNumber 關係約定(本服務不直接持有 contact / visitor / service_number 領域模型,這些保留在 auth / tenant 等下游服務,但其關係約束直接影響事件 scope 與 OpenAPI 路徑設計,在此明確口徑):
- service_number(服務號,ServiceNumber) 是租戶內資源,一個租戶可擁有 1~N 個服務號,服務號本身歸屬唯一租戶。
- contact / visitor(客戶/訪客) 是租戶級主體,在租戶範圍內全域性唯一(visitor 實名歸戶後晉升為 contact)。
- 「進線」是 contact × service_number 的關係實體——同一個 contact 可以進入同租戶下的 1~N 個服務號,在每個服務號下獨立呈現為「該服務號的客戶」。
- 由此推論:
- 主體生命週期(contact.created / contact.updated / contact.deleted)是租戶級事件,不帶
scope.serviceNumberId。 - 進線 / 關注變更(contact.entered / contact.service_number_followed / contact.service_number_unfollowed)是服務號級事件,必填
scope.serviceNumberId;followed/unfollowed表達客戶主動關注 / 取消關注該服務號的狀態變遷。 - OpenAPI 路徑上 contact / visitor 資源始終巢狀在
/openapi/v1/service-numbers/{snId}/之下,因為外部生態應用看到的「客戶」始終是某個服務號下的檢視(詳見 §9.2)。
- 主體生命週期(contact.created / contact.updated / contact.deleted)是租戶級事件,不帶
6.4 TenantIntegrationAudit(審計實體,獨立 collection)
@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;
}