package main import ( "fmt" "strconv" "time" "github.com/wailsapp/wails/v3/pkg/services/notifications" ) // ---------- Todo 绑定 ---------- func (a *App) ListTodos(status string, projectID int64) ([]Todo, error) { if e := a.ready(); e != nil { return nil, e } return a.store.ListTodos(status, projectID) } func (a *App) SaveTodo(t Todo) (Todo, error) { if e := a.ready(); e != nil { return t, e } return a.store.SaveTodo(t) } func (a *App) SetTodoStatus(id int64, status string) error { if e := a.ready(); e != nil { return e } return a.store.SetTodoStatus(id, status) } func (a *App) DeleteTodo(id int64) error { if e := a.ready(); e != nil { return e } return a.store.DeleteTodo(id) } // ---------- 工单绑定 ---------- func (a *App) ListTickets(status string, projectID int64) ([]Ticket, error) { if e := a.ready(); e != nil { return nil, e } return a.store.ListTickets(status, projectID) } func (a *App) SaveTicket(t Ticket) (Ticket, error) { if e := a.ready(); e != nil { return t, e } return a.store.SaveTicket(t) } func (a *App) SetTicketStatus(id int64, status string) error { if e := a.ready(); e != nil { return e } return a.store.SetTicketStatus(id, status) } func (a *App) DeleteTicket(id int64) error { if e := a.ready(); e != nil { return e } return a.store.DeleteTicket(id) } // ---------- 记事本与收藏 ---------- func (a *App) GetNote() (Note, error) { if e := a.ready(); e != nil { return Note{}, e } return a.store.GetNote() } func (a *App) SaveNote(content string) (Note, error) { if e := a.ready(); e != nil { return Note{}, e } return a.store.SaveNote(content) } func (a *App) ListNotes(limit int) ([]Note, error) { if e := a.ready(); e != nil { return nil, e } return a.store.ListNotes(limit) } // SaveNoteByID 保存指定笔记;id=0 新建。 func (a *App) SaveNoteByID(id int64, content string) (Note, error) { if e := a.ready(); e != nil { return Note{}, e } return a.store.SaveNoteByID(id, content) } func (a *App) DeleteNote(id int64) error { if e := a.ready(); e != nil { return e } return a.store.DeleteNote(id) } func (a *App) ListFavorites() ([]int64, error) { if e := a.ready(); e != nil { return nil, e } return a.store.ListFavorites() } func (a *App) ToggleFavorite(projectID int64) (bool, error) { if e := a.ready(); e != nil { return false, e } return a.store.ToggleFavorite(projectID) } // ---------- 消息中心 ---------- func (a *App) ListMessages(limit int64) ([]Message, error) { if e := a.ready(); e != nil { return nil, e } return a.store.ListMessages(limit) } func (a *App) UnreadMessageCount() (int64, error) { if e := a.ready(); e != nil { return 0, e } return a.store.UnreadMessageCount(), nil } func (a *App) MarkMessageRead(id int64) error { if e := a.ready(); e != nil { return e } return a.store.MarkMessageRead(id) } func (a *App) MarkAllMessagesRead() error { if e := a.ready(); e != nil { return e } return a.store.MarkAllMessagesRead() } func (a *App) ClearMessages() error { if e := a.ready(); e != nil { return e } return a.store.ClearMessages() } // pushMessage 写入消息中心、广播给前端,并可选发送系统通知。 func (a *App) pushMessage(kind, title, body, sourceType string, sourceID int64, notify bool) { if a.store == nil { return } m, e := a.store.AddMessage(kind, title, body, sourceType, sourceID) if e != nil { return } a.emit("message:new", m) if notify && a.notifier != nil { _ = a.notifier.SendNotification(notifications.NotificationOptions{ ID: fmt.Sprintf("cc-%s-%d", kind, m.ID), Title: title, Body: body, }) } } // ---------- 到期巡检 ---------- // parseDue 解析待办/工单的排期时间(本地时区),支持日期或日期+时间。 func parseDue(v string) (time.Time, bool) { for _, layout := range []string{"2006-01-02T15:04", "2006-01-02 15:04", "2006-01-02"} { if t, e := time.ParseInLocation(layout, v, time.Local); e == nil { // 纯日期视为当天 23:59 截止 if layout == "2006-01-02" { t = t.Add(23*time.Hour + 59*time.Minute) } return t, true } } return time.Time{}, false } // runReminderLoop 每分钟巡检到期 todo/工单:24 小时内到期与已逾期各提醒一次。 func (a *App) runReminderLoop() { t := time.NewTicker(time.Minute) defer t.Stop() for { select { case <-a.ctx.Done(): return case <-t.C: a.checkDueReminders() } } } const ( remindedSoon = 1 remindedOverdue = 2 ) // reminderTexts 返回按当前语言本地化的提醒文案:[todo 逾期, todo 将到期, 工单逾期, 工单将到期, "截止"前缀]。 func (a *App) reminderTexts() [5]string { locale := "zh-CN" if st, e := a.store.Settings(); e == nil { locale = st.Locale } if locale == "en" { return [5]string{"Todo overdue", "Todo due soon", "Ticket overdue", "Ticket due soon", "due"} } return [5]string{"待办已逾期", "待办即将到期", "工单已逾期", "工单即将到期", "截止"} } func (a *App) checkDueReminders() { if a.store == nil || a.bootstrap.State != BootstrapReady { return } txt := a.reminderTexts() now := time.Now() type row struct { id int64 title string due string reminded int64 } scan := func(query string) []row { rows, e := a.store.db.Query(query) if e != nil { return nil } defer rows.Close() out := []row{} for rows.Next() { var x row if rows.Scan(&x.id, &x.title, &x.due, &x.reminded) == nil { out = append(out, x) } } return out } for _, x := range scan(`SELECT id,title,due_at,reminded FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`) { due, ok := parseDue(x.due) if !ok { continue } if now.After(due) && x.reminded&remindedOverdue == 0 { a.pushMessage("todo_due", txt[0], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true) _, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id) } else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 { a.pushMessage("todo_due", txt[1], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true) _, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id) } } for _, x := range scan(`SELECT id,title,due_at,reminded FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`) { due, ok := parseDue(x.due) if !ok { continue } if now.After(due) && x.reminded&remindedOverdue == 0 { a.pushMessage("ticket_due", txt[2], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true) _, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id) } else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 { a.pushMessage("ticket_due", txt[3], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true) _, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id) } } } // ---------- 导航徽标 ---------- // NavBadges 左侧导航徽标数据(一次查询打包返回)。 type NavBadges struct { Unread int `json:"unread"` // 消息中心:未读 TodosOpen int `json:"todosOpen"` // 待办事项:未完成 TicketsActive int `json:"ticketsActive"` // 需求工单:开放 + 进行中 TodayDue int `json:"todayDue"` // 今日任务:逾期 + 今日截止(未完成) CalendarDot bool `json:"calendarDot"` // 排期日历:今天有到期事项 TeamAssigned int `json:"teamAssigned"` // 团队任务:指派给我未完成(同步时缓存) } func (a *App) GetNavBadges() (NavBadges, error) { if e := a.ready(); e != nil { return NavBadges{}, e } var b NavBadges _ = a.store.db.QueryRow(`SELECT COUNT(*) FROM messages WHERE is_read=0`).Scan(&b.Unread) _ = a.store.db.QueryRow(`SELECT COUNT(*) FROM todos WHERE deleted=0 AND status IN ('open','doing')`).Scan(&b.TodosOpen) _ = a.store.db.QueryRow(`SELECT COUNT(*) FROM tickets WHERE deleted=0 AND status IN ('open','in_progress')`).Scan(&b.TicketsActive) b.TodayDue = a.countDueToday() b.CalendarDot = b.TodayDue > 0 if a.syncUserID() > 0 { b.TeamAssigned, _ = strconv.Atoi(a.store.Meta("team_assigned_cache")) } return b, nil } // countDueToday 统计逾期与今日截止的未完成待办/工单。 func (a *App) countDueToday() int { now := time.Now() end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()) n := 0 for _, q := range []string{ `SELECT due_at FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`, `SELECT due_at FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`, } { rows, e := a.store.db.Query(q) if e != nil { continue } for rows.Next() { var v string if rows.Scan(&v) != nil { continue } if due, ok := parseDue(v); ok && !due.After(end) { n++ } } rows.Close() } return n }