完善角色权限与数据隔离

This commit is contained in:
yml2213
2026-07-26 14:00:50 +08:00
parent b483cbc26d
commit 16d9ab0e5e
38 changed files with 2847 additions and 292 deletions
+4
View File
@@ -48,5 +48,9 @@ func Migrate(db *gorm.DB) error {
&OperationLog{},
&Announcement{},
&TenantSetting{},
&Role{},
&Permission{},
&RolePermission{},
&RoleDataScope{},
)
}
+86 -46
View File
@@ -24,7 +24,7 @@ type Tenant struct {
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
Role string `gorm:"size:20;default:agent" json:"role"`
Role string `gorm:"size:30;default:agent" json:"role"`
Username string `gorm:"size:50;not null;uniqueIndex" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Nickname string `gorm:"size:50" json:"nickname"`
@@ -50,12 +50,12 @@ type Customer struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
Name string `gorm:"size:50;not null" json:"name"`
Phone string `gorm:"size:20" json:"phone"` // 主手机号(兼容列表搜索)
Tel string `gorm:"size:30" json:"tel"` // 联系电话/固话
Phone string `gorm:"size:20" json:"phone"` // 主手机号(兼容列表搜索)
Tel string `gorm:"size:30" json:"tel"` // 联系电话/固话
Email string `gorm:"size:100" json:"email"`
Wechat string `gorm:"size:50" json:"wechat"` // 微信号,默认可空
Wechat string `gorm:"size:50" json:"wechat"` // 微信号,默认可空
QQ string `gorm:"size:20;column:qq" json:"qq"`
Remark string `gorm:"type:text" json:"remark"` // 客户备注
Remark string `gorm:"type:text" json:"remark"` // 客户备注
Tags string `gorm:"type:text" json:"tags"`
Source string `gorm:"size:30" json:"source"`
Status string `gorm:"size:20;default:online" json:"status"`
@@ -95,15 +95,15 @@ type BlacklistEntry struct {
}
type Session struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
ChannelID uint `json:"channel_id"`
CustomerID uint `gorm:"index" json:"customer_id"`
AgentID *uint `gorm:"index" json:"agent_id"`
VisitorTokenHash string `gorm:"size:64;index" json:"-"`
VisitorIP string `gorm:"size:64" json:"visitor_ip"`
VisitorRegion string `gorm:"size:100" json:"visitor_region"`
UserAgent string `gorm:"size:500" json:"user_agent"`
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
ChannelID uint `json:"channel_id"`
CustomerID uint `gorm:"index" json:"customer_id"`
AgentID *uint `gorm:"index" json:"agent_id"`
VisitorTokenHash string `gorm:"size:64;index" json:"-"`
VisitorIP string `gorm:"size:64" json:"visitor_ip"`
VisitorRegion string `gorm:"size:100" json:"visitor_region"`
UserAgent string `gorm:"size:500" json:"user_agent"`
// DeviceKey 访客端持久设备指纹(localStorage),用于设备级拉黑
DeviceKey string `gorm:"size:64;index" json:"device_key"`
// 落地页 / 当前页(访客浏览轨迹)
@@ -115,11 +115,11 @@ type Session struct {
// LastSeenAt 访客最近活跃(心跳/换页),用于在线时长与在线状态
LastSeenAt *time.Time `json:"last_seen_at"`
// DraftText 访客输入框未发送草稿(实时监控用,非聊天消息)
DraftText string `gorm:"type:text" json:"draft_text"`
DraftUpdatedAt *time.Time `json:"draft_updated_at"`
LastReadSeq int `gorm:"default:0" json:"last_read_seq"`
Status string `gorm:"size:20;default:waiting" json:"status"`
Priority string `gorm:"size:20;default:normal" json:"priority"`
DraftText string `gorm:"type:text" json:"draft_text"`
DraftUpdatedAt *time.Time `json:"draft_updated_at"`
LastReadSeq int `gorm:"default:0" json:"last_read_seq"`
Status string `gorm:"size:20;default:waiting" json:"status"`
Priority string `gorm:"size:20;default:normal" json:"priority"`
SatisfactionScore *int `json:"satisfaction_score"`
SatisfactionText string `gorm:"size:500" json:"satisfaction_text"`
EndReason string `gorm:"size:50" json:"end_reason"`
@@ -133,7 +133,7 @@ type CustomerContact struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
CustomerID uint `gorm:"uniqueIndex:idx_cust_contact;not null" json:"customer_id"`
Kind string `gorm:"size:20;uniqueIndex:idx_cust_contact;not null" json:"kind"` // phone|wechat|email|qq
Kind string `gorm:"size:20;uniqueIndex:idx_cust_contact;not null" json:"kind"` // phone|wechat|email|qq
Value string `gorm:"size:100;uniqueIndex:idx_cust_contact;not null" json:"value"`
Source string `gorm:"size:30" json:"source"` // draft|message|leave|agent
CreatedAt time.Time `json:"created_at"`
@@ -165,21 +165,23 @@ type SessionEvent struct {
SessionID uint `gorm:"index;not null" json:"session_id"`
OperatorID uint `json:"operator_id"`
Action string `gorm:"size:50;not null" json:"action"`
Detail string `gorm:"size:500" json:"detail"`
Detail string `gorm:"type:text" json:"detail"`
CreatedAt time.Time `json:"created_at"`
}
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
ParentID *uint `json:"parent_id"`
Name string `gorm:"size:30;not null" json:"name"`
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
CreatedBy uint `gorm:"index" json:"created_by"`
ParentID *uint `json:"parent_id"`
Name string `gorm:"size:30;not null" json:"name"`
}
type KnowledgeEntry struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
CategoryID uint `json:"category_id"`
CreatedBy uint `gorm:"index" json:"created_by"`
Title string `gorm:"size:100;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Status string `gorm:"size:20;default:draft" json:"status"`
@@ -191,16 +193,16 @@ type KnowledgeEntry struct {
// QuickReply 团队/个人快捷回复(与知识库独立)。
// Scope=team 时 OwnerUserID 为空,全租户共享;Scope=personal 时归属 OwnerUserID。
type QuickReply struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
Scope string `gorm:"size:20;index;not null" json:"scope"` // team | personal
OwnerUserID *uint `gorm:"index" json:"owner_user_id,omitempty"`
Title string `gorm:"size:100;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Shortcut string `gorm:"size:32;index" json:"shortcut"` // 输入码,如 nh → /nh
GroupName string `gorm:"size:50" json:"group_name"`
Status string `gorm:"size:20;default:draft;index" json:"status"` // draft | published
SortOrder int `gorm:"default:0" json:"sort_order"`
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"index;not null" json:"tenant_id"`
Scope string `gorm:"size:20;index;not null" json:"scope"` // team | personal
OwnerUserID *uint `gorm:"index" json:"owner_user_id,omitempty"`
Title string `gorm:"size:100;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Shortcut string `gorm:"size:32;index" json:"shortcut"` // 输入码,如 nh → /nh
GroupName string `gorm:"size:50" json:"group_name"`
Status string `gorm:"size:20;default:draft;index" json:"status"` // draft | published
SortOrder int `gorm:"default:0" json:"sort_order"`
// UsageCount 全局累计(管理页展示);排序以个人用量为准见 QuickReplyUserUsage
UsageCount int `gorm:"default:0" json:"usage_count"`
CreatedAt time.Time `json:"created_at"`
@@ -254,26 +256,64 @@ type Announcement struct {
// TenantSetting 租户级系统设置(欢迎语、工作时间、通知开关、分配策略等)。
type TenantSetting struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"uniqueIndex;not null" json:"tenant_id"`
DisplayName string `gorm:"size:100" json:"display_name"`
AgentNickname string `gorm:"size:50" json:"agent_nickname"`
Timezone string `gorm:"size:50;default:Asia/Shanghai" json:"timezone"`
WelcomeMessage string `gorm:"size:500" json:"welcome_message"`
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"uniqueIndex;not null" json:"tenant_id"`
DisplayName string `gorm:"size:100" json:"display_name"`
AgentNickname string `gorm:"size:50" json:"agent_nickname"`
Timezone string `gorm:"size:50;default:Asia/Shanghai" json:"timezone"`
WelcomeMessage string `gorm:"size:500" json:"welcome_message"`
// WelcomeMessagesJSON 多段欢迎语 JSON[{type:text|image, content:...}, ...]
WelcomeMessagesJSON string `gorm:"type:text" json:"welcome_messages_json"`
OfflinePrompt string `gorm:"size:500" json:"offline_prompt"`
WorkHoursJSON string `gorm:"type:text" json:"work_hours_json"`
WorktimePrompt string `gorm:"size:500" json:"worktime_prompt"`
NotifyNewSession bool `gorm:"default:true" json:"notify_new_session"`
NotifyOfflineLeave bool `gorm:"default:true" json:"notify_offline_leave"`
NotifyDailyReport bool `gorm:"default:false" json:"notify_daily_report"`
WorktimePrompt string `gorm:"size:500" json:"worktime_prompt"`
NotifyNewSession bool `gorm:"default:true" json:"notify_new_session"`
NotifyOfflineLeave bool `gorm:"default:true" json:"notify_offline_leave"`
NotifyDailyReport bool `gorm:"default:false" json:"notify_daily_report"`
// AssignStrategy 自动分配策略:least_load(默认)| round_robin
AssignStrategy string `gorm:"size:30;default:least_load" json:"assign_strategy"`
// MaxActivePerAgent 每位坐席最大进行中会话数;0 表示不限制
MaxActivePerAgent int `gorm:"default:0" json:"max_active_per_agent"`
// RRLastAgentID 轮询策略的上次分配坐席游标
RRLastAgentID *uint `json:"-"`
RRLastAgentID *uint `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Role 租户级角色定义。
// Code 为角色标识(admin/supervisor/agent 为内置,其他为自定义)。
// Type="builtin" 的内置角色不可删除、不可修改 Code。
type Role struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"uniqueIndex:idx_tenant_role_code;not null" json:"tenant_id"`
Name string `gorm:"size:50;not null" json:"name"`
Code string `gorm:"size:30;uniqueIndex:idx_tenant_role_code;not null" json:"code"`
Type string `gorm:"size:10;default:custom" json:"type"` // builtin | custom
Desc string `gorm:"size:200" json:"desc"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Permission 系统权限码表(全租户共享,不可删除)。
type Permission struct {
ID uint `gorm:"primaryKey" json:"id"`
Code string `gorm:"size:50;uniqueIndex;not null" json:"code"`
Name string `gorm:"size:50;not null" json:"name"`
Module string `gorm:"size:30;not null" json:"module"`
Category string `gorm:"size:20;not null" json:"category"` // view | operate
SortOrder int `gorm:"default:0" json:"sort_order"`
}
// RolePermission 角色-权限多对多关联。
type RolePermission struct {
RoleID uint `gorm:"primaryKey" json:"role_id"`
PermissionID uint `gorm:"primaryKey" json:"permission_id"`
}
// RoleDataScope 保存角色在业务模块中的数据范围。
// 当前支持 all/selfmodule 维度为后续 group/channel/tag 等范围扩展预留。
type RoleDataScope struct {
RoleID uint `gorm:"primaryKey" json:"role_id"`
Module string `gorm:"size:30;primaryKey" json:"module"`
Scope string `gorm:"size:20;not null" json:"scope"`
}
+190
View File
@@ -0,0 +1,190 @@
package model
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
DataScopeAll = "all"
DataScopeSelf = "self"
)
var dataScopeModules = []string{"session", "customer", "chat_history", "statistics"}
var predefinedPermissions = []Permission{
{Code: "session.view", Name: "查看会话列表", Module: "工作台", Category: "view", SortOrder: 1},
{Code: "session.reply", Name: "回复访客消息", Module: "工作台", Category: "operate", SortOrder: 2},
{Code: "session.transfer", Name: "转接会话", Module: "工作台", Category: "operate", SortOrder: 3},
{Code: "session.end", Name: "结束会话", Module: "工作台", Category: "operate", SortOrder: 4},
{Code: "session.note", Name: "添加内部备注", Module: "工作台", Category: "operate", SortOrder: 5},
{Code: "session.priority", Name: "标记优先级", Module: "工作台", Category: "operate", SortOrder: 6},
{Code: "customer.view", Name: "查看客户列表", Module: "客户管理", Category: "view", SortOrder: 7},
{Code: "customer.create", Name: "新增客户", Module: "客户管理", Category: "operate", SortOrder: 8},
{Code: "customer.edit", Name: "编辑客户资料", Module: "客户管理", Category: "operate", SortOrder: 9},
{Code: "customer.export", Name: "导出客户", Module: "客户管理", Category: "operate", SortOrder: 10},
{Code: "customer.tag", Name: "管理客户标签", Module: "客户管理", Category: "operate", SortOrder: 11},
{Code: "chat_history.view", Name: "查看对话记录", Module: "对话记录", Category: "view", SortOrder: 12},
{Code: "chat_history.detail", Name: "查看会话详情", Module: "对话记录", Category: "view", SortOrder: 13},
{Code: "chat_history.export", Name: "导出对话记录", Module: "对话记录", Category: "operate", SortOrder: 14},
{Code: "chat_history.batch_archive", Name: "批量归档记录", Module: "对话记录", Category: "operate", SortOrder: 15},
{Code: "knowledge.view", Name: "查看知识库", Module: "知识库", Category: "view", SortOrder: 16},
{Code: "knowledge.create", Name: "新建知识条目", Module: "知识库", Category: "operate", SortOrder: 17},
{Code: "knowledge.edit", Name: "编辑知识条目", Module: "知识库", Category: "operate", SortOrder: 18},
{Code: "knowledge.publish", Name: "发布/下架条目", Module: "知识库", Category: "operate", SortOrder: 19},
{Code: "knowledge.delete", Name: "删除知识条目/分类", Module: "知识库", Category: "operate", SortOrder: 20},
{Code: "quick_reply.view", Name: "查看快捷回复", Module: "快捷回复", Category: "view", SortOrder: 21},
{Code: "quick_reply.team_create", Name: "新建团队快捷回复", Module: "快捷回复", Category: "operate", SortOrder: 22},
{Code: "quick_reply.team_edit", Name: "编辑/删除团队回复", Module: "快捷回复", Category: "operate", SortOrder: 23},
{Code: "quick_reply.personal_manage", Name: "管理我的快捷回复", Module: "快捷回复", Category: "operate", SortOrder: 24},
{Code: "blacklist.view", Name: "查看黑名单", Module: "黑名单", Category: "view", SortOrder: 25},
{Code: "blacklist.create", Name: "拉黑访客/IP/设备", Module: "黑名单", Category: "operate", SortOrder: 26},
{Code: "blacklist.delete", Name: "解除黑名单", Module: "黑名单", Category: "operate", SortOrder: 27},
{Code: "statistics.view", Name: "查看数据统计", Module: "数据统计", Category: "view", SortOrder: 28},
{Code: "statistics.export", Name: "导出报告", Module: "数据统计", Category: "operate", SortOrder: 29},
{Code: "statistics.performance", Name: "查看客服绩效排行", Module: "数据统计", Category: "view", SortOrder: 30},
{Code: "settings.basic", Name: "基本设置", Module: "系统设置", Category: "operate", SortOrder: 31},
{Code: "settings.channel", Name: "渠道管理", Module: "系统设置", Category: "operate", SortOrder: 32},
{Code: "settings.staff", Name: "坐席账号", Module: "系统设置", Category: "operate", SortOrder: 33},
{Code: "settings.assign_rule", Name: "客服分配规则", Module: "系统设置", Category: "operate", SortOrder: 34},
{Code: "settings.customer_tag", Name: "客户标签", Module: "系统设置", Category: "operate", SortOrder: 35},
{Code: "settings.auto_reply", Name: "自动回复", Module: "系统设置", Category: "operate", SortOrder: 36},
{Code: "settings.worktime", Name: "工作时间", Module: "系统设置", Category: "operate", SortOrder: 37},
{Code: "settings.notification", Name: "通知设置", Module: "系统设置", Category: "operate", SortOrder: 38},
{Code: "permission.view", Name: "查看角色列表", Module: "权限控制", Category: "view", SortOrder: 39},
{Code: "permission.create_role", Name: "创建/编辑角色", Module: "权限控制", Category: "operate", SortOrder: 40},
{Code: "permission.delete_role", Name: "删除自定义角色", Module: "权限控制", Category: "operate", SortOrder: 41},
{Code: "permission.assign_role", Name: "给账号分配角色", Module: "权限控制", Category: "operate", SortOrder: 42},
}
// PredefinedPermissions 返回系统所有权限码列表。
func PredefinedPermissions() []Permission {
result := make([]Permission, len(predefinedPermissions))
copy(result, predefinedPermissions)
return result
}
// DataScopeModules 返回支持数据范围配置的业务模块。
func DataScopeModules() []string {
result := make([]string, len(dataScopeModules))
copy(result, dataScopeModules)
return result
}
// DefaultRoleDataScopes 返回角色的默认数据范围。
func DefaultRoleDataScopes(roleCode string) map[string]string {
scope := DataScopeSelf
if roleCode == "admin" || roleCode == "supervisor" {
scope = DataScopeAll
}
result := make(map[string]string, len(dataScopeModules))
for _, module := range dataScopeModules {
result[module] = scope
}
return result
}
// BuiltinRolePermissionCodes 返回内置角色对应的权限码集合。
func BuiltinRolePermissionCodes() map[string][]string {
return map[string][]string{
"admin": {
"session.view", "session.reply", "session.transfer", "session.end", "session.note", "session.priority",
"customer.view", "customer.create", "customer.edit", "customer.export", "customer.tag",
"chat_history.view", "chat_history.detail", "chat_history.export", "chat_history.batch_archive",
"knowledge.view", "knowledge.create", "knowledge.edit", "knowledge.publish", "knowledge.delete",
"quick_reply.view", "quick_reply.team_create", "quick_reply.team_edit", "quick_reply.personal_manage",
"blacklist.view", "blacklist.create", "blacklist.delete",
"statistics.view", "statistics.export", "statistics.performance",
"settings.basic", "settings.channel", "settings.staff", "settings.assign_rule",
"settings.customer_tag", "settings.auto_reply", "settings.worktime", "settings.notification",
"permission.view", "permission.create_role", "permission.delete_role", "permission.assign_role",
},
"supervisor": {
"session.view", "session.reply", "session.transfer", "session.end", "session.note", "session.priority",
"customer.view", "customer.create", "customer.edit", "customer.export", "customer.tag",
"chat_history.view", "chat_history.detail", "chat_history.export", "chat_history.batch_archive",
"knowledge.view", "knowledge.create", "knowledge.edit", "knowledge.publish", "knowledge.delete",
"quick_reply.view", "quick_reply.team_create", "quick_reply.team_edit", "quick_reply.personal_manage",
"blacklist.view", "blacklist.create", "blacklist.delete",
"statistics.view", "statistics.export", "statistics.performance",
"settings.customer_tag", "settings.auto_reply",
},
"agent": {
"session.view", "session.reply", "session.end", "session.note", "session.priority",
"customer.view", "customer.create", "customer.edit", "customer.tag",
"chat_history.view", "chat_history.detail",
"knowledge.view",
"quick_reply.view", "quick_reply.personal_manage",
"statistics.view", "statistics.performance",
},
}
}
// EnsurePermissions 补齐并更新系统权限码,支持后续平滑新增权限项。
func EnsurePermissions() error {
for _, permission := range predefinedPermissions {
if err := DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "code"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "module", "category", "sort_order"}),
}).Create(&permission).Error; err != nil {
return err
}
}
return nil
}
// EnsureBuiltinRoles 确保指定租户拥有三个内置角色。
func EnsureBuiltinRoles(tenantID uint) error {
builtins := []struct{ name, code, desc string }{
{"管理员", "admin", "租户最高权限,可管理账号、角色与全部系统设置"},
{"客服主管", "supervisor", "管理一线团队,可查看全部数据并维护知识库、快捷回复、黑名单等"},
{"客服", "agent", "一线接待人员,仅可查看和操作自己相关的会话、客户与个人数据"},
}
permMap := BuiltinRolePermissionCodes()
return DB.Transaction(func(tx *gorm.DB) error {
for _, b := range builtins {
var role Role
created := false
err := tx.Where("tenant_id = ? AND code = ?", tenantID, b.code).First(&role).Error
if err != nil {
if err != gorm.ErrRecordNotFound {
return err
}
role = Role{TenantID: tenantID, Name: b.name, Code: b.code, Type: "builtin", Desc: b.desc}
if err := tx.Create(&role).Error; err != nil {
return err
}
created = true
}
var permissionCount int64
if err := tx.Model(&RolePermission{}).Where("role_id = ?", role.ID).Count(&permissionCount).Error; err != nil {
return err
}
// 新建角色或迁移期空关联才写默认权限,避免覆盖管理员的自定义配置。
if created || permissionCount == 0 {
var permissionIDs []uint
if err := tx.Model(&Permission{}).Where("code IN ?", permMap[b.code]).Pluck("id", &permissionIDs).Error; err != nil {
return err
}
for _, permissionID := range permissionIDs {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&RolePermission{
RoleID: role.ID, PermissionID: permissionID,
}).Error; err != nil {
return err
}
}
}
for module, scope := range DefaultRoleDataScopes(b.code) {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&RoleDataScope{
RoleID: role.ID, Module: module, Scope: scope,
}).Error; err != nil {
return err
}
}
}
return nil
})
}