feat: initial lifelog project
- 待办事项(支持优先级、截止日期) - 每日记录(上午/下午/晚上分条目) - Go后端 + Gin + GORM + MySQL - React前端 + TailwindCSS
This commit is contained in:
@@ -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})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user