清理: - 删除 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 相关修改
1475 lines
48 KiB
Markdown
1475 lines
48 KiB
Markdown
# 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独立存储 | 文件直接存本地/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设计) |