清理: - 删除 34 份过时文档(gap reports/QA临时报告/验收报告/阶段性文档) - 删除 docs/.hermes/skills 第三方 skills 副本(16 文件) - 删除 skills-lock.json 目录归集: - 根目录仅保留 README.md 索引 - product/ — 产品与架构设计(PRD + ARCHITECTURE + P2设计文档 + AI/企业路线图) - tracking/ — Chatwoot parity 开发跟踪 - requirements/ — M01-M12 模块需求 - plans/ — 历史实现计划 - parity/ — 路由 parity 与前端契约 - qa/ — QA 报告与测试计划 - ops/ — 运维部署 命名规范: - 全小写 kebab-case,禁止全大写文件名 - product/tracking/ops 用 NN- 序号前缀 - requirements 用 MNN- 两位零填充模块号 - plans/qa 用 YYYY-MM-DD- 日期前缀 - requirements M1-M9 零填充为 M01-M09(修复字典序) 同步更新: - backend/cmd/route_parity/main.go 路径默认值 - backend/scripts/parity_frontend_smoke.sh 报告路径 - 所有 docs 内部交叉引用 - .gitignore 排除编译产物 (backend/gochat, backend/route_parity) - 新增迁移 000052/000053 - 前端 WS 相关修改
557 lines
21 KiB
Markdown
557 lines
21 KiB
Markdown
# P2E — GoChat 认证授权与实时通信架构设计
|
||
|
||
> 版本: v1.1 | 更新日期: 2026-07-09
|
||
> 参照: Chatwoot DeviseTokenAuth / Pundit / ActionCable / Dispatcher / Listener
|
||
> 状态:设计文档,认证授权与 WebSocket 已落地。当前 auth 包含 JWT/OAuth/MFA/SAML/RBAC/Platform Auth。
|
||
|
||
---
|
||
|
||
## 1. JWT 认证体系
|
||
|
||
### 1.1 对比 Chatwoot DeviseTokenAuth
|
||
|
||
| 特性 | Chatwoot (Rails) | GoChat (Go) |
|
||
|---|---|---|
|
||
| 认证库 | DeviseTokenAuth gem | 自实现 JWT middleware |
|
||
| Token存储 | 多token机制(client_id+token对) | 单JWT + Refresh token |
|
||
| Token传递 | HTTP headers(access-token/client/uid) | Authorization: Bearer <jwt> |
|
||
| Token刷新 | 每次请求自动刷新(竞态锁定) | Refresh token 端点显式刷新 |
|
||
| 多账户 | Devise scope切换 | JWT claims 含 account_id |
|
||
| MFA | Devise two_factor_authentication | TOTP 验证中间件 |
|
||
| OAuth | Omniauth callbacks | OAuth2 redirect flow |
|
||
| SAML | Devise_saml_authenticatable | 自实现 SAML SP(企业版) |
|
||
|
||
### 1.2 JWT Token 结构
|
||
|
||
```go
|
||
// token/jwt.go
|
||
type Claims struct {
|
||
UserID uint `json:"user_id"`
|
||
AccountID uint `json:"account_id"` // 当前活跃账户
|
||
Role string `json:"role"` // agent/administrator/custom_role
|
||
Provider string `json:"provider"` // email/google/saml
|
||
CustomRoleID uint `json:"custom_role_id,omitempty"` // 企业版自定义角色
|
||
jwt.RegisteredClaims
|
||
}
|
||
```
|
||
|
||
**Access Token**: 15分钟过期,含完整Claims
|
||
**Refresh Token**: 7天过期,仅含 UserID + Provider,存储在Redis
|
||
|
||
### 1.3 认证流程
|
||
|
||
```
|
||
1. 用户登录 POST /api/v1/auth/login
|
||
→ 验证 email+password (bcrypt)
|
||
→ 查询 AccountUser 获取角色
|
||
→ 生成 Access Token + Refresh Token
|
||
→ 返回 {user, access_token, refresh_token}
|
||
|
||
2. 切换账户 POST /api/v1/auth/switch_account
|
||
→ JWT claims.account_id 更换
|
||
→ 重新签发 Access Token(新 account_id)
|
||
|
||
3. Token刷新 POST /api/v1/auth/refresh
|
||
→ 验证 Refresh Token
|
||
→ 重新签发 Access Token
|
||
|
||
4. OAuth登录 GET /api/v1/auth/:provider/callback
|
||
→ Google OAuth2 redirect → callback
|
||
→ 创建/查找 User + AccountUser
|
||
→ 签发 JWT
|
||
|
||
5. 企业版SAML (见 §4)
|
||
```
|
||
|
||
### 1.4 认证中间件
|
||
|
||
```go
|
||
// middleware/auth.go
|
||
func AuthMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
token := extractBearerToken(c)
|
||
claims, err := ValidateAccessToken(token)
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "unauthenticated"})
|
||
return
|
||
}
|
||
c.Set("current_user_id", claims.UserID)
|
||
c.Set("current_account_id", claims.AccountID)
|
||
c.Set("current_role", claims.Role)
|
||
c.Set("current_custom_role_id", claims.CustomRoleID)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
**认证级别分层:**
|
||
- `PublicAPI` — 无认证(WebWidget嵌入、CSAT提交、Portal文章)
|
||
- `AuthenticatedAPI` — AuthMiddleware(绝大多数API)
|
||
- `AdminAPI` — AuthMiddleware + RoleCheck("administrator")(账户设置、团队管理等)
|
||
- `EnterpriseAPI` — AuthMiddleware + FeatureFlagCheck("enterprise_*")(企业版功能)
|
||
- `SuperAdminAPI` — SuperAdmin middleware(平台管理API)
|
||
|
||
---
|
||
|
||
## 2. RBAC 权限系统
|
||
|
||
### 2.1 对比 Chatwoot Pundit + CustomRole
|
||
|
||
| 特性 | Chatwoot | GoChat |
|
||
|---|---|---|
|
||
| 权限框架 | Pundit (Policy类) | 中间件+Policy函数 |
|
||
| 内置角色 | agent / administrator | agent / administrator |
|
||
| 自定义角色 | CustomRole (企业版, 6权限维度) | CustomRole (企业版, 同6维度) |
|
||
| Policy检查 | `authorize @resource` in controller | `policy.Check(user, resource, action)` middleware |
|
||
| 范围过滤 | `scope = Policy::Scope.resolve` | `policy.Scope(user, resource)` 返回过滤条件 |
|
||
|
||
### 2.2 角色体系
|
||
|
||
```go
|
||
// model/role.go
|
||
type Role string
|
||
const (
|
||
RoleAgent Role = "agent"
|
||
RoleAdministrator Role = "administrator"
|
||
)
|
||
|
||
// 企业版 CustomRole
|
||
type CustomRole struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
AccountID uint `gorm:"index"`
|
||
Name string `gorm:"size:255"`
|
||
Permissions Permissions `gorm:"type:jsonb"` // 6维度权限
|
||
}
|
||
|
||
type Permissions struct {
|
||
ConversationManage PermissionLevel `json:"conversation_manage"` // full/read
|
||
ConversationDelete PermissionLevel `json:"conversation_delete"` // full/none
|
||
ContactManage PermissionLevel `json:"contact_manage"` // full/read
|
||
ReportManage PermissionLevel `json:"report_manage"` // full/none
|
||
KnowledgeBaseManage PermissionLevel `json:"knowledge_base_manage"` // full/read
|
||
AutomationManage PermissionLevel `json:"automation_manage"` // full/none
|
||
}
|
||
|
||
type PermissionLevel string
|
||
const (
|
||
PermissionFull PermissionLevel = "full"
|
||
PermissionRead PermissionLevel = "read"
|
||
PermissionNone PermissionLevel = "none"
|
||
)
|
||
```
|
||
|
||
### 2.3 权限检查实现
|
||
|
||
```go
|
||
// policy/base.go
|
||
type PolicyContext struct {
|
||
UserID uint
|
||
AccountID uint
|
||
Role Role
|
||
CustomRoleID uint
|
||
Permissions Permissions // 企业版加载
|
||
}
|
||
|
||
func (pc *PolicyContext) Can(action string, resource string) bool {
|
||
// administrator → 全权限
|
||
if pc.Role == RoleAdministrator {
|
||
return true
|
||
}
|
||
// agent → 基本权限
|
||
if pc.Role == RoleAgent {
|
||
return agentDefaultPermissions[action][resource]
|
||
}
|
||
// custom_role → 查权限矩阵
|
||
return checkCustomPermission(pc.Permissions, action, resource)
|
||
}
|
||
|
||
// 在中间件中注入
|
||
func PolicyMiddleware(resource string, action string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
pc := buildPolicyContext(c)
|
||
if !pc.Can(action, resource) {
|
||
c.AbortWithStatusJSON(403, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
c.Set("policy_context", pc)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.4 与Chatwoot对比的关键简化
|
||
|
||
1. **去除Pundit Policy类** — 每个资源一个Policy文件(如AccountPolicy, ConversationPolicy),GoChat统一为一个PolicyContext+权限矩阵
|
||
2. **Scope过滤合并** — Chatwoot的Policy::Scope(返回过滤后的数据集),GoChat用PolicyContext.Scope()返回GORM WHERE条件
|
||
3. **CustomRole权限合并** — Chatwoot的AccountUser.role + CustomRole双判断,GoChat在JWT claims中直接合并
|
||
|
||
---
|
||
|
||
## 3. AccountUser 多账户角色切换
|
||
|
||
### 3.1 模型设计
|
||
|
||
```go
|
||
// model/account_user.go
|
||
type AccountUser struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
AccountID uint `gorm:"index;not null"`
|
||
UserID uint `gorm:"index;not null"`
|
||
Role Role `gorm:"size:255;not null;default:'agent'"`
|
||
CustomRoleID uint `gorm:"index"` // 企业版
|
||
Availability string `gorm:"size:255;default:'offline'"` // online/offline/busy
|
||
AutoOffline bool `gorm:"default:false"`
|
||
AgentCapacityID uint `gorm:"index"` // 企业版容量策略
|
||
ActiveAt *time.Time
|
||
InvitedByID uint
|
||
CreatedAt time.Time
|
||
UpdatedAt time.Time
|
||
}
|
||
```
|
||
|
||
### 3.2 切换流程
|
||
|
||
```
|
||
1. 用户登录 → 默认选择第一个AccountUser对应的账户
|
||
2. JWT claims.account_id = 选中的账户ID
|
||
3. 切换账户 → POST /api/v1/auth/switch_account {account_id: N}
|
||
→ 验证 AccountUser 存在
|
||
→ 重新签发JWT(新account_id + 对应role)
|
||
→ 前端刷新所有数据
|
||
4. 每个API请求 → JWT claims决定当前账户上下文
|
||
```
|
||
|
||
对比Chatwoot:Chatwoot通过`Current.account`全局变量切换,GoChat通过JWT claims显式传递,避免全局状态。
|
||
|
||
---
|
||
|
||
## 4. Redis Pub/Sub 实时通信架构
|
||
|
||
### 4.1 对比 Chatwoot ActionCable
|
||
|
||
| 特性 | Chatwoot (ActionCable) | GoChat (Redis Pub/Sub + WebSocket) |
|
||
|---|---|---|
|
||
| 连接管理 | ActionCable Server (Rails内置) | gorilla/websocket + Redis subscriber |
|
||
| Channel订阅 | `ConversationChannel`, `AccountChannel` | Redis topic: `account:{id}`, `conversation:{id}` |
|
||
| 消息推送 | `broadcast_to` | Redis PUBLISH → WebSocket send |
|
||
| 连接认证 | `connect` 方法验证cookie | WebSocket握手时验证JWT |
|
||
| 并发 | 每连接一个Redis subscriber | 每账户一个Redis subscriber(共享) |
|
||
| 重连 | 客户端自动重连 | 客户端重连+服务器Redis重订阅 |
|
||
|
||
### 4.2 WebSocket 连接架构
|
||
|
||
```go
|
||
// realtime/hub.go
|
||
type Hub struct {
|
||
// account_id → set of websocket connections
|
||
AccountConns map[uint]*AccountRoom
|
||
// conversation_id → set of websocket connections
|
||
ConvConns map[uint]*ConvRoom
|
||
// Redis subscriber
|
||
RedisSub *redis.PubSub
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
type AccountRoom struct {
|
||
AccountID uint
|
||
Conns map[uint]*WSConn // user_id → connection
|
||
Sub *redis.PubSub // 订阅 account:{id} topic
|
||
}
|
||
|
||
type WSConn struct {
|
||
UserID uint
|
||
AccountID uint
|
||
Conn *websocket.Conn
|
||
Send chan []byte
|
||
}
|
||
```
|
||
|
||
### 4.3 Redis Topic 设计
|
||
|
||
```
|
||
Topics:
|
||
account:{account_id} — 账户级事件(新对话、通知、状态变化)
|
||
conversation:{conv_id} — 对话级事件(新消息、状态流转、打字状态)
|
||
inbox:{inbox_id} — Inbox级事件(新对话分配)
|
||
user:{user_id} — 用户级事件(个人通知)
|
||
captain:{assistant_id} — AI助手事件(企业版)
|
||
copilot:{user_id}:{conv_id} — Copilot建议推送(企业版)
|
||
```
|
||
|
||
### 4.4 事件消息格式
|
||
|
||
```go
|
||
// realtime/event.go
|
||
type RealtimeEvent struct {
|
||
Type string `json:"type"` // message_created, conversation_updated, etc.
|
||
ActorID uint `json:"actor_id"`
|
||
Resource string `json:"resource"` // conversation, message, notification, etc.
|
||
Data json.RawMessage `json:"data"` // 资源JSON
|
||
Timestamp time.Time `json:"timestamp"`
|
||
}
|
||
```
|
||
|
||
### 4.5 连接流程
|
||
|
||
```
|
||
1. 客户端 WebSocket握手 → ws://host/cable?token=<jwt>
|
||
→ 验证JWT → 提取user_id+account_id
|
||
→ 注册到Hub.AccountConns[account_id]
|
||
|
||
2. 客户端订阅对话 → subscribe {conversation_id: N}
|
||
→ Hub注册到ConvConns[N]
|
||
→ Redis SUBSCRIBE conversation:N
|
||
|
||
3. 事件到达 → Redis消息 → Hub.Dispatch()
|
||
→ 根据topic路由到对应Room
|
||
→ Room广播到所有连接的WSConn
|
||
→ WSConn.Send channel → websocket.WriteJSON
|
||
|
||
4. 断连 → Hub注销 → Redis UNSUBSCRIBE
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 事件分发器设计
|
||
|
||
### 5.1 对比 Chatwoot Dispatcher/Listener
|
||
|
||
Chatwoot 采用 **Dispatcher → Listener** 模式:
|
||
|
||
```
|
||
Dispatcher.dispatch(event_name, timestamp, event_data)
|
||
→ 构造 Event对象
|
||
→ Redis PUBLISH "chatwoot_events:{account_id}"
|
||
→ 各Listener订阅Redis channel
|
||
→ Listener#process(event) 执行业务逻辑
|
||
```
|
||
|
||
**12个Listener:**
|
||
- ActionCableListener → WebSocket推送
|
||
- AgentBotListener → 触发Bot响应
|
||
- AutomationRuleListener → 触发自动化规则
|
||
- CampaignListener → 触发营销活动
|
||
- CsatSurveyListener → 发送满意度调查
|
||
- HookListener → 触发自定义Webhook
|
||
- InstallationWebhookListener → 触发平台Webhook
|
||
- NotificationListener → 创建通知
|
||
- ParticipationListener → 更新参与状态
|
||
- ReportingEventListener → 记录报告事件
|
||
- WebhookListener → 发送Webhook回调
|
||
- BaseListener → 提供订阅基础设施
|
||
|
||
### 5.2 GoChat Event Dispatcher 设计
|
||
|
||
```go
|
||
// event/dispatcher.go
|
||
|
||
// 事件类型枚举
|
||
type EventType string
|
||
const (
|
||
EventMessageCreated EventType = "message.created"
|
||
EventMessageUpdated EventType = "message.updated"
|
||
EventConversationCreated EventType = "conversation.created"
|
||
EventConversationUpdated EventType = "conversation.updated"
|
||
EventConversationAssigned EventType = "conversation.assigned"
|
||
EventContactCreated EventType = "contact.created"
|
||
EventContactUpdated EventType = "contact.updated"
|
||
EventAgentAssigned EventType = "agent.assigned"
|
||
EventCSATSubmitted EventType = "csat.submitted"
|
||
// ... 更多
|
||
)
|
||
|
||
type Event struct {
|
||
Type EventType `json:"type"`
|
||
AccountID uint `json:"account_id"`
|
||
Data json.RawMessage `json:"data"`
|
||
Timestamp time.Time `json:"timestamp"`
|
||
}
|
||
|
||
type Dispatcher struct {
|
||
Redis *redis.Client
|
||
}
|
||
|
||
func (d *Dispatcher) Dispatch(event Event) error {
|
||
// 1. 发布到Redis(异步Listener消费)
|
||
topic := fmt.Sprintf("events:account:%d", event.AccountID)
|
||
payload, _ := json.Marshal(event)
|
||
return d.Redis.Publish(ctx, topic, payload).Err()
|
||
}
|
||
```
|
||
|
||
### 5.3 Listener Handler 注册
|
||
|
||
```go
|
||
// event/listener.go
|
||
|
||
type EventHandler func(event Event) error
|
||
|
||
type ListenerHub struct {
|
||
handlers map[EventType][]EventHandler
|
||
}
|
||
|
||
func RegisterHandler(eventType EventType, handler EventHandler) {
|
||
globalHub.handlers[eventType] = append(globalHub.handlers[eventType], handler)
|
||
}
|
||
|
||
// GoChat的Listener注册(替代Chatwoot的独立Listener文件)
|
||
func init() {
|
||
RegisterHandler(EventMessageCreated, HandleRealtimePush)
|
||
RegisterHandler(EventMessageCreated, HandleNotification)
|
||
RegisterHandler(EventMessageCreated, HandleWebhook)
|
||
RegisterHandler(EventMessageCreated, HandleAutomationRule)
|
||
RegisterHandler(EventMessageCreated, HandleReporting)
|
||
RegisterHandler(EventMessageCreated, HandleAgentBot)
|
||
RegisterHandler(EventConversationCreated, HandleAutoAssignment)
|
||
RegisterHandler(EventConversationCreated, HandleRealtimePush)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 5.4 Redis Consumer 启动
|
||
|
||
```go
|
||
// event/consumer.go
|
||
func StartConsumer(accountID uint) {
|
||
sub := redis.Subscribe(ctx, fmt.Sprintf("events:account:%d", accountID))
|
||
for msg := range sub.Channel() {
|
||
event := parseEvent(msg.Payload)
|
||
handlers := globalHub.handlers[event.Type]
|
||
for _, h := range handlers {
|
||
go h(event) // 异步执行,不阻塞
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.5 对比总结
|
||
|
||
| 方面 | Chatwoot | GoChat |
|
||
|---|---|---|
|
||
| 事件发布 | Dispatcher → Redis PUBLISH | Dispatcher → Redis PUBLISH(相同) |
|
||
| 事件消费 | 12个独立Listener类 | Handler函数注册表(更轻量) |
|
||
| 执行方式 | Sidekiq异步Job | goroutine异步执行 |
|
||
| 注册方式 | Rails autoload | init()函数静态注册 |
|
||
| 水平扩展 | Sidekiq worker进程 | Consumer goroutine per account |
|
||
|
||
---
|
||
|
||
## 6. 企业版 SAML SSO 设计
|
||
|
||
### 6.1 对比 Chatwoot
|
||
|
||
Chatwoot企业版SAML实现:
|
||
- `AccountSamlSettings` 模型存储IdP配置
|
||
- `DeviseSamlAuthenticatable` gem处理SAML流程
|
||
- `SamlUserBuilder` 构建User对象
|
||
- `Saml::UpdateAccountUsersProviderJob` 批量更新provider
|
||
|
||
### 6.2 GoChat SAML SP 实现
|
||
|
||
```go
|
||
// enterprise/auth/saml.go
|
||
|
||
type AccountSamlSettings struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
AccountID uint `gorm:"uniqueIndex;not null"`
|
||
IdpEntityID string `gorm:"size:512"`
|
||
IdpSsoTargetURL string `gorm:"size:512"`
|
||
IdpSloTargetURL string `gorm:"size:512"`
|
||
IdpCertificate string `gorm:"type:text"`
|
||
SpEntityID string `gorm:"size:512"`
|
||
SpAssertionURL string `gorm:"size:512"` // 自动生成
|
||
SpX509Certificate string `gorm:"type:text"` // 自签证书
|
||
SpPrivateKey string `gorm:"type:text"` // RSA私钥
|
||
RoleMappings json.RawMessage `gorm:"type:jsonb"` // SAML属性→角色映射
|
||
Active bool `gorm:"default:true"`
|
||
}
|
||
|
||
// SAML 流程:
|
||
// 1. SP-initiated: GET /api/v1/auth/saml/{account_id}/login
|
||
// → 生成SAML AuthnRequest → 重定向到IdP
|
||
// 2. IdP回调: POST /api/v1/auth/saml/{account_id}/callback
|
||
// → 解析SAML Response → 验证签名
|
||
// → SamlUserBuilder.Create/Find → 签发JWT
|
||
// 3. SLO: GET /api/v1/auth/saml/{account_id}/logout
|
||
// → 生成SAML LogoutRequest → 重定向到IdP SLO URL
|
||
|
||
type SamlUserBuilder struct {
|
||
Settings AccountSamlSettings
|
||
}
|
||
|
||
func (b *SamlUserBuilder) Build(samlResponse *SamlResponse) (*User, error) {
|
||
email := samlResponse.GetAttribute("email")
|
||
name := samlResponse.GetAttribute("name")
|
||
role := b.mapRole(samlResponse.GetAttribute("role"))
|
||
|
||
// 查找或创建User
|
||
user, err := FindOrCreateUserByEmail(email, name, "saml")
|
||
// 创建/更新AccountUser
|
||
EnsureAccountUser(user.ID, b.Settings.AccountID, role)
|
||
return user, err
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 架构全景图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ GoChat 单体架构 │
|
||
│ │
|
||
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||
│ │ Auth │ │ RBAC │ │ API │ │Event │ │
|
||
│ │ JWT │ │Polic │ │ Gin │ │Disp │ │
|
||
│ └──────┘ └──────┘ └──────┘ └──────┘ │
|
||
│ │ │ │ │ │
|
||
│ ▼ ▼ ▼ ▼ │
|
||
│ ┌─────────────────────────────────────┐ │
|
||
│ │ Service Layer │ │
|
||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
|
||
│ │ │Conv│ │Msg │ │Cntc│ │Auto│ │ │
|
||
│ │ └────┘ └────┘ └────┘ └────┘ │ │
|
||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
|
||
│ │ │Rprt│ │Ntfy│ │Team│ │Capn│ │ │
|
||
│ │ └────┘ └────┘ └────┘ └────┘ │ │
|
||
│ └─────────────────────────────────────┘ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
|
||
│ │ GORM/ │ │ Redis │ │ Channel │ │
|
||
│ │ PgSQL │ │ Pub/Sub │ │ Registry │ │
|
||
│ └─────────┘ └──────────┘ └───────────┘ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌─────────────────────────────────────┐ │
|
||
│ │ WebSocket Hub + Event Listener │ │
|
||
│ └─────────────────────────────────────┘ │
|
||
│ │
|
||
│ 企业版模块: │
|
||
│ ┌──────────┐ ┌────────┐ ┌──────┐ ┌──────┐ │
|
||
│ │ SAML SSO │ │CustomR │ │ SLA │ │Call │ │
|
||
│ └──────────┘ └────────┘ └──────┘ └──────┘ │
|
||
│ ┌──────────┐ ┌────────┐ ┌──────┐ │
|
||
│ │ Captain │ │Copilot │ │Audit │ │
|
||
│ └──────────┘ └────────┘ └──────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 关键设计决策总结
|
||
|
||
| # | 决策 | 原因 | 对比Chatwoot |
|
||
|---|---|---|---|
|
||
| 1 | JWT替代DeviseTokenAuth | Go无对应gem,JWT更标准 | 多token→单token+refresh |
|
||
| 2 | PolicyContext替代Pundit | 统一权限检查入口 | 12个Policy类→1个PolicyContext |
|
||
| 3 | Handler注册表替代Listener类 | Go无Rails autoload,函数注册更轻量 | 12个Listener→Handler map |
|
||
| 4 | JWT claims传递账户上下文 | 避免全局状态 | Current.account全局→JWT claims |
|
||
| 5 | Redis Pub/Sub+WebSocket | Go生态成熟选择 | ActionCable→自定义WS Hub |
|
||
| 6 | goroutine异步替代Sidekiq | 单体架构内异步足够 | Sidekiq进程→goroutine |
|
||
| 7 | 共享Redis subscriber | 减少Redis连接数 | 每连接1subscriber→每账户1 |
|
||
| 8 | 企业版SAML自实现 | Go无成熟SAML SP库 | DeviseSamlAuthenticatable→自实现 |
|
||
|
||
---
|
||
|
||
> **下一步:** P2B(数据库设计)和 P2C(路由设计)完成后,P2架构设计阶段完整收官。 |