feat: initial lifelog project

- 待办事项(支持优先级、截止日期)
- 每日记录(上午/下午/晚上分条目)
- Go后端 + Gin + GORM + MySQL
- React前端 + TailwindCSS
This commit is contained in:
2026-07-28 16:22:46 +08:00
commit d495a2b798
32 changed files with 4501 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
package handler
import (
"net/http"
"strconv"
"time"
"lifelog/internal/service"
"github.com/gin-gonic/gin"
)
type Handler struct {
todoSvc *service.TodoService
dailyLogSvc *service.DailyLogService
}
func NewHandler(todoSvc *service.TodoService, dailyLogSvc *service.DailyLogService) *Handler {
return &Handler{todoSvc: todoSvc, dailyLogSvc: dailyLogSvc}
}
// Todo handlers
func (h *Handler) GetTodos(c *gin.Context) {
todos, err := h.todoSvc.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": todos})
}
func (h *Handler) CreateTodo(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Priority int `json:"priority"`
DueDate string `json:"due_date"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Priority == 0 {
req.Priority = 2
}
var dueDate *time.Time
if req.DueDate != "" {
t, err := time.Parse("2006-01-02", req.DueDate)
if err == nil {
dueDate = &t
}
}
todo, err := h.todoSvc.Create(req.Title, req.Priority, dueDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": todo})
}
func (h *Handler) UpdateTodo(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req struct {
Title string `json:"title" binding:"required"`
Priority int `json:"priority"`
Done bool `json:"done"`
DueDate string `json:"due_date"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var dueDate *time.Time
if req.DueDate != "" {
t, err := time.Parse("2006-01-02", req.DueDate)
if err == nil {
dueDate = &t
}
}
todo, err := h.todoSvc.Update(id, req.Title, req.Priority, req.Done, dueDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": todo})
}
func (h *Handler) ToggleTodo(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
todo, err := h.todoSvc.ToggleDone(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": todo})
}
func (h *Handler) DeleteTodo(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
err = h.todoSvc.Delete(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// DailyLog handlers
func (h *Handler) GetDailyLog(c *gin.Context) {
date := c.Param("date")
if date == "" {
date = time.Now().Format("2006-01-02")
}
log, err := h.dailyLogSvc.GetByDate(date)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": log})
}
func (h *Handler) GetRecentLogs(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "30")
limit, _ := strconv.Atoi(limitStr)
logs, err := h.dailyLogSvc.GetRecent(limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": logs})
}
func (h *Handler) SaveDailyLog(c *gin.Context) {
date := c.Param("date")
if date == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "date is required"})
return
}
var req struct {
Morning string `json:"morning"`
Afternoon string `json:"afternoon"`
Evening string `json:"evening"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log, err := h.dailyLogSvc.Save(date, req.Morning, req.Afternoon, req.Evening)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": log})
}
+33
View File
@@ -0,0 +1,33 @@
package model
import (
"time"
)
type Todo struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Title string `gorm:"type:varchar(255);not null" json:"title"`
Done bool `gorm:"type:tinyint(1);not null;default:0" json:"done"`
Priority int `gorm:"type:tinyint(1);not null;default:2" json:"priority"` // 1高 2中 3低
DueDate *time.Time `gorm:"type:date" json:"due_date"` // 可选截止日期
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Todo) TableName() string {
return "todos"
}
type DailyLog struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Date string `gorm:"type:date;not null;uniqueIndex" json:"date"` // 格式:2024-07-24
Morning string `gorm:"type:text" json:"morning"`
Afternoon string `gorm:"type:text" json:"afternoon"`
Evening string `gorm:"type:text" json:"evening"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (DailyLog) TableName() string {
return "daily_logs"
}
+78
View File
@@ -0,0 +1,78 @@
package repository
import (
"lifelog/internal/model"
"gorm.io/gorm"
)
type TodoRepository struct {
db *gorm.DB
}
func NewTodoRepository(db *gorm.DB) *TodoRepository {
return &TodoRepository{db: db}
}
func (r *TodoRepository) GetAll() ([]model.Todo, error) {
var todos []model.Todo
err := r.db.Order("priority asc, created_at desc").Find(&todos).Error
return todos, err
}
func (r *TodoRepository) Create(todo *model.Todo) error {
return r.db.Create(todo).Error
}
func (r *TodoRepository) Update(todo *model.Todo) error {
return r.db.Save(todo).Error
}
func (r *TodoRepository) Delete(id uint64) error {
return r.db.Delete(&model.Todo{}, id).Error
}
func (r *TodoRepository) GetByID(id uint64) (*model.Todo, error) {
var todo model.Todo
err := r.db.First(&todo, id).Error
return &todo, err
}
type DailyLogRepository struct {
db *gorm.DB
}
func NewDailyLogRepository(db *gorm.DB) *DailyLogRepository {
return &DailyLogRepository{db: db}
}
func (r *DailyLogRepository) GetByDate(date string) (*model.DailyLog, error) {
var log model.DailyLog
err := r.db.Where("date = ?", date).First(&log).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &log, err
}
func (r *DailyLogRepository) GetRecent(limit int) ([]model.DailyLog, error) {
var logs []model.DailyLog
err := r.db.Order("date desc").Limit(limit).Find(&logs).Error
return logs, err
}
func (r *DailyLogRepository) Upsert(log *model.DailyLog) error {
var existing model.DailyLog
err := r.db.Where("date = ?", log.Date).First(&existing).Error
if err == gorm.ErrRecordNotFound {
return r.db.Create(log).Error
}
if err != nil {
return err
}
// 更新已有记录
existing.Morning = log.Morning
existing.Afternoon = log.Afternoon
existing.Evening = log.Evening
return r.db.Save(&existing).Error
}
+99
View File
@@ -0,0 +1,99 @@
package service
import (
"errors"
"lifelog/internal/model"
"lifelog/internal/repository"
"time"
)
type TodoService struct {
repo *repository.TodoRepository
}
func NewTodoService(repo *repository.TodoRepository) *TodoService {
return &TodoService{repo: repo}
}
func (s *TodoService) GetAll() ([]model.Todo, error) {
return s.repo.GetAll()
}
func (s *TodoService) Create(title string, priority int, dueDate *time.Time) (*model.Todo, error) {
todo := &model.Todo{
Title: title,
Done: false,
Priority: priority,
DueDate: dueDate,
}
err := s.repo.Create(todo)
return todo, err
}
func (s *TodoService) Update(id uint64, title string, priority int, done bool, dueDate *time.Time) (*model.Todo, error) {
todo, err := s.repo.GetByID(id)
if err != nil {
return nil, err
}
todo.Title = title
todo.Priority = priority
todo.Done = done
todo.DueDate = dueDate
err = s.repo.Update(todo)
return todo, err
}
func (s *TodoService) ToggleDone(id uint64) (*model.Todo, error) {
todo, err := s.repo.GetByID(id)
if err != nil {
return nil, err
}
todo.Done = !todo.Done
err = s.repo.Update(todo)
return todo, err
}
func (s *TodoService) Delete(id uint64) error {
return s.repo.Delete(id)
}
type DailyLogService struct {
repo *repository.DailyLogRepository
}
func NewDailyLogService(repo *repository.DailyLogRepository) *DailyLogService {
return &DailyLogService{repo: repo}
}
func (s *DailyLogService) GetByDate(date string) (*model.DailyLog, error) {
log, err := s.repo.GetByDate(date)
if err != nil {
return nil, err
}
if log == nil {
// 返回空记录
return &model.DailyLog{Date: date}, nil
}
return log, nil
}
func (s *DailyLogService) GetRecent(limit int) ([]model.DailyLog, error) {
if limit <= 0 {
limit = 30
}
return s.repo.GetRecent(limit)
}
func (s *DailyLogService) Save(date, morning, afternoon, evening string) (*model.DailyLog, error) {
if date == "" {
return nil, errors.New("date is required")
}
log := &model.DailyLog{
Date: date,
Morning: morning,
Afternoon: afternoon,
Evening: evening,
}
err := s.repo.Upsert(log)
return log, err
}