From 462a8870ad9f28f5726d3ee846fa5267b3ccf96c Mon Sep 17 00:00:00 2001 From: LinXin Date: Tue, 23 Sep 2025 16:05:48 +0800 Subject: [PATCH 1/2] feat: add basic database structure --- backend/.gitignore | 3 +- backend/README_dev.md | 4 ++- backend/database/database.go | 51 ++++++++++++++++++++++++++ backend/database/migrate.go | 44 +++++++++++++++++++++++ backend/database/seed.go | 45 +++++++++++++++++++++++ backend/docs/user_permission.md | 11 ++++++ backend/go.mod | 15 +++++++- backend/go.sum | 17 +++++++++ backend/main.go | 19 +++++++++- backend/models/attachment.go | 35 ++++++++++++++++++ backend/models/board.go | 35 ++++++++++++++++++ backend/models/column.go | 33 +++++++++++++++++ backend/models/comment.go | 35 ++++++++++++++++++ backend/models/label.go | 33 +++++++++++++++++ backend/models/project.go | 60 +++++++++++++++++++++++++++++++ backend/models/task.go | 58 ++++++++++++++++++++++++++++++ backend/models/user.go | 40 +++++++++++++++++++++ backend/models/user_permission.go | 48 +++++++++++++++++++++++++ 18 files changed, 582 insertions(+), 4 deletions(-) create mode 100644 backend/database/database.go create mode 100644 backend/database/migrate.go create mode 100644 backend/database/seed.go create mode 100644 backend/docs/user_permission.md create mode 100644 backend/models/attachment.go create mode 100644 backend/models/board.go create mode 100644 backend/models/column.go create mode 100644 backend/models/comment.go create mode 100644 backend/models/label.go create mode 100644 backend/models/project.go create mode 100644 backend/models/task.go create mode 100644 backend/models/user.go create mode 100644 backend/models/user_permission.go diff --git a/backend/.gitignore b/backend/.gitignore index 2eea525..17a52bf 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +progress_wall.db \ No newline at end of file diff --git a/backend/README_dev.md b/backend/README_dev.md index 2d5c44b..94c0db6 100644 --- a/backend/README_dev.md +++ b/backend/README_dev.md @@ -16,7 +16,9 @@ backend/ ├── controllers/ # 控制器层 │ └── ... ├── database/ # 数据库连接 -│ └── database.go +| |- seed.go # 初始权限组设定 +| |- migrate.go # 数据库迁移(自动建表) +│ └── database.go # 数据库连接 ├── middleware/ # 中间件 │ └── ... ├── models/ # 数据模型 diff --git a/backend/database/database.go b/backend/database/database.go new file mode 100644 index 0000000..4364294 --- /dev/null +++ b/backend/database/database.go @@ -0,0 +1,51 @@ +package database + +import ( + "fmt" + "log" + "progress-wall-backend/config" + + "gorm.io/driver/mysql" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// InitDB 初始化数据库连接 +func InitDB(cfg *config.Config) (*gorm.DB, error) { + var db *gorm.DB + var err error + + // 配置GORM日志 + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + } + + switch cfg.DB.Type { + case "mysql": + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.Name) + db, err = gorm.Open(mysql.Open(dsn), gormConfig) + case "sqlite": + db, err = gorm.Open(sqlite.Open(cfg.DB.Name+".db"), gormConfig) + default: + return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.DB.Type) + } + + if err != nil { + return nil, err + } + + // 配置连接池 + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // 设置连接池参数 + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + + log.Printf("数据库连接成功: %s", cfg.DB.Type) + return db, nil +} diff --git a/backend/database/migrate.go b/backend/database/migrate.go new file mode 100644 index 0000000..7bff6b8 --- /dev/null +++ b/backend/database/migrate.go @@ -0,0 +1,44 @@ +package database + +import ( + "log" + "progress-wall-backend/models" + + "gorm.io/gorm" +) + +// Migrate 执行数据库迁移 +func Migrate(db *gorm.DB) error { + log.Println("开始执行数据库迁移...") + + // 迁移所有模型 + err := db.AutoMigrate( + // 用户和权限相关 + &models.User{}, + &models.UserPermission{}, + &models.UserPermissionGroup{}, + &models.UserPermissionGroupUser{}, + &models.UserPermissionGroup{}, + + // 项目和看板相关 + &models.Project{}, + &models.ProjectMember{}, + &models.Board{}, + + // 任务相关 + &models.Column{}, + &models.Task{}, + &models.Comment{}, + &models.Attachment{}, + &models.Label{}, + &models.TaskLabel{}, + ) + + if err != nil { + log.Printf("数据库迁移失败: %v", err) + return err + } + + log.Println("数据库迁移完成") + return nil +} diff --git a/backend/database/seed.go b/backend/database/seed.go new file mode 100644 index 0000000..bc125a3 --- /dev/null +++ b/backend/database/seed.go @@ -0,0 +1,45 @@ +package database + +import ( + "log" + "progress-wall-backend/models" + + "gorm.io/gorm" +) + +// Seed 初始化基础数据 +func Seed(db *gorm.DB) error { + log.Println("开始初始化基础数据...") + + // 创建默认用户 + if err := createDefaultUsers(db); err != nil { + return err + } + + // 创建默认权限 + if err := createDefaultPermissions(db); err != nil { + return err + } + + log.Println("基础数据初始化完成") + return nil +} + +func createDefaultUsers(db *gorm.DB) error { + // TODO: 创建默认用户,目前密码加密方法还没确定 + return nil +} + +func createDefaultPermissions(db *gorm.DB) error { + adminPermissionGroup := models.UserPermissionGroup{ + GroupName: "admin", + PermissionStrings: "*", + } + db.Create(&adminPermissionGroup) + guestPermissionGroup := models.UserPermissionGroup{ + GroupName: "guest", + PermissionStrings: "users.list;users.detail", + } + db.Create(&guestPermissionGroup) + return nil +} diff --git a/backend/docs/user_permission.md b/backend/docs/user_permission.md new file mode 100644 index 0000000..3d27446 --- /dev/null +++ b/backend/docs/user_permission.md @@ -0,0 +1,11 @@ +# 用户权限设计 + +## 权限树 + +所有的权限都基于根权限'*',当检查权限时,扫描器会检查所需权限是否在的用户拥有的权限的子集中,举几个例子: + +全局管理员:'*' +用户管理员:'users.*' +Project A (project id=1) 项目管理员:project.1.* +Project B (project id=2) 项目管理员:project.2.* +Project A (project id=1) 项目成员:project.1.view; project.1.write; ... diff --git a/backend/go.mod b/backend/go.mod index 04afd44..ee2715d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,4 +2,17 @@ module progress-wall-backend go 1.21 -require github.com/joho/godotenv v1.4.0 +require ( + github.com/joho/godotenv v1.4.0 + golang.org/x/crypto v0.15.0 + gorm.io/driver/mysql v1.5.2 + gorm.io/driver/sqlite v1.5.4 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect +) diff --git a/backend/go.sum b/backend/go.sum index 8c9f290..fba38f4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,2 +1,19 @@ +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/backend/main.go b/backend/main.go index 5212720..59061af 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "progress-wall-backend/config" + "progress-wall-backend/database" ) func main() { @@ -19,5 +20,21 @@ func main() { fmt.Printf("- JWT密钥长度: %d\n", len(cfg.JWT.Secret)) fmt.Printf("- CORS允许来源: %s\n", cfg.CORS.AllowOrigins) - log.Println("配置加载完成") + // 初始化数据库 + db, err := database.InitDB(cfg) + if err != nil { + log.Fatal("数据库初始化失败:", err) + } + + // 执行数据库迁移 + if err := database.Migrate(db); err != nil { + log.Fatal("数据库迁移失败:", err) + } + + // 初始化基础数据 + if err := database.Seed(db); err != nil { + log.Fatal("初始化基础数据失败:", err) + } + + log.Println("数据库初始化完成") } diff --git a/backend/models/attachment.go b/backend/models/attachment.go new file mode 100644 index 0000000..74f734b --- /dev/null +++ b/backend/models/attachment.go @@ -0,0 +1,35 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Attachment 附件表 +type Attachment struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Filename string `json:"filename" gorm:"size:255;not null"` + OriginalName string `json:"original_name" gorm:"size:255;not null"` + FilePath string `json:"file_path" gorm:"size:500;not null"` + FileSize int64 `json:"file_size" gorm:"not null;comment:'文件大小(字节)'"` + MimeType string `json:"mime_type" gorm:"size:100;not null"` + TaskID uint `json:"task_id" gorm:"not null;index"` + UploaderID uint `json:"uploader_id" gorm:"not null;index"` + Status AttachmentStatus `json:"status" gorm:"type:tinyint;default:1;comment:'附件状态:1=正常,2=删除'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"` + Uploader User `json:"uploader,omitempty" gorm:"foreignKey:UploaderID"` +} + +// AttachmentStatus 附件状态枚举 +type AttachmentStatus int + +const ( + AttachmentStatusNormal AttachmentStatus = 1 // 正常 + AttachmentStatusDeleted AttachmentStatus = 2 // 删除 +) diff --git a/backend/models/board.go b/backend/models/board.go new file mode 100644 index 0000000..280d240 --- /dev/null +++ b/backend/models/board.go @@ -0,0 +1,35 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Board 看板表 +type Board struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:100;not null"` + Description string `json:"description" gorm:"type:text"` + Color string `json:"color" gorm:"size:7;default:'#3498db';comment:'看板颜色,十六进制格式'"` + Status BoardStatus `json:"status" gorm:"type:tinyint;default:1;comment:'看板状态:1=活跃,2=归档'"` + ProjectID uint `json:"project_id" gorm:"not null;index"` + OwnerID uint `json:"owner_id" gorm:"not null;index"` + Position int `json:"position" gorm:"default:0;comment:'看板在项目中的排序位置'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"` + Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID"` + Columns []Column `json:"columns,omitempty" gorm:"foreignKey:BoardID"` +} + +// BoardStatus 看板状态枚举 +type BoardStatus int + +const ( + BoardStatusActive BoardStatus = 1 // 活跃 + BoardStatusArchived BoardStatus = 2 // 归档 +) diff --git a/backend/models/column.go b/backend/models/column.go new file mode 100644 index 0000000..81c3c5e --- /dev/null +++ b/backend/models/column.go @@ -0,0 +1,33 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Column 列表 +type Column struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:50;not null"` + Description string `json:"description" gorm:"size:255"` + Color string `json:"color" gorm:"size:7;default:'#95a5a6';comment:'列颜色,十六进制格式'"` + Position int `json:"position" gorm:"not null;comment:'列在看板中的排序位置'"` + BoardID uint `json:"board_id" gorm:"not null;index"` + Status ColumnStatus `json:"status" gorm:"type:tinyint;default:1;comment:'列状态:1=正常,2=禁用'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Board Board `json:"board,omitempty" gorm:"foreignKey:BoardID"` + Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:ColumnID"` +} + +// ColumnStatus 列状态枚举 +type ColumnStatus int + +const ( + ColumnStatusActive ColumnStatus = 1 // 正常 + ColumnStatusDisabled ColumnStatus = 2 // 禁用 +) diff --git a/backend/models/comment.go b/backend/models/comment.go new file mode 100644 index 0000000..8dc1c42 --- /dev/null +++ b/backend/models/comment.go @@ -0,0 +1,35 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Comment 评论表 +type Comment struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Content string `json:"content" gorm:"type:text;not null"` + TaskID uint `json:"task_id" gorm:"not null;index"` + UserID uint `json:"user_id" gorm:"not null;index"` + ParentID *uint `json:"parent_id" gorm:"index;comment:'父评论ID,用于回复评论'"` + Status CommentStatus `json:"status" gorm:"type:tinyint;default:1;comment:'评论状态:1=正常,2=隐藏,3=删除'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"` + User User `json:"user,omitempty" gorm:"foreignKey:UserID"` + Parent *Comment `json:"parent,omitempty" gorm:"foreignKey:ParentID"` + Replies []Comment `json:"replies,omitempty" gorm:"foreignKey:ParentID"` +} + +// CommentStatus 评论状态枚举 +type CommentStatus int + +const ( + CommentStatusNormal CommentStatus = 1 // 正常 + CommentStatusHidden CommentStatus = 2 // 隐藏 + CommentStatusDeleted CommentStatus = 3 // 删除 +) diff --git a/backend/models/label.go b/backend/models/label.go new file mode 100644 index 0000000..d637083 --- /dev/null +++ b/backend/models/label.go @@ -0,0 +1,33 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Label 标签表 +type Label struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:50;not null"` + Color string `json:"color" gorm:"size:7;default:'#3498db';comment:'标签颜色,十六进制格式'"` + ProjectID uint `json:"project_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"` + Tasks []Task `json:"tasks,omitempty" gorm:"many2many:task_labels"` +} + +// TaskLabel 任务标签关联表 +type TaskLabel struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + TaskID uint `json:"task_id" gorm:"not null;index"` + LabelID uint `json:"label_id" gorm:"not null;index"` + + // 关联关系 + Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"` + Label Label `json:"label,omitempty" gorm:"foreignKey:LabelID"` +} diff --git a/backend/models/project.go b/backend/models/project.go new file mode 100644 index 0000000..327da47 --- /dev/null +++ b/backend/models/project.go @@ -0,0 +1,60 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Project 项目表 +type Project struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"size:100;not null"` + Description string `json:"description" gorm:"type:text"` + Status ProjectStatus `json:"status" gorm:"type:tinyint;default:1;comment:'项目状态:1=进行中,2=已完成,3=已暂停,4=已取消'"` + StartDate *time.Time `json:"start_date"` + EndDate *time.Time `json:"end_date"` + OwnerID uint `json:"owner_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID" comment:"项目所有者,必须有项目根权限*.project.project_id.*"` + Boards []Board `json:"boards,omitempty" gorm:"foreignKey:ProjectID"` + Members []User `json:"members,omitempty" gorm:"many2many:project_members"` +} + +// ProjectMember 项目成员关联表 +type ProjectMember struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + ProjectID uint `json:"project_id" gorm:"not null;index"` + UserID uint `json:"user_id" gorm:"not null;index"` + PorjectPermission string `json:"project_permission" gorm:"type:text;comment:'项目权限,如*.project.project_id.*'"` + JoinedAt time.Time `json:"joined_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"` + User User `json:"user,omitempty" gorm:"foreignKey:UserID"` +} + +// ProjectStatus 项目状态枚举 +type ProjectStatus int + +const ( + ProjectStatusActive ProjectStatus = 1 // 进行中 + ProjectStatusCompleted ProjectStatus = 2 // 已完成 + ProjectStatusPaused ProjectStatus = 3 // 已暂停 + ProjectStatusCancelled ProjectStatus = 4 // 已取消 +) + +// VisibilityStatus 项目可见性枚举 +type ProjectVisibility int + +const ( + ProjectVisibilityPublic ProjectVisibility = 1 // 公开 + ProjectVisibilityPrivate ProjectVisibility = 2 // 私有 +) diff --git a/backend/models/task.go b/backend/models/task.go new file mode 100644 index 0000000..7e13571 --- /dev/null +++ b/backend/models/task.go @@ -0,0 +1,58 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Task 任务表 +type Task struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Title string `json:"title" gorm:"size:200;not null"` + Description string `json:"description" gorm:"type:text"` + Priority TaskPriority `json:"priority" gorm:"type:tinyint;default:2;comment:'任务优先级:1=低,2=中,3=高,4=紧急'"` + Status TaskStatus `json:"status" gorm:"type:tinyint;default:1;comment:'任务状态:1=待办,2=进行中,3=已完成,4=已取消'"` + Position int `json:"position" gorm:"not null;comment:'任务在列中的排序位置'"` + DueDate *time.Time `json:"due_date"` + StartDate *time.Time `json:"start_date"` + EndDate *time.Time `json:"end_date"` + EstimatedHours *float64 `json:"estimated_hours" gorm:"type:decimal(8,2);comment:'预估工时(小时)'"` + ActualHours *float64 `json:"actual_hours" gorm:"type:decimal(8,2);comment:'实际工时(小时)'"` + ColumnID uint `json:"column_id" gorm:"not null;index"` + CreatorID uint `json:"creator_id" gorm:"not null;index"` + AssigneeID *uint `json:"assignee_id" gorm:"index"` + ProjectID uint `json:"project_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Column Column `json:"column,omitempty" gorm:"foreignKey:ColumnID"` + Creator User `json:"creator,omitempty" gorm:"foreignKey:CreatorID"` + Assignee *User `json:"assignee,omitempty" gorm:"foreignKey:AssigneeID"` + Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"` + Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:TaskID"` + Attachments []Attachment `json:"attachments,omitempty" gorm:"foreignKey:TaskID"` + Labels []Label `json:"labels,omitempty" gorm:"many2many:task_labels"` +} + +// TaskPriority 任务优先级枚举 +type TaskPriority int + +const ( + TaskPriorityLow TaskPriority = 1 // 低 + TaskPriorityMedium TaskPriority = 2 // 中 + TaskPriorityHigh TaskPriority = 3 // 高 + TaskPriorityUrgent TaskPriority = 4 // 紧急 +) + +// TaskStatus 任务状态枚举 +type TaskStatus int + +const ( + TaskStatusTodo TaskStatus = 1 // 待办 + TaskStatusInProgress TaskStatus = 2 // 进行中 + TaskStatusCompleted TaskStatus = 3 // 已完成 + TaskStatusCancelled TaskStatus = 4 // 已取消 +) diff --git a/backend/models/user.go b/backend/models/user.go new file mode 100644 index 0000000..1844175 --- /dev/null +++ b/backend/models/user.go @@ -0,0 +1,40 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// User 用户表 +type User struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` + Email string `json:"email" gorm:"uniqueIndex;size:100;not null"` + Password string `json:"-" gorm:"size:255;not null"` + Nickname string `json:"nickname" gorm:"size:50"` + Avatar string `json:"avatar" gorm:"size:255"` + Phone string `json:"phone" gorm:"size:20"` + Status UserStatus `json:"status" gorm:"type:tinyint;default:1;comment:'用户状态:1=正常,2=禁用,3=删除'"` + LastLogin *time.Time `json:"last_login"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Permissions []UserPermission `json:"permissions,omitempty" gorm:"foreignKey:UserID"` + PermissionGroups []UserPermissionGroup `json:"permission_groups,omitempty" gorm:"many2many:user_permission_group_users"` + Projects []Project `json:"projects,omitempty" gorm:"many2many:project_members"` + CreatedTasks []Task `json:"created_tasks,omitempty" gorm:"foreignKey:CreatorID"` + AssignedTasks []Task `json:"assigned_tasks,omitempty" gorm:"foreignKey:AssigneeID"` + Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:UserID"` +} + +// UserStatus 用户状态枚举 +type UserStatus int + +const ( + UserStatusActive UserStatus = 1 // 正常 + UserStatusDisabled UserStatus = 2 // 禁用 + UserStatusDeleted UserStatus = 3 // 删除 +) diff --git a/backend/models/user_permission.go b/backend/models/user_permission.go new file mode 100644 index 0000000..e9a2c36 --- /dev/null +++ b/backend/models/user_permission.go @@ -0,0 +1,48 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// UserPermission 用户权限表 +type UserPermission struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + UserID uint `json:"user_id" gorm:"not null;index"` + PermissionString string `json:"permission_string" gorm:"not null;comment:'权限字符串,;分隔,如*.user.read;project.myproject.task.create'"` + GrantedBy uint `json:"granted_by" gorm:"not null;comment:'授权人ID'"` + ExpiresAt *time.Time `json:"expires_at" gorm:"comment:'权限过期时间,为空表示永不过期'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + User User `json:"user,omitempty" gorm:"foreignKey:UserID"` + GrantedByUser User `json:"granted_by_user,omitempty" gorm:"foreignKey:GrantedBy"` +} + +// UserPermissionGroup 用户权限组表 +type UserPermissionGroup struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + GroupName string `json:"group_name" gorm:"size:100;not null;comment:'权限组名称'"` + PermissionStrings string `json:"permission_strings" gorm:"type:text;not null;comment:'权限字符串列表,;分隔'"` + CreatedBy uint `json:"created_by" gorm:"not null;comment:'创建人ID'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // 关联关系 + Users []User `json:"users,omitempty" gorm:"many2many:user_permission_group_users"` + CreatedByUser User `json:"created_by_user,omitempty" gorm:"foreignKey:CreatedBy"` +} + +// UserPermissionGroupUser 用户权限组用户关联表 +type UserPermissionGroupUser struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + UserPermissionGroupID uint `json:"user_permission_group_id" gorm:"not null;index"` + UserID uint `json:"user_id" gorm:"not null;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} From aaa8f071b9aede83e67fe2c14f11b749cba729ad Mon Sep 17 00:00:00 2001 From: LinXin Date: Tue, 23 Sep 2025 16:43:05 +0800 Subject: [PATCH 2/2] update: ER image and fix the warmings --- backend/database/seed.go | 2 +- backend/docs/database/ER-structure.svg | 3373 ++++++++++++++++++++++++ backend/docs/database/structure.mwb | Bin 0 -> 30154 bytes backend/docs/user_permission.md | 11 - 4 files changed, 3374 insertions(+), 12 deletions(-) create mode 100644 backend/docs/database/ER-structure.svg create mode 100644 backend/docs/database/structure.mwb delete mode 100644 backend/docs/user_permission.md diff --git a/backend/database/seed.go b/backend/database/seed.go index bc125a3..7da972b 100644 --- a/backend/database/seed.go +++ b/backend/database/seed.go @@ -25,7 +25,7 @@ func Seed(db *gorm.DB) error { return nil } -func createDefaultUsers(db *gorm.DB) error { +func createDefaultUsers(_ *gorm.DB) error { // TODO: 创建默认用户,目前密码加密方法还没确定 return nil } diff --git a/backend/docs/database/ER-structure.svg b/backend/docs/database/ER-structure.svg new file mode 100644 index 0000000..86f4652 --- /dev/null +++ b/backend/docs/database/ER-structure.svg @@ -0,0 +1,3373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/docs/database/structure.mwb b/backend/docs/database/structure.mwb new file mode 100644 index 0000000000000000000000000000000000000000..9a02f91c0d3132156d5d86ebe06b30c7a3ad8e48 GIT binary patch literal 30154 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 literal 0 HcmV?d00001 diff --git a/backend/docs/user_permission.md b/backend/docs/user_permission.md deleted file mode 100644 index 3d27446..0000000 --- a/backend/docs/user_permission.md +++ /dev/null @@ -1,11 +0,0 @@ -# 用户权限设计 - -## 权限树 - -所有的权限都基于根权限'*',当检查权限时,扫描器会检查所需权限是否在的用户拥有的权限的子集中,举几个例子: - -全局管理员:'*' -用户管理员:'users.*' -Project A (project id=1) 项目管理员:project.1.* -Project B (project id=2) 项目管理员:project.2.* -Project A (project id=1) 项目成员:project.1.view; project.1.write; ...