48 KiB
P2B — GoChat 数据库设计文档
版本: v1.0 | 作者: CTO | 日期: 2026-05-22 参照: Chatwoot db/schema.rb (87表) + M1-M12需求文档 技术选型: GORM + PostgreSQL
设计原则
- 精简合并 — 去除Rails元数据表(schema_migrations, ar_internal_metadata, active_storage系列, action_mailbox系列),Chatwoot87表 → GoChat约70表
- Go惯例 — snake_case表名,模型CamelCase,枚举用string+常量而非Rails integer flags
- GORM特性 — 利用gorm:"type:jsonb"替代Rails serialized属性,gorm:"index"替代add_index
- 时间统一 — time.Time替代Rails datetime/timestamp
- 外键声明式 — GORM关联替代Rails references/belongs_to
- 企业版标注 — 🔒标记企业版表/字段
M1: 核心基础表
users
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
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
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 🔒
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 🔒
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 🔒
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 🔒
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
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
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
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
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
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
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
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
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
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
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
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
// 结构类似,各有渠道特有字段
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 🔒
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
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
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
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
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
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
// 简化:仅保留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
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
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
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
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系统)
// 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
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
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
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 🔒
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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 🔒
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
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
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
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
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
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
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
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 🔒
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 🔒
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 🔒
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
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
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
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 🔒
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独立存储 | 文件直接存本地/S3,Attachment表内嵌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设计)