Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/progress-wall.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions backend/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ Authorization: Bearer <accessToken>
- `boardId`: 看板ID (number)

**请求体**:
```json
```json
{
"name": "string (必填)",
"description": "string (可选)",
Expand Down Expand Up @@ -698,7 +698,7 @@ Authorization: Bearer <accessToken>
"assignee_id": "number (可选)",
"project_id": "number (必填)"
}
```
```

**响应** (201 Created):
```json
Expand Down
1 change: 1 addition & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/joho/godotenv v1.4.0
github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.43.0
gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.7
Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQ
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
72 changes: 72 additions & 0 deletions backend/handlers/notification/notification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package notification

import (
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
"progress-wall-backend/services"
)

// NotificationHandler 通知服务处理器
type NotificationHandler struct {
notificationService *services.TaskNotificationService // 若有服务层可关联,无则留空
}

// NewNotificationHandler 创建通知处理器
func NewNotificationHandler(db *gorm.DB) *NotificationHandler {
return &NotificationHandler{
notificationService: services.NewTaskNotificationService(db), // 若无需服务层可简化为 nil
}
}

// 通知请求体结构(与定时任务格式匹配)
type TaskNotificationReq struct {
UserID uint `json:"user_id"` // 任务创建者ID
TaskID uint `json:"task_id"` // 任务ID
TaskTitle string `json:"task_title"` // 任务标题
NotificationType string `json:"notification_type"` // 通知类型
}

// ReceiveTaskNotification 接收定时任务发送的通知
// 对应路由:POST /api/notifications
func (h *NotificationHandler) ReceiveTaskNotification(c *gin.Context) {
var req TaskNotificationReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "无效的通知参数",
"error": err.Error(),
})
return
}

// 终端打印定时任务信息(核心逻辑)
h.printNotification(req)

// 若有业务逻辑可调用服务层处理(如存储通知记录)
// err := h.notificationService.SaveNotification(req)
// if err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"message": "保存通知失败"})
// return
// }

c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "通知接收成功",
})
}

// 终端打印通知信息
func (h *NotificationHandler) printNotification(req TaskNotificationReq) {
fmt.Println("======================================")
fmt.Printf("【定时任务通知】\n")
fmt.Printf("时间:%s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("用户ID:%d\n", req.UserID)
fmt.Printf("任务ID:%d\n", req.TaskID)
fmt.Printf("任务标题:%s\n", req.TaskTitle)
fmt.Printf("类型:%s\n", req.NotificationType)
fmt.Println("======================================\n")
}
30 changes: 30 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"

"progress-wall-backend/config"
"progress-wall-backend/database"
"progress-wall-backend/routes"
"progress-wall-backend/services"

"github.com/robfig/cron/v3"
)

func main() {
Expand Down Expand Up @@ -38,10 +44,34 @@ func main() {
// 设置路由
r := routes.SetupRoutes(db, cfg)

// 初始化并启动定时任务调度器(核心新增逻辑)
var cronInstance *cron.Cron // 声明定时任务实例
// 创建调度器实例(传入数据库连接和通知服务URL)(若是后端有了NotificationService,则将URL改成cfg.NotificationService.URL)
schedulerIns := services.NewScheduler(db, "http://localhost:8080")
// 启动定时任务,返回cron实例用于后续关闭
cronInstance = schedulerIns.Start()
defer cronInstance.Stop() // 程序退出时停止定时任务
log.Println("定时任务调度器已启动")

// 启动HTTP服务器
addr := fmt.Sprintf(":%s", cfg.Server.Port)
log.Printf("服务器启动在端口 %s", cfg.Server.Port)
if err := r.Run(addr); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}

// 等待中断信号(优雅退出逻辑)
quit := make(chan os.Signal, 1)
// 监听Ctrl+C和kill信号
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 阻塞等待信号
log.Println("开始优雅关闭服务...")

// 若定时任务已启动,等待其完全停止
if cronInstance != nil {
cronInstance.Stop()
log.Println("定时任务已停止")
}

log.Println("服务已正常退出")
}
38 changes: 20 additions & 18 deletions backend/models/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@ import (

// 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"`
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=已取消,5=已归档'"`
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"`
DeadlineAlertSent int `json:"deadline_alert_sent" gorm:"default:0;index;comment:'是否发送提醒:1=已发送,0=未发送'"` // 新增字段:是否已发送提醒

// 关联关系
Column Column `json:"column,omitempty" gorm:"foreignKey:ColumnID"`
Expand Down Expand Up @@ -55,4 +56,5 @@ const (
TaskStatusInProgress TaskStatus = 2 // 进行中
TaskStatusCompleted TaskStatus = 3 // 已完成
TaskStatusCancelled TaskStatus = 4 // 已取消
TaskStatusArchived TaskStatus = 5 // 已归档
)
6 changes: 6 additions & 0 deletions backend/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"progress-wall-backend/handlers/auth"
"progress-wall-backend/handlers/board"
"progress-wall-backend/handlers/column"
"progress-wall-backend/handlers/notification"
"progress-wall-backend/handlers/project"
"progress-wall-backend/handlers/task"
"progress-wall-backend/handlers/team"
Expand Down Expand Up @@ -63,6 +64,8 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {
teamHandler := team.NewTeamHandler(db)
boardActivitiesHandler := activity.NewBoardActivitiesHandler(db)
taskActivitiesHandler := activity.NewTaskActivitiesHandler(db)
// 添加通知处理器初始化
notificationHandler := notification.NewNotificationHandler(db)

// 公开路由(不需要认证)
api := r.Group("/api")
Expand All @@ -72,6 +75,9 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine {
authGroup.POST("/login", loginHandler.Login)
authGroup.POST("/register", registerHandler.Register)
}

// 通知相关
api.POST("/notifications", notificationHandler.ReceiveTaskNotification)
}

// 受保护的路由(需要认证)
Expand Down
61 changes: 61 additions & 0 deletions backend/services/notification_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package services

import (
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

// TaskNotificationService定时任务服务
type TaskNotificationService struct {
UserID uint `json:"user_id"` // 对应定时任务的UserID
TaskID uint `json:"task_id"` // 对应定时任务的TaskID
TaskTitle string `json:"task_title"` // 对应定时任务的TaskTitle
NotificationType string `json:"notification_type"` // 通知类型
db *gorm.DB
}

func NewTaskNotificationService(db *gorm.DB) *TaskNotificationService {
return &TaskNotificationService{
db: db,
}
}

// 可直接绑定到 Gin 路由
func HandleTaskNotification(c *gin.Context) {
var req TaskNotificationService
// 解析定时任务发送的 JSON 请求体
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数解析失败",
"error": err.Error(),
})
return
}

// 核心逻辑:在终端打印定时任务筛选出的任务信息
printTaskNotification(req)

// 返回成功响应,让定时任务确认通知发送成功
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "通知接收成功",
"data": req,
})
}

// printTaskNotification 封装终端打印逻辑(可复用)
func printTaskNotification(req TaskNotificationService) {
fmt.Println("======================================")
fmt.Printf("【定时任务自动触发通知】\n")
fmt.Printf("接收时间:%s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("任务创建者ID:%d\n", req.UserID)
fmt.Printf("任务ID:%d\n", req.TaskID)
fmt.Printf("任务标题:%s\n", req.TaskTitle)
fmt.Printf("通知类型:%s\n", req.NotificationType)
fmt.Println("======================================\n")
}
Loading