Add one-way task completion and exportable pet identity cards
ci-cd / verify (push) Successful in 1m31s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 11:32:32 +08:00
parent f0ee63e642
commit cb4ffa13e9
21 changed files with 999 additions and 117 deletions
+72 -14
View File
@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/sundynix/pets-be/internal/model"
)
@@ -179,11 +180,6 @@ func (a *API) deleteTask(c *gin.Context) {
}
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"`
@@ -192,20 +188,82 @@ func (a *API) setTaskCompletion(c *gin.Context) {
fail(c, http.StatusBadRequest, "完成状态参数无效")
return
}
if !in.Completed {
fail(c, http.StatusBadRequest, "已完成的任务不能取消完成")
return
}
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
}
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, "取消完成失败")
item := model.TaskCompletion{TaskID: task.ID, UserID: actor(c), Date: date, CompletedAt: time.Now()}
if err := a.db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "task_id"}, {Name: "date"}}, DoNothing: true}).Create(&item).Error; err != nil {
fail(c, http.StatusInternalServerError, "保存完成状态失败")
return
}
ok(c, gin.H{"id": task.ID, "date": in.Date, "completed": in.Completed})
ok(c, gin.H{"id": task.ID, "date": in.Date, "completed": true})
}
// Complete the displayed tasks atomically, allowing safe retries and concurrent taps.
func (a *API) completeTasks(c *gin.Context) {
var in struct {
Date string `json:"date" binding:"required"`
TaskIDs []string `json:"task_ids" binding:"required,min=1,max=200"`
}
if c.ShouldBindJSON(&in) != nil {
fail(c, http.StatusBadRequest, "请选择 1 至 200 项任务")
return
}
date, err := parseDay(in.Date)
if err != nil {
fail(c, http.StatusBadRequest, "日期格式应为 YYYY-MM-DD")
return
}
ids := make([]string, 0, len(in.TaskIDs))
seen := map[string]bool{}
for _, id := range in.TaskIDs {
if !validID(id) {
fail(c, http.StatusBadRequest, "任务 ID 无效")
return
}
if !seen[id] {
ids = append(ids, id)
seen[id] = true
}
}
invalid := false
err = a.db.Transaction(func(tx *gorm.DB) error {
var tasks []model.CareTask
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id IN ? AND user_id = ?", ids, actor(c)).Order("id").Find(&tasks).Error; err != nil {
return err
}
if len(tasks) != len(ids) {
invalid = true
return gorm.ErrRecordNotFound
}
items := make([]model.TaskCompletion, 0, len(tasks))
for _, task := range tasks {
if !due(task, date) {
invalid = true
return gorm.ErrRecordNotFound
}
items = append(items, model.TaskCompletion{TaskID: task.ID, UserID: actor(c), Date: date, CompletedAt: time.Now()})
}
return tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "task_id"}, {Name: "date"}}, DoNothing: true}).Create(&items).Error
})
if err != nil {
if invalid {
fail(c, http.StatusBadRequest, "包含不存在或该日期无需完成的任务")
} else {
fail(c, http.StatusInternalServerError, "完成任务失败,请重试")
}
return
}
ok(c, gin.H{"task_ids": ids, "date": in.Date, "completed": true})
}