Files
gochat/docs/product/04-design-database.md
T
rogee 0dabb8cfa5 docs: 整理文档目录结构 — 清理过时文档、归集功能子目录、统一命名规范
清理:
- 删除 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 相关修改
2026-07-09 14:53:27 +08:00

1475 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P2B — GoChat 数据库设计文档
> 版本: v1.1 | 更新日期: 2026-07-09
> 参照: Chatwoot db/schema.rb (87表) + M1-M12需求文档
> 技术选型: GORM + PostgreSQL
> 状态:设计文档,数据库已实际落地。当前 53 个编号迁移(backend/migrations/),表结构以迁移文件为权威。
---
## 设计原则
1. **精简合并** — 去除Rails元数据表(schema_migrations, ar_internal_metadata, active_storage系列, action_mailbox系列),Chatwoot87表 → GoChat约70表
2. **Go惯例** — snake_case表名,模型CamelCase,枚举用string+常量而非Rails integer flags
3. **GORM特性** — 利用gorm:"type:jsonb"替代Rails serialized属性,gorm:"index"替代add_index
4. **时间统一** — time.Time替代Rails datetime/timestamp
5. **外键声明式** — GORM关联替代Rails references/belongs_to
6. **企业版标注** — 🔒标记企业版表/字段
---
## M1: 核心基础表
### users
```go
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Email string `gorm:"size:255;uniqueIndex;not null"`
PasswordDigest string `gorm:"size:255;not null"` // bcrypt
Provider string `gorm:"size:255;default:'email'"` // email/google/saml 🔒
UID string `gorm:"size:255"` // OAuth UID
AvatarURL string `gorm:"size:512"`
Availability string `gorm:"size:50;default:'offline'"` // online/offline/busy
AutoOffline bool `gorm:"default:false"`
Type string `gorm:"size:50;default:'user'"` // user/super_admin
Locale string `gorm:"size:10;default:'en'"`
DisplayName string `gorm:"size:255"`
PhoneNumber string `gorm:"size:50"`
ConfirmedAt *time.Time
LastSignInAt *time.Time
LastSignInIP string `gorm:"size:255"`
CurrentSignInIP string `gorm:"size:255"`
SignInCount int `gorm:"default:0"`
TOTPSecret string `gorm:"size:255"` // 🔒 MFA
TOTPEnabled bool `gorm:"default:false"` // 🔒 MFA
CustomAttributes json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
AccountUsers []AccountUser `gorm:"foreignKey:UserID"`
AssignedConvs []Conversation `gorm:"foreignKey:AssigneeID"`
}
```
**对比Chatwoot**: Devise字段合并(encrypted_password→PasswordDigest, current_sign_in_at→LastSignInAt等);删除Devise冗余字段(reset_password_token/remember_token等);新增TOTP字段直接内嵌而非独立模型
### accounts
```go
type Account struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Locale string `gorm:"size:10;default:'en'"`
Domain string `gorm:"size:255"`
SupportEmail string `gorm:"size:255"`
LogoURL string `gorm:"size:512"`
Features json.RawMessage `gorm:"type:jsonb;default:'{}'"` // Feature flags
AutoResolveDuration int `gorm:"default:0"` // 自动关闭天数
Namespace string `gorm:"size:255"`
BrandingEnabled bool `gorm:"default:false"` // 🔒
BrandingColors json.RawMessage `gorm:"type:jsonb"` // 🔒
BrandingLogoURL string `gorm:"size:512"` // 🔒
BrandingPageURL string `gorm:"size:512"` // 🔒
CustomCustomAttributesEnabled bool `gorm:"default:false"` // 🔒
CreatedAt time.Time
UpdatedAt time.Time
Users []AccountUser `gorm:"foreignKey:AccountID"`
Inboxes []Inbox `gorm:"foreignKey:AccountID"`
Teams []Team `gorm:"foreignKey:AccountID"`
Conversations []Conversation `gorm:"foreignKey:AccountID"`
}
```
**对比**: features从FlagShihTzu位运算→jsonb KV存储,更直观;branding字段从独立方法→内嵌字段
### account_users
```go
type AccountUser struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
Role string `gorm:"size:50;not null;default:'agent'"` // agent/administrator 🔒+custom_role_id
CustomRoleID uint `gorm:"index"` // 🔒 企业版自定义角色
Availability string `gorm:"size:50;default:'offline'"` // online/offline/busy
AutoOffline bool `gorm:"default:false"`
ActiveAt *time.Time
InvitedByID uint `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
User User `gorm:"foreignKey:UserID"`
CustomRole *CustomRole `gorm:"foreignKey:CustomRoleID"` // 🔒
}
```
### custom_roles 🔒
```go
type CustomRole struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Permissions json.RawMessage `gorm:"type:jsonb;not null"` // 6维度权限矩阵
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
AccountUsers []AccountUser `gorm:"foreignKey:CustomRoleID"`
}
```
### companies 🔒
```go
type Company struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
WebsiteURL string `gorm:"size:512"`
FaviconURL string `gorm:"size:512"`
Domain string `gorm:"size:255"`
LastActivityAt *time.Time
CustomAttributes json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Contacts []Contact `gorm:"many2many:company_contacts"` // 🔒
}
```
### agent_capacity_policies 🔒
```go
type AgentCapacityPolicy struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
AssignmentLogic string `gorm:"size:50"` // round_robin/least_busy
ExclusionRules json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
AccountUsers []AccountUser `gorm:"foreignKey:AgentCapacityID"` // 🔒
}
```
### account_saml_settings 🔒
```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"`
SpX509Certificate string `gorm:"type:text"`
SpPrivateKey string `gorm:"type:text"`
RoleMappings json.RawMessage `gorm:"type:jsonb"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
}
```
---
## M2: Inbox 与渠道表
### inboxes
```go
type Inbox struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
ChannelType string `gorm:"size:50;not null;index"` // Channel::WebWidget等
ChannelID uint `gorm:"not null;index"` // polymorphic channel关联
AvatarURL string `gorm:"size:512"`
Greeting string `gorm:"type:text"`
GreetingEnabled bool `gorm:"default:false"`
WelcomeMessage string `gorm:"type:text"`
WelcomeTitle string `gorm:"size:255"`
CSATEnabled bool `gorm:"default:false"`
CSATMessage string `gorm:"type:text"`
AutoAssignmentEnabled bool `gorm:"default:false"`
EnableEmailCollect bool `gorm:"default:true"`
AgentBotID *uint `gorm:"index"`
WorkingHoursEnabled bool `gorm:"default:false"` // 🔒
OutOfOfficeMessage string `gorm:"type:text"`
LockChatDuringOOO bool `gorm:"default:false"` // 🔒
EmailAddress string `gorm:"size:255"`
EnableAutoTopicCreationOnReopen bool `gorm:"default:false"`
AllowMessagesAfterResolved bool `gorm:"default:false"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Channel Channelable `gorm:"polymorphic:Channel"` // 多态关联
Members []InboxMember `gorm:"foreignKey:InboxID"`
Conversations []Conversation `gorm:"foreignKey:InboxID"`
}
```
**对比**: Chatwoot的polymorphic `channelable` concern → GORM polymorphic关联;lock_chat_after_resolved合并多个标志位
### inbox_members
```go
type InboxMember struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
User User `gorm:"foreignKey:UserID"`
}
```
### agent_bots
```go
type AgentBot struct {
ID uint `gorm:"primaryKey"`
AccountID *uint `gorm:"index"`
Name string `gorm:"size:255;not null"`
AvatarURL string `gorm:"size:512"`
BotType string `gorm:"size:50"` // web_widget/api
Config json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
Inboxes []AgentBotInbox `gorm:"foreignKey:AgentBotID"`
}
```
### agent_bot_inboxes
```go
type AgentBotInbox struct {
ID uint `gorm:"primaryKey"`
AgentBotID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### channel_web_widgets
```go
type ChannelWebWidget struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
WebsiteToken string `gorm:"size:255;uniqueIndex;not null"`
WebsiteURL string `gorm:"size:512"`
WelcomeHeading string `gorm:"size:255"`
WelcomeTagline string `gorm:"type:text"`
PreChatFormEnabled bool `gorm:"default:false"`
PreChatFormMessage string `gorm:"type:text"`
PreChatFormOptions json.RawMessage `gorm:"type:jsonb"`
HMACToken string `gorm:"size:255"`
ReplyTime string `gorm:"size:50"` // in_a_few_minutes/in_a_few_hours/in_a_day
ContinuousPolyfill bool `gorm:"default:false"`
FeatureFlags json.RawMessage `gorm:"type:jsonb"`
ReferrerHost string `gorm:"size:255"`
WidgetColor string `gorm:"size:50"`
AvatarURL string `gorm:"size:512"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_telegrams
```go
type ChannelTelegram struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
BotToken string `gorm:"size:255;not null"`
BotName string `gorm:"size:255"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_facebook_pages
```go
type ChannelFacebookPage struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
PageID string `gorm:"size:255;not null"`
UserAccessToken string `gorm:"size:512;not null"` // encrypted 🔒
PageAccessToken string `gorm:"size:512;not null"` // encrypted 🔒
ReauthorizationRequired bool `gorm:"default:false"` // 🔒
MarkSeen bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_whatsapp
```go
type ChannelWhatsapp struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
PhoneNumberID string `gorm:"size:255"`
BusinessID string `gorm:"size:255"`
WhatsAppBusinessAccountID string `gorm:"size:255"`
Provider string `gorm:"size:50;default:'whatsapp_cloud'"` // 360dialog/whatsapp_cloud
ProviderConfig json.RawMessage `gorm:"type:jsonb"`
MessageTemplates json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_email
```go
type ChannelEmail struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
Email string `gorm:"size:255;not null"`
IMAPAddress string `gorm:"size:255"`
IMAPPort int
IMAPLogin string `gorm:"size:255"`
IMAPPassword string `gorm:"size:255"` // encrypted
IMAPSSL bool `gorm:"default:true"`
SMTPAddress string `gorm:"size:255"`
SMTPPort int
SMTPLogin string `gorm:"size:255"`
SMTPPassword string `gorm:"size:255"` // encrypted
SMTPSSL bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_api
```go
type ChannelAPI struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_twilio_sms
```go
type ChannelTwilioSMS struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
PhoneNumber string `gorm:"size:255;not null"`
AccountSID string `gorm:"size:255;not null"`
AuthToken string `gorm:"size:255;not null"` // encrypted
Medium string `gorm:"size:50;default:'sms'"` // sms/whatsapp
ContentSID string `gorm:"size:255"`
ContentVariables string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### channel_line / channel_sms / channel_instagram / channel_tiktok
```go
// 结构类似,各有渠道特有字段
type ChannelLine struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
ChannelID string `gorm:"size:255;not null"`
ChannelSecret string `gorm:"size:255;not null"`
CallbackURL string `gorm:"size:512"`
CreatedAt time.Time; UpdatedAt time.Time
}
type ChannelSms struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
PhoneNumber string `gorm:"size:255;not null"`
Provider string `gorm:"size:50"` // bandwidth
ProviderConfig json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time; UpdatedAt time.Time
}
type ChannelInstagram struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
IGBusinessAccountID string `gorm:"size:255;not null"`
AccessToken string `gorm:"size:512"` // encrypted 🔒
ReauthorizationRequired bool `gorm:"default:false"` // 🔒
CreatedAt time.Time; UpdatedAt time.Time
}
type ChannelTiktok struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"uniqueIndex;not null"`
AccessToken string `gorm:"size:512"` // encrypted 🔒
CreatedAt time.Time; UpdatedAt time.Time
}
```
### inbox_assignment_policies 🔒 / inbox_capacity_limits 🔒
```go
type InboxAssignmentPolicy struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
PolicyType string `gorm:"size:50"` // round_robin/least_busy
Active bool `gorm:"default:true"`
CreatedAt time.Time; UpdatedAt time.Time
}
type InboxCapacityLimit struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
CapacityLimit int `gorm:"not null"`
CreatedAt time.Time; UpdatedAt time.Time
}
```
### working_hours
```go
type WorkingHour struct {
ID uint `gorm:"primaryKey"`
InboxID uint `gorm:"not null;index"`
DayOfWeek int `gorm:"not null"` // 0-6
OpenHour int `gorm:"not null"`
OpenMin int `gorm:"not null"`
CloseHour int `gorm:"not null"`
CloseMin int `gorm:"not null"`
ClosedAllDay bool `gorm:"default:false"`
CreatedAt time.Time; UpdatedAt time.Time
}
```
---
## M3: 对话与消息表
### conversations
```go
type Conversation struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
AssigneeID *uint `gorm:"index"`
TeamID *uint `gorm:"index"`
ContactID uint `gorm:"not null;index"`
ContactInboxID uint `gorm:"not null;index"`
Status string `gorm:"size:50;not null;default:'open';index"` // open/resolved/pending/snoozed
Priority *string `gorm:"size:50"` // urgent/high/medium/low
UUID string `gorm:"size:255;uniqueIndex"`
LastMessageID *uint
LastMessageAt *time.Time `gorm:"index"`
LastNonActivityMessageAt *time.Time
AdditionalAttributes json.RawMessage `gorm:"type:jsonb"`
CustomAttributes json.RawMessage `gorm:"type:jsonb"`
LabelList []string `gorm:"type:text[];serializer:json"` // PG array
Muted bool `gorm:"default:false"`
SnoozedUntil *time.Time
UnreadCount int `gorm:"default:0"`
FirstReplyCreatedAt *time.Time
SLAAppliedID *uint `gorm:"index"` // 🔒
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Inbox Inbox `gorm:"foreignKey:InboxID"`
Assignee *User `gorm:"foreignKey:AssigneeID"`
Team *Team `gorm:"foreignKey:TeamID"`
Contact Contact `gorm:"foreignKey:ContactID"`
ContactInbox ContactInbox `gorm:"foreignKey:ContactInboxID"`
Messages []Message `gorm:"foreignKey:ConversationID"`
Participants []ConversationParticipant `gorm:"foreignKey:ConversationID"`
}
```
### messages
```go
type Message struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
MessageType string `gorm:"size:50;not null;index"` // incoming/outgoing/template/activity/private
Content string `gorm:"type:text"`
ContentType string `gorm:"size:50;default:'text'"` // text/image/audio/video/file/location/emoji
SourceID string `gorm:"size:255;index"`
SenderID *uint `gorm:"index"`
SenderType string `gorm:"size:50"` // Contact/User/AgentBot
Private bool `gorm:"default:false"`
Status string `gorm:"size:50;default:'sent'"` // sent/delivered/read/failed
ExternalSourceID string `gorm:"size:255"`
AdditionalAttributes json.RawMessage `gorm:"type:jsonb"`
CustomAttributes json.RawMessage `gorm:"type:jsonb"`
Conversation Conversation `gorm:"foreignKey:ConversationID"`
ParentMessageID *uint `gorm:"index"` // 回复引用
Sender Senderable `gorm:"polymorphic:Sender"`
Attachments []Attachment `gorm:"foreignKey:MessageID"`
RepliedBy []Message `gorm:"foreignKey:ParentMessageID"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### conversation_participants
```go
type ConversationParticipant struct {
ID uint `gorm:"primaryKey"`
ConversationID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
LastReadAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
Conversation Conversation `gorm:"foreignKey:ConversationID"`
User User `gorm:"foreignKey:UserID"`
}
```
### attachments
```go
type Attachment struct {
ID uint `gorm:"primaryKey"`
MessageID uint `gorm:"not null;index"`
FileType string `gorm:"size:50"` // image/audio/video/file
ExternalURL string `gorm:"size:512"`
FileURL string `gorm:"size:512"` // 本地存储路径
FileSize int64
ThumbURL string `gorm:"size:512"`
FileName string `gorm:"size:255"`
Width int
Height int
CreatedAt time.Time
Message Message `gorm:"foreignKey:MessageID"`
}
```
**对比**: Chatwoot用ActiveStorage(独立blob/attachment表)→ GoChat简化为单一Attachment表,FileURL直接指向本地/云存储路径
### message_endorsements
```go
// 简化:仅保留copilot建议的认可记录
type MessageEndorsement struct {
ID uint `gorm:"primaryKey"`
MessageID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
CreatedAt time.Time
}
```
---
## M4: 联系人管理表
### contacts
```go
type Contact struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255"`
Email string `gorm:"size:255"`
PhoneNumber string `gorm:"size:50"`
AvatarURL string `gorm:"size:512"`
SourceID string `gorm:"size:255"`
Source string `gorm:"size:50;default:'chatwoot'"` // chatwoot/FB/WA/TG等
AdditionalAttributes json.RawMessage `gorm:"type:jsonb"`
CustomAttributes json.RawMessage `gorm:"type:jsonb"`
LastActivityAt *time.Time `gorm:"index"`
CompanyID *uint `gorm:"index"` // 🔒
LabelList []string `gorm:"type:text[];serializer:json"`
Blocked bool `gorm:"default:false"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
ContactInboxes []ContactInbox `gorm:"foreignKey:ContactID"`
Conversations []Conversation `gorm:"foreignKey:ContactID"`
Company *Company `gorm:"foreignKey:CompanyID"` // 🔒
}
```
### contact_inboxes
```go
type ContactInbox struct {
ID uint `gorm:"primaryKey"`
ContactID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
SourceID string `gorm:"size:255;index"` // 渠道侧联系人ID
HmacVerified bool `gorm:"default:false"`
CreatedAt time.Time
UpdatedAt time.Time
Contact Contact `gorm:"foreignKey:ContactID"`
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### custom_attribute_definitions
```go
type CustomAttributeDefinition struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
AttributeModel string `gorm:"size:50;not null"` // conversation/contact
AttributeKey string `gorm:"size:255;not null"`
AttributeName string `gorm:"size:255;not null"`
AttributeType string `gorm:"size:50"` // text/number/date/list/checkbox
AttributeValues json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### custom_filters
```go
type CustomFilter struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
FilterType string `gorm:"size:50"` // conversation/contact/inbox/message
Name string `gorm:"size:255;not null"`
Query json.RawMessage `gorm:"type:jsonb;not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### labels (tag系统)
```go
// GoChat合并 tags + taggings 为 Label + 关联
type Label struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Title string `gorm:"size:255;not null"`
Color string `gorm:"size:50"`
ShowOnSidebar bool `gorm:"default:false"`
CreatedAt time.Time
UpdatedAt time.Time
Conversations []Conversation `gorm:"many2many:conversation_labels"`
Contacts []Contact `gorm:"many2many:contact_labels"`
}
```
**对比**: Chatwoot用tags+taggings两表(polymorphic)→ GoChat用Label+many2many关联表(conversation_labels/contact_labels
### notes
```go
type Note struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
ContactID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
Content string `gorm:"type:text;not null"`
CreatedAt time.Time
UpdatedAt time.Time
Contact Contact `gorm:"foreignKey:ContactID"`
User User `gorm:"foreignKey:UserID"`
}
```
---
## M5: 团队与分配表
### teams
```go
type Team struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
AllowAutoAssign bool `gorm:"default:false"`
AutoAssignmentMethod string `gorm:"size:50"` // round_robin/least_busy 🔒
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Members []TeamMember `gorm:"foreignKey:TeamID"`
Conversations []Conversation `gorm:"foreignKey:TeamID"`
}
```
### team_members
```go
type TeamMember struct {
ID uint `gorm:"primaryKey"`
TeamID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
CreatedAt time.Time
UpdatedAt time.Time
Team Team `gorm:"foreignKey:TeamID"`
User User `gorm:"foreignKey:UserID"`
}
```
### assignment_policies 🔒
```go
type AssignmentPolicy struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
TeamID uint `gorm:"not null;index"`
PolicyType string `gorm:"size:50;not null"` // round_robin/least_busy
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## M6: 自动化与模板表
### automation_rules
```go
type AutomationRule struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
EventType string `gorm:"size:50;index"` // message_created/conversation_created/conversation_updated
Conditions json.RawMessage `gorm:"type:jsonb;not null"`
Actions json.RawMessage `gorm:"type:jsonb;not null"`
Active bool `gorm:"default:true;index"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
}
```
### macros
```go
type Macro struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Actions json.RawMessage `gorm:"type:jsonb;not null"`
Visibility string `gorm:"size:50;default:'personal'"` // personal/team/global 🔒
CreatedByID uint `gorm:"index"`
UpdatedByID uint `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### canned_responses
```go
type CannedResponse struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
ShortCode string `gorm:"size:255;not null"`
Content string `gorm:"type:text;not null"`
CategoryID *uint `gorm:"index"` // 🔒
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## M7: 报告与CSAT表
### csat_survey_responses
```go
type CsatSurveyResponse struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
MessageID uint `gorm:"not null;index"`
ContactID uint `gorm:"not null;index"`
AssigneeID *uint `gorm:"index"`
Rating int `gorm:"not null"` // 1-5
FeedbackMessage string `gorm:"type:text"`
CreatedAt time.Time
Conversation Conversation `gorm:"foreignKey:ConversationID"`
Message Message `gorm:"foreignKey:MessageID"`
}
```
### campaigns
```go
type Campaign struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
Title string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
CampaignType string `gorm:"size:50;not null"` // ongoing/one_off
TriggerRules json.RawMessage `gorm:"type:jsonb"`
SenderID *uint `gorm:"index"`
Message string `gorm:"type:text"`
TemplateID *uint `gorm:"index"` // WA模板
ScheduledAt *time.Time `gorm:"index"`
CompletedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Inbox Inbox `gorm:"foreignKey:InboxID"`
}
```
### reporting_events
```go
type ReportingEvent struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID *uint `gorm:"index"`
UserID *uint `gorm:"index"`
ConversationID *uint `gorm:"index"`
EventType string `gorm:"size:50;not null;index"`
Value float64
CreatedAt time.Time `gorm:"index"`
Account Account `gorm:"foreignKey:AccountID"`
}
```
### reporting_events_rollups
```go
type ReportingEventsRollup struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID *uint `gorm:"index"`
UserID *uint `gorm:"index"`
RollupType string `gorm:"size:50;not null;index"` // daily/weekly/monthly
Value float64
MetricName string `gorm:"size:100;not null;index"`
StartDate time.Time `gorm:"not null;index"`
EndDate time.Time `gorm:"not null;index"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## M8: 通知与Webhook表
### notifications
```go
type Notification struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
PrimaryActorType string `gorm:"size:50"`
PrimaryActorID uint `gorm:"index"`
SecondaryActorType string `gorm:"size:50"`
SecondaryActorID *uint `gorm:"index"`
PushEnabled bool `gorm:"default:true"`
EmailEnabled bool `gorm:"default:true"`
ReadAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
User User `gorm:"foreignKey:UserID"`
}
```
### notification_settings
```go
type NotificationSetting struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
EventType string `gorm:"size:100;not null;index"`
PushEnabled bool `gorm:"default:false"`
EmailEnabled bool `gorm:"default:false"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### notification_subscriptions
```go
type NotificationSubscription struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"not null;index"`
SubscriptionType string `gorm:"size:50"` // push/email/webpush
SubscriptionToken string `gorm:"size:512;not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### integrations_hooks (Webhook)
```go
type IntegrationsHook struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
InboxID *uint `gorm:"index"`
HookType string `gorm:"size:50;not null"` // inbox_outgoing/account_outgoing
URL string `gorm:"size:512;not null"`
Settings json.RawMessage `gorm:"type:jsonb"`
Secret string `gorm:"size:255"`
Status string `gorm:"size:50;default:'active'"` // active/disabled
ProcessID *uint `gorm:"index"` // kbase process
AppID string `gorm:"size:255"`
ReferenceID string `gorm:"size:255"`
AccessToken string `gorm:"size:255"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### webhooks
```go
type Webhook struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
URL string `gorm:"size:512;not null"`
Events json.RawMessage `gorm:"type:jsonb"` // 订阅的事件列表
Secret string `gorm:"size:255"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### platform_apps
```go
type PlatformApp struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Platform string `gorm:"size:50"` // facebook/slack/etc
Config json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### platform_app_permissibles
```go
type PlatformAppPermissible struct {
ID uint `gorm:"primaryKey"`
PlatformAppID uint `gorm:"not null;index"`
PermissibleType string `gorm:"size:50"` // Account/User/Inbox
PermissibleID uint `gorm:"index"`
CreatedAt time.Time
}
```
### platform_banners
```go
type PlatformBanner struct {
ID uint `gorm:"primaryKey"`
Message string `gorm:"type:text;not null"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## M9: 知识库与帮助中心表
### portals
```go
type Portal struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Slug string `gorm:"size:255;uniqueIndex;not null"`
Description string `gorm:"type:text"`
CustomDomain string `gorm:"size:255"`
LogoURL string `gorm:"size:512"`
HeroIconURL string `gorm:"size:512"`
HomepageLayout string `gorm:"size:100"`
Color string `gorm:"size:50"`
Locale string `gorm:"size:10;default:'en'"`
Archived bool `gorm:"default:false"`
AllowedLocales []string `gorm:"type:text[];serializer:json"`
Config json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Categories []Category `gorm:"foreignKey:PortalID"`
Articles []Article `gorm:"foreignKey:PortalID"`
}
```
### categories
```go
type Category struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
PortalID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Slug string `gorm:"size:255;uniqueIndex"`
Description string `gorm:"type:text"`
Position int `gorm:"default:0"`
Locale string `gorm:"size:10;default:'en'"`
ParentID *uint `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
Portal Portal `gorm:"foreignKey:PortalID"`
Parent *Category `gorm:"foreignKey:ParentID"`
Articles []Article `gorm:"foreignKey:CategoryID"`
RelatedCategories []RelatedCategory `gorm:"foreignKey:CategoryID"`
}
```
### articles
```go
type Article struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
PortalID uint `gorm:"not null;index"`
CategoryID *uint `gorm:"index"`
FolderID *uint `gorm:"index"`
Title string `gorm:"size:255;not null"`
Slug string `gorm:"size:255;uniqueIndex"`
Content string `gorm:"type:text"`
Description string `gorm:"type:text"`
Status string `gorm:"size:50;default:'draft'"` // draft/published/archived
AuthorID *uint `gorm:"index"`
Views int `gorm:"default:0"`
Locale string `gorm:"size:10;default:'en'"`
AssociatedArticleID *uint `gorm:"index"` // 关联翻译文章
CreatedAt time.Time
UpdatedAt time.Time
Portal Portal `gorm:"foreignKey:PortalID"`
Category *Category `gorm:"foreignKey:CategoryID"`
Folder *Folder `gorm:"foreignKey:FolderID"`
Embedding *ArticleEmbedding `gorm:"foreignKey:ArticleID"`
}
```
### article_embeddings 🔒
```go
type ArticleEmbedding struct {
ID uint `gorm:"primaryKey"`
ArticleID uint `gorm:"uniqueIndex;not null"`
Embedding []float64 `gorm:"type:vector(1536)"` // pg_vector
CreatedAt time.Time
UpdatedAt time.Time
Article Article `gorm:"foreignKey:ArticleID"`
}
```
### related_categories / folders / portals_members
```go
type RelatedCategory struct {
ID uint `gorm:"primaryKey"`
CategoryID uint `gorm:"not null;index"`
RelatedCategoryID uint `gorm:"not null;index"`
CreatedAt time.Time
}
type Folder struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
PortalID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Slug string `gorm:"size:255"`
CreatedAt time.Time; UpdatedAt time.Time
}
type PortalMember struct {
ID uint `gorm:"primaryKey"`
PortalID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
CreatedAt time.Time
}
```
---
## M10: Captain AI 与 Copilot 表 🔒
### captain_assistants
```go
type CaptainAssistant struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
SystemPrompt string `gorm:"type:text"`
ModelConfig json.RawMessage `gorm:"type:jsonb"` // LLM配置(model/temperature等)
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
Account Account `gorm:"foreignKey:AccountID"`
Documents []CaptainDocument `gorm:"foreignKey:AssistantID"`
Scenarios []CaptainScenario `gorm:"foreignKey:AssistantID"`
CaptainInboxes []CaptainInbox `gorm:"foreignKey:AssistantID"`
}
```
### captain_documents
```go
type CaptainDocument struct {
ID uint `gorm:"primaryKey"`
AssistantID uint `gorm:"not null;index"`
AccountID uint `gorm:"not null;index"`
ExternalLink string `gorm:"size:512"`
Content string `gorm:"type:text"`
Status string `gorm:"size:50;default:'in_progress'"` // in_progress/completed/failed
Embedding []float64 `gorm:"type:vector(1536)"` // pg_vector
FAQ json.RawMessage `gorm:"type:jsonb"` // 自动生成的FAQ
LastSyncedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
Assistant CaptainAssistant `gorm:"foreignKey:AssistantID"`
}
```
### captain_scenarios
```go
type CaptainScenario struct {
ID uint `gorm:"primaryKey"`
AssistantID uint `gorm:"not null;index"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
TriggerRules json.RawMessage `gorm:"type:jsonb"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### captain_inboxes / captain_assistant_responses / captain_custom_tools
```go
type CaptainInbox struct {
ID uint `gorm:"primaryKey"`
AssistantID uint `gorm:"not null;index"`
InboxID uint `gorm:"not null;index"`
CreatedAt time.Time
}
type CaptainAssistantResponse struct {
ID uint `gorm:"primaryKey"`
AssistantID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
Content string `gorm:"type:text"`
CreatedAt time.Time
}
type CaptainCustomTool struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
Endpoint string `gorm:"size:512"`
Method string `gorm:"size:10"` // GET/POST
Headers json.RawMessage `gorm:"type:jsonb"`
BodyTemplate string `gorm:"type:text"`
CreatedAt time.Time; UpdatedAt time.Time
}
```
### copilot_messages / copilot_threads
```go
type CopilotThread struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
UserID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
Messages []CopilotMessage `gorm:"foreignKey:ThreadID"`
}
type CopilotMessage struct {
ID uint `gorm:"primaryKey"`
ThreadID uint `gorm:"not null;index"`
AccountID uint `gorm:"not null;index"`
Role string `gorm:"size:50;not null"` // user/assistant/tool
Content string `gorm:"type:text"`
MessageType string `gorm:"size:50"` // reply_suggestion/summarize/rewrite
CreatedAt time.Time
Thread CopilotThread `gorm:"foreignKey:ThreadID"`
}
```
---
## M11: 企业版功能表 🔒
### sla_policies / applied_slas / sla_events
```go
type SlaPolicy struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
FRT int `gorm:"not null"` // First Response Time (minutes)
NRT int `gorm:"not null"` // Next Response Time (minutes)
RT int `gorm:"not null"` // Resolution Time (minutes)
OnlyBusinessHours bool `gorm:"default:false"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
type AppliedSla struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
SlaPolicyID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
FRTAt *time.Time
NRTAt *time.Time
RTAt *time.Time
SLAStatus string `gorm:"size:50;default:'active'"` // active/violated/completed
CreatedAt time.Time
UpdatedAt time.Time
SlaPolicy SlaPolicy `gorm:"foreignKey:SlaPolicyID"`
Conversation Conversation `gorm:"foreignKey:ConversationID"`
}
type SlaEvent struct {
ID uint `gorm:"primaryKey"`
AppliedSlaID uint `gorm:"not null;index"`
EventType string `gorm:"size:50;not null"` // frt_violated/nrt_violated/rt_violated/applied/replied/resolved
CreatedAt time.Time
AppliedSla AppliedSla `gorm:"foreignKey:AppliedSlaID"`
}
```
### audits 🔒
```go
type Audit struct {
ID uint `gorm:"primaryKey"`
AuditableID uint `gorm:"index"`
AuditableType string `gorm:"size:100;index"`
AssociatedID *uint
AssociatedType string `gorm:"size:100"`
UserID *uint `gorm:"index"`
UserType string `gorm:"size:100"`
Action string `gorm:"size:50;not null;index"` // create/update/delete
AuditedChanges json.RawMessage `gorm:"type:jsonb"`
Comment string `gorm:"type:text"`
RemoteIP string `gorm:"size:50"`
RequestUUID string `gorm:"size:255"`
CreatedAt time.Time
}
```
**对比**: Chatwoot用Audited gem → GoChat自实现审计log,存储变更jsonb
### calls 🔒
```go
type Call struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
ConversationID uint `gorm:"not null;index"`
CallerID *uint `gorm:"index"`
CallType string `gorm:"size:50"` // inbound/outbound
Status string `gorm:"size:50;default:'ringing'"` // ringing/ongoing/completed/failed
Duration int // seconds
CallSID string `gorm:"size:255"` // Twilio Call SID
RecordingURL string `gorm:"size:512"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
### email_templates 🔒
```go
type EmailTemplate struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Name string `gorm:"size:255;not null"`
Subject string `gorm:"size:255"`
Body string `gorm:"type:text"`
Locale string `gorm:"size:10;default:'en'"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## M12: 平台与集成表
### access_tokens
```go
type AccessToken struct {
ID uint `gorm:"primaryKey"`
Token string `gorm:"size:255;uniqueIndex;not null"`
OwnerID uint `gorm:"index"`
OwnerType string `gorm:"size:100;index"` // User/PlatformApp/AgentBot
CreatedAt time.Time
}
```
### data_imports
```go
type DataImport struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
DataType string `gorm:"size:50;not null"` // contacts/conversations
Status string `gorm:"size:50;default:'pending'"` // pending/processing/completed/failed
ProcessingErrors json.RawMessage `gorm:"type:jsonb"`
TotalRecords int
ImportedRecords int
CreatedAt time.Time
UpdatedAt time.Time
}
```
### installation_configs
```go
type InstallationConfig struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;uniqueIndex;not null"`
Value json.RawMessage `gorm:"type:jsonb;not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
**对比**: Chatwoot用此表存储全局配置(DISPLAY_MANIFEST等)→ GoChat同样用途
### dashboard_apps 🔒
```go
type DashboardApp struct {
ID uint `gorm:"primaryKey"`
AccountID uint `gorm:"not null;index"`
Title string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
URL string `gorm:"size:512;not null"`
IconURL string `gorm:"size:512"`
Position int `gorm:"default:0"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## 去除的Chatwoot表
| 原表 | 原因 | GoChat替代 |
|---|---|---|
| schema_migrations | Rails元数据 | GORM AutoMigrate |
| ar_internal_metadata | Rails元数据 | 不需要 |
| active_storage_blobs | ActiveStorage独立存储 | 文件直接存本地/S3Attachment表内嵌FileURL |
| active_storage_attachments | 同上 | Attachment表内嵌 |
| active_storage_variant_records | 同上 | 图片缩略图URL |
| action_mailbox_inbound_emails | Rails ActionMailbox | Email渠道直接处理 |
| mentions | 对话内@提及 | Message内容解析提取 |
| leaves | Call参与者离开 | Call模型内嵌 |
| channel_twitter_profiles | Twitter API已关 | 不实现 |
---
## 表统计
| 模块 | GoChat表数 | Chatwoot原表数 | 变化 |
|---|---|---|---|
| M1 核心 | 7 | 8+ | 合并Devise字段 |
| M2 渠道 | 17 | 17 | 保留 |
| M3 对话 | 5 | 7 | 合并active_storage |
| M4 联系人 | 6 | 8 | 合并tags+taggings |
| M5 团队 | 3 | 3 | 保留 |
| M6 自动化 | 3 | 3 | 保留 |
| M7 报告 | 4 | 4 | 保留 |
| M8 通知 | 8 | 8 | 保留 |
| M9 知识库 | 6 | 7 | 合并 |
| M10 AI | 8 | 8 | 保留 |
| M11 企业 | 6 | 6 | 保留 |
| M12 平台 | 4 | 4 | 保留 |
| **合计** | **~68** | **87** | **-19** |
---
> 🔒 = 企业版功能表
> **下一步**: P2C(路由与API设计)