版本: v1.0\ 最後更新: 2026-06-13\ 服務範圍: aile-service-account、aile-service-tenant、aile-service-room
Aile 的身份體系採用分層設計,以 Account(帳號) 為頂層統一身份,向下關聯 AppAccount(應用帳號)、TenantEmployee(員工)、TenantContact(客戶/聯繫人) 等租戶級身份。系統支援匿名訪客到實名用戶的平滑過渡,並透過 Scope 機制管理渠道級身份映射。
| 原則 | 說明 |
|---|---|
| Account 是統一身份錨點 | 所有租戶級身份(員工、客戶)都歸屬於一個 Account |
| 租戶隔離 | 同一 Account 在不同租戶中有獨立的 Employee 或 Contact 記錄 |
| 渠道多身份 | 同一 Account 可透過多個 channel+scopeId 訪問同一服務號 |
| 匿名可合併 | 匿名訪客的 Contact 資料可在實名登入後合併到實名 Account |
┌─────────────────────────────────────────────────────────┐
│ AccountModel │
│ (aile.account) — 頂層統一身份 │
│ 欄位: mobile, channel, status, isSystem, personRoomId │
│ 類型: Anonymous | NonRealName | RealName | System │
└────────────┬──────────────┬──────────────────────────────┘
│ │
┌──────▼──────┐ ┌───▼──────────────────────────┐
│ AppAccount │ │ AccountScopeModel │
│ Model │ │ (aile.account.scope) │
│ (aile. │ │ 帳號在渠道上的 scope 映射 │
│ account │ │ accountId ↔ channel ↔ scopeId│
│ .app) │ └───────────────────────────────┘
│ 應用帳號 │
└─────────────┘
┌──────────────────────────────────────────────────────────┐
│ 租戶層 (Tenant) │
│ │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ TenantEmployeeModel │ │ TenantContactModel │ │
│ │ (aile.tenant. │ │ (aile.tenant.contact) │ │
│ │ employee) │ │ 客戶 / 聯繫人 │ │
│ │ 員工 │ │ type: RealName | │ │
│ │ joinType: Guarantor │ │ Anonymous | │ │
│ │ | Invitation │ │ Independent │ │
│ └─────────┬───────────┘ └─────────────┬──────────────┘ │
│ │ │ │
└────────────┼─────────────────────────────┼────────────────┘
│ │
┌──────▼──────┐ ┌───────▼──────────────┐
│ServiceMember│ │ ServiceNumberScope │
│ Model │ │ Model │
│ 服務號成員 │ │ 服務號進線 scope │
│ privilege: │ │ customerId ↔ │
│ Owner / │ │ serviceNumberId ↔ │
│ Manager / │ │ channel ↔ scopeId │
│ Common │ │ ↔ roomId │
└─────────────┘ └───────────────────────┘
│
┌───────▼──────────────┐
│ServiceNumberRelation │
│ Model │
│ 服務號訂閱關係 │
│ customerId ↔ │
│ serviceNumberId ↔ │
│ accountId │
└───────────────────────┘MongoDB 集合:aile.account
// aile-api/aile-account-api/.../model/AccountModel.java
@Document("aile.account")
public class AccountModel extends BaseModel {
String mobile; // 手機號碼
String countryCode; // 國碼
Channel channel; // 註冊渠道
Status status; // Enable / Disable(Saga 歸併可標記為 Deleted)
Boolean isSystem; // 是否為系統帳號
String personRoomId; // 個人聊天室 ID(系統級)
}帳號類型透過 AccountType.get(AccountModel) 推導,非持久化欄位:
// aile-api/aile-account-api/.../enums/AccountType.java
public enum AccountType {
Anonymous, // 匿名用戶(Channel.isAnonymous() == true)
NonRealName, // 未實名用戶(無手機號)
RealName, // 實名用戶(有手機號)
System // 系統用戶(isSystem == true)
}// aile-api/aile-account-api/.../constant/LoginType.java
LINE(1), // LINE 登入
THIRD(2), // 第三方登入
OTP(3), // OTP 登入
PWD(4), // 密碼登入
ONCE_TOKEN(5), // 一次性 Token 登入
DEVICE(6), // 裝置登入
QRCODE(7), // 掃描登入
ACCESS(8), // 授權登入
SERVICE(9), // 服務登入
AUTO(10), // 自動登入
ANONYMOUS(11) // 匿名登入位置:aile-service/aile-service-account/.../service/impl/AccountServiceImpl.java
| 方法 | 說明 |
|---|---|
register(dto) | 註冊新帳號(手機號 + 渠道) |
login(dto) | 登入(支援多種登入方式) |
syncCreate(dto) | 同步建立(供跨服務調用) |
update(dto) | 更新帳號資訊 |
MongoDB 集合:aile.account.app
// aile-api/aile-account-api/.../model/AppAccountModel.java
@Document("aile.account.app")
@CompoundIndexes({
@CompoundIndex(name = "accountId_1_channel_1", def = "{'accountId': 1, 'channel': 1}"),
@CompoundIndex(name = "accountId_1_channel_1_status_1", def = "{'accountId': 1, 'channel': 1, 'status': 1}"),
@CompoundIndex(name = "accountId_1_updateTime_1", def = "{'accountId': 1, 'updateTime': 1}")
})
public class AppAccountModel extends BaseModel {
String accountId; // 關聯的 Account ID
String name; // 顯示名稱
Integer age; // 年齡
Gender gender; // 性別
String birthday; // 生日
List<String> interests;// 興趣標籤
Channel channel; // 渠道
String password; // 密碼(加密)
String avatarId; // 頭像 ID
Status status; // Enable / Disable
Boolean androidMute; // Android 靜音
Boolean iosMute; // iOS 靜音
Boolean pcMute; // PC 靜音
// ...
}accountId + channel 組合對應一個 AppAccountMongoDB 集合:aile.account.scope
// aile-api/aile-account-api/.../model/AccountScopeModel.java
@Document("aile.account.scope")
@CompoundIndexes({
@CompoundIndex(name = "scopeId_1_channel_1", def = "{'scopeId': 1, 'channel': 1}"),
@CompoundIndex(name = "accountId_1_channel_1_scopeId_1", def = "{'accountId': 1, 'channel': 1, 'scopeId': 1}")
})
public class AccountScopeModel extends BaseModel {
String scopeId; // 渠道側唯一 ID(如 LINE userId)
Channel channel; // 渠道
String accountId; // 關聯的 Account ID
String name; // 渠道側顯示名稱
}AccountScopeModel 是 Account 層級的渠道映射,與 Tenant 層級的 ServiceNumberScopeModel 互補:
| 層級 | 模型 | 用途 |
|---|---|---|
| Account 層 | AccountScopeModel | Account ↔ channel ↔ scopeId(登入級) |
| Tenant 層 | ServiceNumberScopeModel | Contact ↔ serviceNumberId ↔ channel ↔ scopeId(服務號級) |
MongoDB 集合:aile.tenant.employee
// aile-api/aile-tenant-api/.../model/TenantEmployeeModel.java
@Document("aile.tenant.employee")
public class TenantEmployeeModel extends BaseModel {
String name; // 姓名
String avatarId; // 頭像 ID
String mood; // 個性簽名
Integer age; // 年齡
Gender gender; // 性別
String birthday; // 生日
Status status; // Enable / Disable
String accountId; // 關聯的 Account ID
String tenantId; // 所屬租戶 ID
Channel channel; // 渠道
String personRoomId; // 個人聊天室 ID
String systemRoomId; // 系統聊天室 ID
TenantJoinType joinType; // 加入方式:Guarantor / Invitation
String openId; // 外部 OpenID
Boolean isJoinAile; // 是否已加入 Aile 生態
Boolean isBindAile; // 是否已綁定 Aile 生態
Boolean isCollectInfo; // 是否已收集資訊
String homePagePicId; // 主頁背景圖 ID
}// aile-api/aile-tenant-api/.../enums/TenantJoinType.java
public enum TenantJoinType {
Guarantor, // 擔保人邀請
Invitation // 邀請碼加入
}accountId 關聯 AccountModelAccount 登入
│
└── TenantBindService.send(TenantBindDto)
│
├── 向 Account 的個人聊天室發送「加入生態」卡片
├── Account 接受 → TenantBindService.collectInfo()
│ ├── 建立 TenantEmployeeModel
│ ├── isJoinAile = true
│ └── isBindAile = true
│
└── TenantBindService.check()
└── 檢查 Account 是否已加入該租戶MongoDB 集合:aile.tenant.contact
// aile-api/aile-tenant-api/.../model/TenantContactModel.java
@Document("aile.tenant.contact")
public class TenantContactModel extends BaseModel {
// 基本資料
String name; // 姓名
Integer age;
Gender gender;
String birthday;
String phone;
String email;
// 公司資料
String company;
Boolean isAileCompany;
String companyAddress;
String companyPhone;
String companyEmail;
String companyDuty;
String companyDepartment;
// 其他
BloodType bloodType;
MaritalStatus maritalStatus;
List<Language> languages;
String interests;
String alias; // 備註名
String description; // 描述
String businessCardId; // 名片 ID
String openId; // 外部 OpenID
String avatarId; // 頭像 ID
// 關聯
Status status;
String accountId; // 關聯的 Account ID
String tenantId; // 所屬租戶 ID
Channel channel; // 渠道
Boolean isJoinAile; // 是否已加入 Aile
Boolean isBindAile; // 是否已綁定
Boolean isCollectInfo; // 是否已收集資訊
// 類型
ContactType type; // RealName / Anonymous / Independent
}// aile-api/aile-tenant-api/.../enums/contact/ContactType.java
public enum ContactType {
RealName, // 實名會員(關聯了 Account)
Anonymous, // 匿名會員(未關聯 Account,僅有渠道 scopeId)
Independent // 獨立客戶(如 LineGroup 帶入的客戶,不屬於任何 Account)
}tenantId,沒有 serviceNumberIdtype = Anonymous 的客戶在實名登入後可被合併到 type = RealName 的客戶MongoDB 集合:aile.tenant.servicenumber.member
// aile-api/aile-tenant-api/.../model/servicenumber/ServiceMemberModel.java
@Document("aile.tenant.servicenumber.member")
public class ServiceMemberModel extends BaseModel {
String serviceNumberId; // 服務號 ID
String memberId; // 成員 ID(即 TenantEmployeeModel.id)
String accountId; // 關聯 Account
String tenantId; // 租戶 ID
ServiceNumberType serviceNumberType; // 服務號類型
ServiceMemberPrivilege privilege; // Owner / Manager / Common
Status status; // Enable / Disable
}// aile-api/aile-tenant-api/.../enums/ServiceMemberPrivilege.java
public enum ServiceMemberPrivilege {
Owner, // 擁有者
Manager, // 管理員
Common // 一般成員
}Account ──→ TenantEmployee ──→ ServiceMember
│
├── serviceNumberId
└── privilege (Owner / Manager / Common)一個員工可以同時是多個服務號的成員,每個服務號中有獨立的權限。
MongoDB 集合:aile.tenant.servicenumber.scope
// aile-api/aile-tenant-api/.../model/servicenumber/ServiceNumberScopeModel.java
@Document("aile.tenant.servicenumber.scope")
public class ServiceNumberScopeModel extends BaseModel {
String tenantId; // 租戶 ID
String accountId; // 帳號 ID
String customerId; // 客戶 ID(TenantContactModel.id)
Channel channel; // 渠道
String scopeId; // 渠道側唯一 ID(如 LINE userId 或 chat.code)
String serviceNumberId; // 服務號 ID
String code; // 渠道側主體 code
String name; // 渠道側顯示名稱
String avatarId; // 頭像 ID
String pictureUrl; // 頭像 URL
String roomId; // 對應的 Services Room ID
Status status; // Enable / Disable
}ServiceNumberScopeModel 是系統中最關鍵的橋接模型:
客戶 ──→ ServiceNumberScopeModel ──→ 服務號
│ │ │
│ ┌──────┴──────────┐ │
│ │ customerId │ │
│ │ serviceNumberId │ │
│ │ channel │ │
│ │ scopeId │ │
│ │ roomId │ │
│ └─────────────────┘ │
│ │
└── TenantContactModel ───────────────┘一個客戶可以在同一服務號下有多條 Scope 記錄(不同 channel),但同一 channel + scopeId + serviceNumberId 組合唯一。
MongoDB 集合:aile.tenant.servicenumber.relation
// aile-api/aile-tenant-api/.../model/servicenumber/ServiceNumberRelationModel.java
@Document("aile.tenant.servicenumber.relation")
public class ServiceNumberRelationModel extends BaseModel {
String serviceNumberId;
ServiceNumberType serviceNumberType;
String accountId; // 帳號 ID
String customerId; // 客戶 ID
String tenantId; // 租戶 ID
String roomId; // 聊天室 ID
ServiceOpenType openType;// 公開類型
ServiceNumberRelationStatus status; // 訂閱狀態
String alias; // 備註名
}| 模型 | 用途 | 粒度 |
|---|---|---|
ServiceNumberScopeModel | 渠道進線路由(實際收發消息) | channel + scopeId + serviceNumberId |
ServiceNumberRelationModel | 業務層訂閱/關注關係 | customerId + serviceNumberId |
1. 用戶透過 LINE 匿名進入
→ Gateway Webhook 事件
→ 若無對應 Account:建立 AccountModel(type=Anonymous)
→ 建立 TenantContactModel(type=Anonymous)
→ 建立 ServiceNumberScopeModel
→ 建立 ServiceNumberRelationModel// TenantContactModel.type 可為:
ContactType.Anonymous // 匿名訪客,未關聯實名 Account
ContactType.RealName // 實名客戶,關聯了實名 Account
ContactType.Independent // 獨立客戶(如 LineGroup 帶入,不經過 Account)當匿名訪客進行實名登入(如輸入手機號)時:
AnonymousVisitorMergePlanAdapter.resolve(anonymousAccountId, targetAccountId)
│
├── 查詢 anonymousAccountId 下的所有 TenantContactModel(status=Enable)
│
├── 逐個客戶生成 ContactMergePlan:
│ ├── 查找目標 Account 在該租戶是否已有 Contact(targetContact)
│ ├── 收集匿名客戶的 ServiceNumberRelation 列表
│ ├── 收集目標客戶的 ServiceNumberRelation 列表
│ └── 生成合併計劃(標記源客戶刪除、遷移關聯)
│
└── Saga 編排執行:
├── MergeAnonymousVisitorContactDataStep(合併客戶資料)
├── ProcessServiceSessionMergeStep(合併會話)
├── MergeLineGroupContactRelationStep(合併 LineGroup 關聯)
└── 其他相關 Step| Step | 說明 |
|---|---|
ResolveContactMergePlanStep | 解析合併計劃 |
MergeAnonymousVisitorContactDataStep | 合併匿名訪客客戶資料 |
ProcessServiceSessionMergeStep | 合併會話記錄 |
MergeLineGroupContactRelationStep | 合併 LineGroup 關聯 |
MarkSourceServiceSessionDeletedStep | 標記源會話刪除 |
GatewayCustomerDeleteStep | 通知 Gateway 清理舊客戶 |
// aile-api/aile-room-api/.../enums/RoomMemberType.java
public enum RoomMemberType {
User, // 一般用戶(員工或客戶)
ServiceNumber, // 服務號
Robot, // 機器人
Assistant, // AI 助手
Room // 聊天室本身(系統消息)
}
| 成員類型 | 對應實體 | 說明 |
|---|---|---|
User | TenantEmployeeModel 或 TenantContactModel | 房間中的人類成員 |
ServiceNumber | ServiceNumberModel | 服務號作為房間成員 |
Robot | 機器人帳號 | 自動回覆機器人 |
Assistant | TenantAssistantModel | AI 助手 |
┌──────────────────┐
│ AccountModel │
│ (aile.account) │
│ mobile, channel │
│ status, isSystem│
└───┬──────┬───────┘
│ │
┌───────────────┘ └───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ AppAccountModel │ │ AccountScopeModel │
│ (aile.account. │ │ (aile.account. │
│ app) │ │ scope) │
│ name, avatar │ │ channel, scopeId │
│ channel, mute │ └──────────────────────┘
└──────────────────┘
│ accountId
│
┌──────────┴─────────────────────────────────────┐
│ Tenant (租戶) │
│ │
│ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ TenantEmployeeModel │ │TenantContactModel│ │
│ │ (aile.tenant. │ │(aile.tenant. │ │
│ │ employee) │ │ contact) │ │
│ │ id → memberId │ │ id → customerId │ │
│ └─────────┬────────────┘ └────────┬─────────┘ │
│ │ │ │
└────────────┼────────────────────────┼───────────┘
│ │
┌──────▼──────┐ ┌──────▼───────────────┐
│ServiceMember│ │ServiceNumberScopeModel│
│ Model │ │ (aile.tenant. │
│ memberId │ │ servicenumber.scope) │
│ privilege │ │ customerId │
│ (Owner / │ │ serviceNumberId │
│ Manager / │ │ channel ↔ scopeId │
│ Common) │ │ roomId │
└─────────────┘ └──────────┬────────────┘
│
┌────────▼────────────┐
│ServiceNumberRelation │
│ Model │
│ (aile.tenant. │
│ servicenumber. │
│ relation) │
│ customerId │
│ serviceNumberId │
│ accountId │
│ status │
└──────────────────────┘LINE 用戶掃碼進入服務號
│
├── 無 Account → 建立 Anonymous Account
├── 無 TenantContact → 建立 Contact(type=Anonymous)
├── 建立 ServiceNumberScope(customerId + serviceNumberId + channel + scopeId)
├── 建立 ServiceNumberRelation(customerId + serviceNumberId + accountId)
└── 建立 Services Room → 開始會話Anonymous Account 輸入手機號進行實名登入
│
├── Account 升級:type: Anonymous → RealName(填入手機號)
├── 觸發 Saga:AnonymousVisitorMergePlanAdapter
│ ├── 查找該 Account 的所有 Anonymous Contact
│ ├── 對每個 Contact 生成合併計劃
│ └── Saga 編排執行(合併 Contact、合併 Session、合併 Relation)
└── Contact 升級:type: Anonymous → RealName已實名 Account 透過 LINE 進入新租戶的服務號
│
├── 查找 Account(依 channel + scopeId → AccountScope)
├── 在該租戶建立 TenantContactModel(type=RealName)
├── 建立 ServiceNumberScope
└── 建立 ServiceNumberRelation用戶登入 → 通過邀請/擔保加入租戶
│
├── 若無 Account → 建立 Account
├── 建立 TenantEmployeeModel(joinType=Guarantor/Invitation)
├── TenantBindService.send() → 發送加入生態卡片
├── 用戶接受 → isJoinAile=true, isBindAile=true
└── 可被加入 ServiceMember(成為服務號客服)LINE 群組事件(MemberJoin / Messaging)
│
├── 群組成員可能沒有 Account
├── 建立 TenantContactModel(type=Independent)
│ └── Independent:不關聯 Account,僅作為群組內的用戶身份
├── 建立 LineGroupContactRelationModel
└── 加入 LineGroup Room 為成員| 層級 | 檔案 | 說明 |
|---|---|---|
| API Model | aile-api/aile-account-api/.../model/AccountModel.java | 頂層帳號 |
| API Model | aile-api/aile-account-api/.../model/AppAccountModel.java | 應用帳號 |
| API Model | aile-api/aile-account-api/.../model/AccountScopeModel.java | 帳號渠道 Scope |
| API Enum | aile-api/aile-account-api/.../enums/AccountType.java | 帳號類型(Anonymous/NonRealName/RealName/System) |
| API Const | aile-api/aile-account-api/.../constant/LoginType.java | 11 種登入方式 |
| API Model | aile-api/aile-tenant-api/.../model/TenantEmployeeModel.java | 員工 |
| API Model | aile-api/aile-tenant-api/.../model/TenantContactModel.java | 客戶/聯繫人 |
| API Enum | aile-api/aile-tenant-api/.../enums/contact/ContactType.java | 客戶類型(RealName/Anonymous/Independent) |
| API Enum | aile-api/aile-tenant-api/.../enums/TenantJoinType.java | 加入方式 |
| API Model | aile-api/aile-tenant-api/.../model/servicenumber/ServiceMemberModel.java | 服務號成員 |
| API Enum | aile-api/aile-tenant-api/.../enums/ServiceMemberPrivilege.java | 服務號權限 |
| API Model | aile-api/aile-tenant-api/.../model/servicenumber/ServiceNumberScopeModel.java | 服務號進線 Scope |
| API Model | aile-api/aile-tenant-api/.../model/servicenumber/ServiceNumberRelationModel.java | 服務號訂閱 |
| API Enum | aile-api/aile-room-api/.../enums/RoomMemberType.java | 房間成員類型 |
| API Enum | aile-api/aile-job-api/.../enums/UserTypeEnums.java | 用戶類型(Employee/Contact/Visitor) |
| Service | aile-service/aile-service-account/.../service/impl/AccountServiceImpl.java | 帳號服務 |
| Service | aile-service/aile-service-tenant/.../service/impl/TenantBindServiceImpl.java | 租戶綁定服務 |
| Merge | aile-service/aile-service-tenant/.../merge/adapter/AnonymousVisitorMergePlanAdapter.java | 匿名訪客合併 |
| Saga | aile-service/aile-service-tenant/.../saga/step/MergeAnonymousVisitorContactDataStep.java | Saga 合併步驟 |
| Saga | aile-service/aile-service-tenant/.../saga/step/ResolveContactMergePlanStep.java | 合併計劃解析 |