Initial commit: Gitea Project Management System

Features:
- Complete project management system with Epic/Story/Task hierarchy
- Vue.js 3 + Element Plus frontend with kanban board
- Go backend with Gin framework and GORM
- OAuth2 integration with Gitea
- Docker containerization with MySQL
- RESTful API for project, task, and user management
- JWT authentication and authorization
- Responsive web interface with dashboard
This commit is contained in:
huxunan
2025-09-22 14:53:53 +08:00
commit 885fad6c64
33 changed files with 4128 additions and 0 deletions

152
internal/models/common.go Normal file
View File

@@ -0,0 +1,152 @@
package models
import (
"time"
)
type Comment struct {
ID uint `json:"id" gorm:"primaryKey"`
EntityType string `json:"entity_type" gorm:"type:varchar(50)"`
EntityID uint `json:"entity_id"`
UserID uint `json:"user_id"`
Content string `json:"content"`
User User `json:"user" gorm:"foreignKey:UserID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type EntityType string
const (
EntityTypeProject EntityType = "project"
EntityTypeEpic EntityType = "epic"
EntityTypeStory EntityType = "story"
EntityTypeTask EntityType = "task"
)
type Tag struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique;not null"`
Color string `json:"color" gorm:"default:'#1890ff'"`
CreatedAt time.Time `json:"created_at"`
}
type EntityTag struct {
ID uint `json:"id" gorm:"primaryKey"`
EntityType EntityType `json:"entity_type" gorm:"type:varchar(50)"`
EntityID uint `json:"entity_id"`
TagID uint `json:"tag_id"`
Tag Tag `json:"tag" gorm:"foreignKey:TagID"`
CreatedAt time.Time `json:"created_at"`
}
type TimeLog struct {
ID uint `json:"id" gorm:"primaryKey"`
TaskID uint `json:"task_id"`
UserID uint `json:"user_id"`
Hours float64 `json:"hours"`
Description string `json:"description"`
LogDate time.Time `json:"log_date"`
Task Task `json:"task" gorm:"foreignKey:TaskID"`
User User `json:"user" gorm:"foreignKey:UserID"`
CreatedAt time.Time `json:"created_at"`
}
func CreateComment(comment *Comment) error {
return DB.Create(comment).Error
}
func GetCommentsByEntity(entityType string, entityID uint) ([]Comment, error) {
var comments []Comment
err := DB.Where("entity_type = ? AND entity_id = ?", entityType, entityID).
Preload("User").
Order("created_at DESC").
Find(&comments).Error
return comments, err
}
func UpdateComment(comment *Comment) error {
return DB.Save(comment).Error
}
func DeleteComment(id uint) error {
return DB.Delete(&Comment{}, id).Error
}
func CreateTag(tag *Tag) error {
return DB.Create(tag).Error
}
func GetAllTags() ([]Tag, error) {
var tags []Tag
err := DB.Find(&tags).Error
return tags, err
}
func GetTagByName(name string) (*Tag, error) {
var tag Tag
err := DB.Where("name = ?", name).First(&tag).Error
return &tag, err
}
func UpdateTag(tag *Tag) error {
return DB.Save(tag).Error
}
func DeleteTag(id uint) error {
return DB.Delete(&Tag{}, id).Error
}
func AddTagToEntity(entityType EntityType, entityID, tagID uint) error {
entityTag := EntityTag{
EntityType: entityType,
EntityID: entityID,
TagID: tagID,
}
return DB.Create(&entityTag).Error
}
func RemoveTagFromEntity(entityType EntityType, entityID, tagID uint) error {
return DB.Where("entity_type = ? AND entity_id = ? AND tag_id = ?",
entityType, entityID, tagID).Delete(&EntityTag{}).Error
}
func GetTagsByEntity(entityType EntityType, entityID uint) ([]Tag, error) {
var tags []Tag
err := DB.Joins("JOIN entity_tags ON tags.id = entity_tags.tag_id").
Where("entity_tags.entity_type = ? AND entity_tags.entity_id = ?",
entityType, entityID).
Find(&tags).Error
return tags, err
}
func CreateTimeLog(timeLog *TimeLog) error {
return DB.Create(timeLog).Error
}
func GetTimeLogsByTask(taskID uint) ([]TimeLog, error) {
var timeLogs []TimeLog
err := DB.Where("task_id = ?", taskID).
Preload("User").
Order("log_date DESC").
Find(&timeLogs).Error
return timeLogs, err
}
func GetTimeLogsByUser(userID uint, startDate, endDate time.Time) ([]TimeLog, error) {
var timeLogs []TimeLog
err := DB.Where("user_id = ? AND log_date BETWEEN ? AND ?",
userID, startDate, endDate).
Preload("Task.Project").
Order("log_date DESC").
Find(&timeLogs).Error
return timeLogs, err
}
func UpdateTimeLog(timeLog *TimeLog) error {
return DB.Save(timeLog).Error
}
func DeleteTimeLog(id uint) error {
return DB.Delete(&TimeLog{}, id).Error
}

View File

@@ -0,0 +1,60 @@
package models
import (
"fmt"
"time"
"giteapm/config"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func InitDB(cfg config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.Charset)
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %v", err)
}
sqlDB, err := DB.DB()
if err != nil {
return nil, fmt.Errorf("获取底层数据库连接失败: %v", err)
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
if err := autoMigrate(); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %v", err)
}
return DB, nil
}
func autoMigrate() error {
return DB.AutoMigrate(
&User{},
&Project{},
&ProjectMember{},
&Epic{},
&Story{},
&Task{},
&GiteaRelation{},
&Sprint{},
&Comment{},
&Tag{},
&EntityTag{},
&TimeLog{},
&WebhookConfig{},
)
}

129
internal/models/epic.go Normal file
View File

@@ -0,0 +1,129 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Epic struct {
ID uint `json:"id" gorm:"primaryKey"`
ProjectID uint `json:"project_id"`
Title string `json:"title" gorm:"not null"`
Description string `json:"description"`
Status string `json:"status" gorm:"default:'backlog'"`
Priority string `json:"priority" gorm:"default:'medium'"`
AssigneeID *uint `json:"assignee_id"`
ReporterID uint `json:"reporter_id"`
StartDate *time.Time `json:"start_date"`
DueDate *time.Time `json:"due_date"`
EstimatedHours *float64 `json:"estimated_hours"`
ActualHours float64 `json:"actual_hours" gorm:"default:0"`
ProgressPercentage int `json:"progress_percentage" gorm:"default:0"`
Project Project `json:"project" gorm:"foreignKey:ProjectID"`
Assignee *User `json:"assignee" gorm:"foreignKey:AssigneeID"`
Reporter User `json:"reporter" gorm:"foreignKey:ReporterID"`
Stories []Story `json:"stories" gorm:"foreignKey:EpicID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type EpicStatus string
const (
EpicStatusBacklog EpicStatus = "backlog"
EpicStatusPlanning EpicStatus = "planning"
EpicStatusInProgress EpicStatus = "in_progress"
EpicStatusTesting EpicStatus = "testing"
EpicStatusDone EpicStatus = "done"
EpicStatusCancelled EpicStatus = "cancelled"
)
func (e *Epic) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (e *Epic) UpdateProgress() error {
var totalStories int64
var completedStories int64
err := DB.Model(&Story{}).Where("epic_id = ?", e.ID).Count(&totalStories).Error
if err != nil {
return err
}
if totalStories == 0 {
e.ProgressPercentage = 0
return DB.Save(e).Error
}
err = DB.Model(&Story{}).Where("epic_id = ? AND status = 'done'", e.ID).Count(&completedStories).Error
if err != nil {
return err
}
e.ProgressPercentage = int(float64(completedStories) / float64(totalStories) * 100)
return DB.Save(e).Error
}
func (e *Epic) GetStories() ([]Story, error) {
var stories []Story
err := DB.Where("epic_id = ?", e.ID).Preload("Assignee").Find(&stories).Error
return stories, err
}
func CreateEpic(epic *Epic) error {
return DB.Create(epic).Error
}
func GetEpicByID(id uint) (*Epic, error) {
var epic Epic
err := DB.Preload("Project").Preload("Assignee").Preload("Reporter").
Preload("Stories").First(&epic, id).Error
return &epic, err
}
func GetEpicsByProject(projectID uint) ([]Epic, error) {
var epics []Epic
err := DB.Where("project_id = ?", projectID).
Preload("Assignee").Preload("Reporter").
Find(&epics).Error
return epics, err
}
func UpdateEpic(epic *Epic) error {
return DB.Save(epic).Error
}
func DeleteEpic(id uint) error {
return DB.Delete(&Epic{}, id).Error
}
func ListEpics(offset, limit int, filters map[string]interface{}) ([]Epic, int64, error) {
var epics []Epic
var total int64
query := DB.Model(&Epic{})
for key, value := range filters {
switch key {
case "project_id":
query = query.Where("project_id = ?", value)
case "status":
query = query.Where("status = ?", value)
case "priority":
query = query.Where("priority = ?", value)
case "assignee_id":
query = query.Where("assignee_id = ?", value)
}
}
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = query.Preload("Project").Preload("Assignee").Preload("Reporter").
Offset(offset).Limit(limit).Find(&epics).Error
return epics, total, err
}

88
internal/models/gitea.go Normal file
View File

@@ -0,0 +1,88 @@
package models
import (
"time"
)
type GiteaRelation struct {
ID uint `json:"id" gorm:"primaryKey"`
TaskID uint `json:"task_id"`
GiteaRepoID uint `json:"gitea_repo_id"`
GiteaRepoName string `json:"gitea_repo_name"`
RelationType string `json:"relation_type" gorm:"type:varchar(50)"`
GiteaObjectID string `json:"gitea_object_id" gorm:"type:varchar(255)"`
GiteaObjectNumber *uint `json:"gitea_object_number"`
GiteaObjectTitle string `json:"gitea_object_title"`
GiteaObjectURL string `json:"gitea_object_url"`
Task Task `json:"task" gorm:"foreignKey:TaskID"`
CreatedAt time.Time `json:"created_at"`
}
type GiteaRelationType string
const (
RelationTypeBranch GiteaRelationType = "branch"
RelationTypeCommit GiteaRelationType = "commit"
RelationTypePullRequest GiteaRelationType = "pull_request"
RelationTypeIssue GiteaRelationType = "issue"
)
type WebhookConfig struct {
ID uint `json:"id" gorm:"primaryKey"`
ProjectID uint `json:"project_id"`
GiteaRepoID uint `json:"gitea_repo_id"`
GiteaRepoName string `json:"gitea_repo_name"`
WebhookURL string `json:"webhook_url"`
Secret string `json:"secret"`
Events string `json:"events"` // JSON string
Active bool `json:"active" gorm:"default:true"`
Project Project `json:"project" gorm:"foreignKey:ProjectID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func CreateGiteaRelation(relation *GiteaRelation) error {
return DB.Create(relation).Error
}
func GetGiteaRelationsByTask(taskID uint) ([]GiteaRelation, error) {
var relations []GiteaRelation
err := DB.Where("task_id = ?", taskID).Find(&relations).Error
return relations, err
}
func GetGiteaRelationsByRepo(repoID uint, relationType string) ([]GiteaRelation, error) {
var relations []GiteaRelation
query := DB.Where("gitea_repo_id = ?", repoID)
if relationType != "" {
query = query.Where("relation_type = ?", relationType)
}
err := query.Preload("Task").Find(&relations).Error
return relations, err
}
func UpdateGiteaRelation(relation *GiteaRelation) error {
return DB.Save(relation).Error
}
func DeleteGiteaRelation(id uint) error {
return DB.Delete(&GiteaRelation{}, id).Error
}
func CreateWebhookConfig(config *WebhookConfig) error {
return DB.Create(config).Error
}
func GetWebhookConfigByRepo(repoID uint) (*WebhookConfig, error) {
var config WebhookConfig
err := DB.Where("gitea_repo_id = ?", repoID).First(&config).Error
return &config, err
}
func UpdateWebhookConfig(config *WebhookConfig) error {
return DB.Save(config).Error
}
func DeleteWebhookConfig(id uint) error {
return DB.Delete(&WebhookConfig{}, id).Error
}

163
internal/models/project.go Normal file
View File

@@ -0,0 +1,163 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Project struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Description string `json:"description"`
GiteaOrg string `json:"gitea_org"`
Status string `json:"status" gorm:"default:'planning'"`
Priority string `json:"priority" gorm:"default:'medium'"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
OwnerID uint `json:"owner_id"`
Owner User `json:"owner" gorm:"foreignKey:OwnerID"`
Members []ProjectMember `json:"members" gorm:"foreignKey:ProjectID"`
Epics []Epic `json:"epics" gorm:"foreignKey:ProjectID"`
Stories []Story `json:"stories" gorm:"foreignKey:ProjectID"`
Tasks []Task `json:"tasks" gorm:"foreignKey:ProjectID"`
Sprints []Sprint `json:"sprints" gorm:"foreignKey:ProjectID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProjectMember struct {
ID uint `json:"id" gorm:"primaryKey"`
ProjectID uint `json:"project_id"`
UserID uint `json:"user_id"`
Role string `json:"role" gorm:"default:'developer'"`
User User `json:"user" gorm:"foreignKey:UserID"`
JoinedAt time.Time `json:"joined_at"`
}
type ProjectStatus string
const (
ProjectStatusActive ProjectStatus = "active"
ProjectStatusArchived ProjectStatus = "archived"
ProjectStatusPlanning ProjectStatus = "planning"
)
type ProjectPriority string
const (
PriorityLow ProjectPriority = "low"
PriorityMedium ProjectPriority = "medium"
PriorityHigh ProjectPriority = "high"
PriorityCritical ProjectPriority = "critical"
)
type ProjectMemberRole string
const (
ProjectRoleOwner ProjectMemberRole = "owner"
ProjectRoleManager ProjectMemberRole = "manager"
ProjectRoleDeveloper ProjectMemberRole = "developer"
ProjectRoleTester ProjectMemberRole = "tester"
ProjectRoleViewer ProjectMemberRole = "viewer"
)
func (p *Project) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (p *Project) GetMembers() ([]User, error) {
var users []User
err := DB.Joins("JOIN project_members ON users.id = project_members.user_id").
Where("project_members.project_id = ?", p.ID).
Find(&users).Error
return users, err
}
func (p *Project) AddMember(userID uint, role string) error {
member := ProjectMember{
ProjectID: p.ID,
UserID: userID,
Role: role,
JoinedAt: time.Now(),
}
return DB.Create(&member).Error
}
func (p *Project) RemoveMember(userID uint) error {
return DB.Where("project_id = ? AND user_id = ?", p.ID, userID).Delete(&ProjectMember{}).Error
}
func (p *Project) GetProgress() (float64, error) {
var totalTasks int64
var completedTasks int64
err := DB.Model(&Task{}).Where("project_id = ?", p.ID).Count(&totalTasks).Error
if err != nil {
return 0, err
}
if totalTasks == 0 {
return 0, nil
}
err = DB.Model(&Task{}).Where("project_id = ? AND status = 'done'", p.ID).Count(&completedTasks).Error
if err != nil {
return 0, err
}
return float64(completedTasks) / float64(totalTasks) * 100, nil
}
func CreateProject(project *Project) error {
return DB.Create(project).Error
}
func GetProjectByID(id uint) (*Project, error) {
var project Project
err := DB.Preload("Owner").Preload("Members.User").First(&project, id).Error
return &project, err
}
func GetProjectsByUser(userID uint) ([]Project, error) {
var projects []Project
err := DB.Where("owner_id = ?", userID).
Or("id IN (SELECT project_id FROM project_members WHERE user_id = ?)", userID).
Preload("Owner").
Find(&projects).Error
return projects, err
}
func UpdateProject(project *Project) error {
return DB.Save(project).Error
}
func DeleteProject(id uint) error {
return DB.Delete(&Project{}, id).Error
}
func ListProjects(offset, limit int, filters map[string]interface{}) ([]Project, int64, error) {
var projects []Project
var total int64
query := DB.Model(&Project{})
for key, value := range filters {
switch key {
case "status":
query = query.Where("status = ?", value)
case "priority":
query = query.Where("priority = ?", value)
case "owner_id":
query = query.Where("owner_id = ?", value)
}
}
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = query.Preload("Owner").Offset(offset).Limit(limit).Find(&projects).Error
return projects, total, err
}

139
internal/models/sprint.go Normal file
View File

@@ -0,0 +1,139 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Sprint struct {
ID uint `json:"id" gorm:"primaryKey"`
ProjectID uint `json:"project_id"`
Name string `json:"name" gorm:"not null"`
Description string `json:"description"`
Status string `json:"status" gorm:"default:'planning'"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
Goal string `json:"goal"`
Project Project `json:"project" gorm:"foreignKey:ProjectID"`
Stories []Story `json:"stories" gorm:"foreignKey:SprintID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SprintStatus string
const (
SprintStatusPlanning SprintStatus = "planning"
SprintStatusActive SprintStatus = "active"
SprintStatusCompleted SprintStatus = "completed"
)
func (s *Sprint) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (s *Sprint) GetVelocity() (int, error) {
var totalStoryPoints int
err := DB.Model(&Story{}).
Where("sprint_id = ? AND status = 'done'", s.ID).
Select("COALESCE(SUM(story_points), 0)").
Scan(&totalStoryPoints).Error
return totalStoryPoints, err
}
func (s *Sprint) GetBurndownData() ([]map[string]interface{}, error) {
var stories []Story
err := DB.Where("sprint_id = ?", s.ID).Find(&stories).Error
if err != nil {
return nil, err
}
var burndownData []map[string]interface{}
// Calculate total story points
totalPoints := 0
for _, story := range stories {
if story.StoryPoints != nil {
totalPoints += *story.StoryPoints
}
}
// This is a simplified version - in a real implementation,
// you'd track daily progress
burndownData = append(burndownData, map[string]interface{}{
"date": s.StartDate,
"remaining": totalPoints,
"ideal": totalPoints,
})
if s.EndDate != nil {
burndownData = append(burndownData, map[string]interface{}{
"date": s.EndDate,
"remaining": 0,
"ideal": 0,
})
}
return burndownData, nil
}
func CreateSprint(sprint *Sprint) error {
return DB.Create(sprint).Error
}
func GetSprintByID(id uint) (*Sprint, error) {
var sprint Sprint
err := DB.Preload("Project").Preload("Stories.Assignee").
First(&sprint, id).Error
return &sprint, err
}
func GetSprintsByProject(projectID uint) ([]Sprint, error) {
var sprints []Sprint
err := DB.Where("project_id = ?", projectID).
Preload("Stories").
Find(&sprints).Error
return sprints, err
}
func GetActiveSprintByProject(projectID uint) (*Sprint, error) {
var sprint Sprint
err := DB.Where("project_id = ? AND status = 'active'", projectID).
Preload("Stories.Assignee").
First(&sprint).Error
return &sprint, err
}
func UpdateSprint(sprint *Sprint) error {
return DB.Save(sprint).Error
}
func DeleteSprint(id uint) error {
return DB.Delete(&Sprint{}, id).Error
}
func ListSprints(offset, limit int, filters map[string]interface{}) ([]Sprint, int64, error) {
var sprints []Sprint
var total int64
query := DB.Model(&Sprint{})
for key, value := range filters {
switch key {
case "project_id":
query = query.Where("project_id = ?", value)
case "status":
query = query.Where("status = ?", value)
}
}
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = query.Preload("Project").
Offset(offset).Limit(limit).Find(&sprints).Error
return sprints, total, err
}

159
internal/models/story.go Normal file
View File

@@ -0,0 +1,159 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Story struct {
ID uint `json:"id" gorm:"primaryKey"`
EpicID *uint `json:"epic_id"`
ProjectID uint `json:"project_id"`
Title string `json:"title" gorm:"not null"`
Description string `json:"description"`
AcceptanceCriteria string `json:"acceptance_criteria"`
Status string `json:"status" gorm:"default:'backlog'"`
Priority string `json:"priority" gorm:"default:'medium'"`
StoryPoints *int `json:"story_points"`
AssigneeID *uint `json:"assignee_id"`
ReporterID uint `json:"reporter_id"`
SprintID *uint `json:"sprint_id"`
EstimatedHours *float64 `json:"estimated_hours"`
ActualHours float64 `json:"actual_hours" gorm:"default:0"`
Epic *Epic `json:"epic" gorm:"foreignKey:EpicID"`
Project Project `json:"project" gorm:"foreignKey:ProjectID"`
Assignee *User `json:"assignee" gorm:"foreignKey:AssigneeID"`
Reporter User `json:"reporter" gorm:"foreignKey:ReporterID"`
Sprint *Sprint `json:"sprint" gorm:"foreignKey:SprintID"`
Tasks []Task `json:"tasks" gorm:"foreignKey:StoryID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type StoryStatus string
const (
StoryStatusBacklog StoryStatus = "backlog"
StoryStatusTodo StoryStatus = "todo"
StoryStatusInProgress StoryStatus = "in_progress"
StoryStatusReview StoryStatus = "review"
StoryStatusTesting StoryStatus = "testing"
StoryStatusDone StoryStatus = "done"
StoryStatusCancelled StoryStatus = "cancelled"
)
func (s *Story) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (s *Story) GetTasks() ([]Task, error) {
var tasks []Task
err := DB.Where("story_id = ?", s.ID).Preload("Assignee").Find(&tasks).Error
return tasks, err
}
func (s *Story) UpdateProgress() error {
var totalTasks int64
var completedTasks int64
err := DB.Model(&Task{}).Where("story_id = ?", s.ID).Count(&totalTasks).Error
if err != nil {
return err
}
if totalTasks == 0 {
return nil
}
err = DB.Model(&Task{}).Where("story_id = ? AND status = 'done'", s.ID).Count(&completedTasks).Error
if err != nil {
return err
}
if completedTasks == totalTasks {
s.Status = string(StoryStatusDone)
} else if completedTasks > 0 {
s.Status = string(StoryStatusInProgress)
}
return DB.Save(s).Error
}
func CreateStory(story *Story) error {
return DB.Create(story).Error
}
func GetStoryByID(id uint) (*Story, error) {
var story Story
err := DB.Preload("Epic").Preload("Project").Preload("Assignee").
Preload("Reporter").Preload("Sprint").Preload("Tasks").
First(&story, id).Error
return &story, err
}
func GetStoriesByProject(projectID uint) ([]Story, error) {
var stories []Story
err := DB.Where("project_id = ?", projectID).
Preload("Epic").Preload("Assignee").Preload("Reporter").
Find(&stories).Error
return stories, err
}
func GetStoriesByEpic(epicID uint) ([]Story, error) {
var stories []Story
err := DB.Where("epic_id = ?", epicID).
Preload("Assignee").Preload("Reporter").
Find(&stories).Error
return stories, err
}
func GetStoriesBySprint(sprintID uint) ([]Story, error) {
var stories []Story
err := DB.Where("sprint_id = ?", sprintID).
Preload("Assignee").Preload("Reporter").
Find(&stories).Error
return stories, err
}
func UpdateStory(story *Story) error {
return DB.Save(story).Error
}
func DeleteStory(id uint) error {
return DB.Delete(&Story{}, id).Error
}
func ListStories(offset, limit int, filters map[string]interface{}) ([]Story, int64, error) {
var stories []Story
var total int64
query := DB.Model(&Story{})
for key, value := range filters {
switch key {
case "project_id":
query = query.Where("project_id = ?", value)
case "epic_id":
query = query.Where("epic_id = ?", value)
case "sprint_id":
query = query.Where("sprint_id = ?", value)
case "status":
query = query.Where("status = ?", value)
case "priority":
query = query.Where("priority = ?", value)
case "assignee_id":
query = query.Where("assignee_id = ?", value)
}
}
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = query.Preload("Epic").Preload("Project").Preload("Assignee").
Preload("Reporter").Preload("Sprint").
Offset(offset).Limit(limit).Find(&stories).Error
return stories, total, err
}

166
internal/models/task.go Normal file
View File

@@ -0,0 +1,166 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Task struct {
ID uint `json:"id" gorm:"primaryKey"`
StoryID *uint `json:"story_id"`
ProjectID uint `json:"project_id"`
Title string `json:"title" gorm:"not null"`
Description string `json:"description"`
Status string `json:"status" gorm:"default:'todo'"`
Priority string `json:"priority" gorm:"default:'medium'"`
TaskType string `json:"task_type" gorm:"default:'feature'"`
AssigneeID *uint `json:"assignee_id"`
ReporterID uint `json:"reporter_id"`
EstimatedHours *float64 `json:"estimated_hours"`
ActualHours float64 `json:"actual_hours" gorm:"default:0"`
DueDate *time.Time `json:"due_date"`
Story *Story `json:"story" gorm:"foreignKey:StoryID"`
Project Project `json:"project" gorm:"foreignKey:ProjectID"`
Assignee *User `json:"assignee" gorm:"foreignKey:AssigneeID"`
Reporter User `json:"reporter" gorm:"foreignKey:ReporterID"`
GiteaRelations []GiteaRelation `json:"gitea_relations" gorm:"foreignKey:TaskID"`
TimeLogs []TimeLog `json:"time_logs" gorm:"foreignKey:TaskID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type TaskStatus string
const (
TaskStatusTodo TaskStatus = "todo"
TaskStatusInProgress TaskStatus = "in_progress"
TaskStatusReview TaskStatus = "review"
TaskStatusTesting TaskStatus = "testing"
TaskStatusDone TaskStatus = "done"
TaskStatusBlocked TaskStatus = "blocked"
)
type TaskType string
const (
TaskTypeFeature TaskType = "feature"
TaskTypeBug TaskType = "bug"
TaskTypeImprovement TaskType = "improvement"
TaskTypeResearch TaskType = "research"
TaskTypeDocumentation TaskType = "documentation"
)
func (t *Task) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (t *Task) LogTime(userID uint, hours float64, description string, logDate time.Time) error {
timeLog := TimeLog{
TaskID: t.ID,
UserID: userID,
Hours: hours,
Description: description,
LogDate: logDate,
}
if err := DB.Create(&timeLog).Error; err != nil {
return err
}
t.ActualHours += hours
return DB.Save(t).Error
}
func (t *Task) LinkGiteaObject(repoID uint, repoName, relationType, objectID string, objectNumber *uint, title, url string) error {
relation := GiteaRelation{
TaskID: t.ID,
GiteaRepoID: repoID,
GiteaRepoName: repoName,
RelationType: relationType,
GiteaObjectID: objectID,
GiteaObjectNumber: objectNumber,
GiteaObjectTitle: title,
GiteaObjectURL: url,
}
return DB.Create(&relation).Error
}
func CreateTask(task *Task) error {
return DB.Create(task).Error
}
func GetTaskByID(id uint) (*Task, error) {
var task Task
err := DB.Preload("Story").Preload("Project").Preload("Assignee").
Preload("Reporter").Preload("GiteaRelations").Preload("TimeLogs").
First(&task, id).Error
return &task, err
}
func GetTasksByProject(projectID uint) ([]Task, error) {
var tasks []Task
err := DB.Where("project_id = ?", projectID).
Preload("Story").Preload("Assignee").Preload("Reporter").
Find(&tasks).Error
return tasks, err
}
func GetTasksByStory(storyID uint) ([]Task, error) {
var tasks []Task
err := DB.Where("story_id = ?", storyID).
Preload("Assignee").Preload("Reporter").
Find(&tasks).Error
return tasks, err
}
func GetTasksByAssignee(assigneeID uint) ([]Task, error) {
var tasks []Task
err := DB.Where("assignee_id = ?", assigneeID).
Preload("Story").Preload("Project").
Find(&tasks).Error
return tasks, err
}
func UpdateTask(task *Task) error {
return DB.Save(task).Error
}
func DeleteTask(id uint) error {
return DB.Delete(&Task{}, id).Error
}
func ListTasks(offset, limit int, filters map[string]interface{}) ([]Task, int64, error) {
var tasks []Task
var total int64
query := DB.Model(&Task{})
for key, value := range filters {
switch key {
case "project_id":
query = query.Where("project_id = ?", value)
case "story_id":
query = query.Where("story_id = ?", value)
case "status":
query = query.Where("status = ?", value)
case "priority":
query = query.Where("priority = ?", value)
case "task_type":
query = query.Where("task_type = ?", value)
case "assignee_id":
query = query.Where("assignee_id = ?", value)
}
}
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = query.Preload("Story").Preload("Project").Preload("Assignee").
Preload("Reporter").Preload("GiteaRelations").
Offset(offset).Limit(limit).Find(&tasks).Error
return tasks, total, err
}

96
internal/models/user.go Normal file
View File

@@ -0,0 +1,96 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
GiteaUserID *uint `json:"gitea_user_id" gorm:"uniqueIndex"`
Username string `json:"username" gorm:"unique;not null"`
Email string `json:"email" gorm:"not null"`
FullName string `json:"full_name"`
AvatarURL string `json:"avatar_url"`
Role string `json:"role" gorm:"default:'developer'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleDeveloper UserRole = "developer"
RoleTester UserRole = "tester"
RoleViewer UserRole = "viewer"
)
func (u *User) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (u *User) GetOwnedProjects() ([]Project, error) {
var projects []Project
err := DB.Where("owner_id = ?", u.ID).Find(&projects).Error
return projects, err
}
func (u *User) GetJoinedProjects() ([]Project, error) {
var projects []Project
err := DB.Joins("JOIN project_members ON projects.id = project_members.project_id").
Where("project_members.user_id = ?", u.ID).
Find(&projects).Error
return projects, err
}
func (u *User) GetAssignedTasks() ([]Task, error) {
var tasks []Task
err := DB.Where("assignee_id = ?", u.ID).Find(&tasks).Error
return tasks, err
}
func CreateUser(user *User) error {
return DB.Create(user).Error
}
func GetUserByID(id uint) (*User, error) {
var user User
err := DB.First(&user, id).Error
return &user, err
}
func GetUserByUsername(username string) (*User, error) {
var user User
err := DB.Where("username = ?", username).First(&user).Error
return &user, err
}
func GetUserByGiteaID(giteaUserID uint) (*User, error) {
var user User
err := DB.Where("gitea_user_id = ?", giteaUserID).First(&user).Error
return &user, err
}
func UpdateUser(user *User) error {
return DB.Save(user).Error
}
func DeleteUser(id uint) error {
return DB.Delete(&User{}, id).Error
}
func ListUsers(offset, limit int) ([]User, int64, error) {
var users []User
var total int64
err := DB.Model(&User{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
err = DB.Offset(offset).Limit(limit).Find(&users).Error
return users, total, err
}