feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
var clockPattern = regexp.MustCompile(`^([01][0-9]|2[0-3]):[0-5][0-9]$`)
|
||||
|
||||
type taskInput struct {
|
||||
PetID string `json:"pet_id" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Category string `json:"category"`
|
||||
TimeOfDay string `json:"time_of_day"`
|
||||
RepeatRule string `json:"repeat_rule"`
|
||||
ReminderEnabled bool `json:"reminder_enabled"`
|
||||
}
|
||||
|
||||
func due(task model.CareTask, date time.Time) bool {
|
||||
switch task.RepeatRule {
|
||||
case "daily":
|
||||
return true
|
||||
case "weekdays":
|
||||
return date.Weekday() != time.Saturday && date.Weekday() != time.Sunday
|
||||
case "weekends":
|
||||
return date.Weekday() == time.Saturday || date.Weekday() == time.Sunday
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) listTasks(c *gin.Context) {
|
||||
dateString := c.DefaultQuery("date", time.Now().Format("2006-01-02"))
|
||||
date, err := parseDay(dateString)
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "日期格式应为 YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
query := a.db.Where("user_id = ?", actor(c))
|
||||
if petID := c.Query("pet_id"); petID != "" {
|
||||
if _, err := a.ownedPet(actor(c), petID); err != nil {
|
||||
fail(c, http.StatusNotFound, "宠物不存在")
|
||||
return
|
||||
}
|
||||
query = query.Where("pet_id = ?", petID)
|
||||
}
|
||||
var tasks []model.CareTask
|
||||
if err := query.Order("time_of_day ASC").Find(&tasks).Error; err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取任务失败")
|
||||
return
|
||||
}
|
||||
var completions []model.TaskCompletion
|
||||
if err := a.db.Where("user_id = ? AND date = ?", actor(c), date).Find(&completions).Error; err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取完成状态失败")
|
||||
return
|
||||
}
|
||||
done := make(map[string]bool, len(completions))
|
||||
for _, item := range completions {
|
||||
done[item.TaskID] = true
|
||||
}
|
||||
items := make([]gin.H, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if due(task, date) {
|
||||
items = append(items, taskView(task, done[task.ID]))
|
||||
}
|
||||
}
|
||||
ok(c, gin.H{"date": dateString, "items": items})
|
||||
}
|
||||
|
||||
func taskView(task model.CareTask, completed bool) gin.H {
|
||||
return gin.H{"id": task.ID, "pet_id": task.PetID, "title": task.Title,
|
||||
"category": task.Category, "time_of_day": task.TimeOfDay,
|
||||
"repeat_rule": task.RepeatRule, "reminder_enabled": task.ReminderEnabled,
|
||||
"completed": completed}
|
||||
}
|
||||
|
||||
func (a *API) buildTask(c *gin.Context, in taskInput) (model.CareTask, bool) {
|
||||
task := model.CareTask{}
|
||||
if _, err := a.ownedPet(actor(c), in.PetID); err != nil {
|
||||
fail(c, http.StatusNotFound, "宠物不存在")
|
||||
return task, false
|
||||
}
|
||||
in.Title = strings.TrimSpace(in.Title)
|
||||
if in.Title == "" || len([]rune(in.Title)) > 128 {
|
||||
fail(c, http.StatusBadRequest, "任务名称无效")
|
||||
return task, false
|
||||
}
|
||||
if in.Category == "" {
|
||||
in.Category = "other"
|
||||
}
|
||||
if in.TimeOfDay == "" {
|
||||
in.TimeOfDay = "09:00"
|
||||
}
|
||||
if !clockPattern.MatchString(in.TimeOfDay) {
|
||||
fail(c, http.StatusBadRequest, "时间格式应为 HH:MM")
|
||||
return task, false
|
||||
}
|
||||
if in.RepeatRule == "" {
|
||||
in.RepeatRule = "daily"
|
||||
}
|
||||
if in.RepeatRule != "daily" && in.RepeatRule != "weekdays" && in.RepeatRule != "weekends" {
|
||||
fail(c, http.StatusBadRequest, "重复规则无效")
|
||||
return task, false
|
||||
}
|
||||
task = model.CareTask{UserID: actor(c), PetID: in.PetID, Title: in.Title,
|
||||
Category: in.Category, TimeOfDay: in.TimeOfDay, RepeatRule: in.RepeatRule,
|
||||
ReminderEnabled: in.ReminderEnabled}
|
||||
return task, true
|
||||
}
|
||||
|
||||
func (a *API) createTask(c *gin.Context) {
|
||||
var in taskInput
|
||||
if c.ShouldBindJSON(&in) != nil {
|
||||
fail(c, http.StatusBadRequest, "任务资料无效")
|
||||
return
|
||||
}
|
||||
task, valid := a.buildTask(c, in)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
if err := a.db.Create(&task).Error; err != nil {
|
||||
fail(c, http.StatusInternalServerError, "添加任务失败")
|
||||
return
|
||||
}
|
||||
created(c, taskView(task, false))
|
||||
}
|
||||
|
||||
func (a *API) updateTask(c *gin.Context) {
|
||||
var task model.CareTask
|
||||
if !validID(c.Param("id")) || a.db.Where("id = ? AND user_id = ?", c.Param("id"), actor(c)).First(&task).Error != nil {
|
||||
fail(c, http.StatusNotFound, "任务不存在")
|
||||
return
|
||||
}
|
||||
var in taskInput
|
||||
if c.ShouldBindJSON(&in) != nil {
|
||||
fail(c, http.StatusBadRequest, "任务资料无效")
|
||||
return
|
||||
}
|
||||
updated, valid := a.buildTask(c, in)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
task.PetID, task.Title, task.Category = updated.PetID, updated.Title, updated.Category
|
||||
task.TimeOfDay, task.RepeatRule, task.ReminderEnabled = updated.TimeOfDay, updated.RepeatRule, updated.ReminderEnabled
|
||||
if a.db.Save(&task).Error != nil {
|
||||
fail(c, http.StatusInternalServerError, "更新任务失败")
|
||||
return
|
||||
}
|
||||
ok(c, taskView(task, false))
|
||||
}
|
||||
|
||||
func (a *API) deleteTask(c *gin.Context) {
|
||||
if !validID(c.Param("id")) {
|
||||
fail(c, http.StatusBadRequest, "任务 ID 无效")
|
||||
return
|
||||
}
|
||||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||
var task model.CareTask
|
||||
if err := tx.Where("id = ? AND user_id = ?", c.Param("id"), actor(c)).First(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("task_id = ?", task.ID).Delete(&model.TaskCompletion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&task).Error
|
||||
})
|
||||
if err != nil {
|
||||
fail(c, http.StatusNotFound, "任务不存在或删除失败")
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (a *API) setTaskCompletion(c *gin.Context) {
|
||||
var task model.CareTask
|
||||
if !validID(c.Param("id")) || a.db.Where("id = ? AND user_id = ?", c.Param("id"), actor(c)).First(&task).Error != nil {
|
||||
fail(c, http.StatusNotFound, "任务不存在")
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Date string `json:"date" binding:"required"`
|
||||
Completed bool `json:"completed"`
|
||||
}
|
||||
if c.ShouldBindJSON(&in) != nil {
|
||||
fail(c, http.StatusBadRequest, "完成状态参数无效")
|
||||
return
|
||||
}
|
||||
date, err := parseDay(in.Date)
|
||||
if err != nil || !due(task, date) {
|
||||
fail(c, http.StatusBadRequest, "该日期没有这项任务")
|
||||
return
|
||||
}
|
||||
if in.Completed {
|
||||
item := model.TaskCompletion{TaskID: task.ID, UserID: actor(c), Date: date, CompletedAt: time.Now()}
|
||||
if err := a.db.Where("task_id = ? AND date = ?", task.ID, date).FirstOrCreate(&item).Error; err != nil {
|
||||
fail(c, http.StatusInternalServerError, "保存完成状态失败")
|
||||
return
|
||||
}
|
||||
} else if err := a.db.Where("task_id = ? AND date = ?", task.ID, date).Delete(&model.TaskCompletion{}).Error; err != nil {
|
||||
fail(c, http.StatusInternalServerError, "取消完成失败")
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"id": task.ID, "date": in.Date, "completed": in.Completed})
|
||||
}
|
||||
Reference in New Issue
Block a user