refactor(be): 今日任务并进计划节点,养护方案改成可选、按重复规则配
## 三个问题,一起解决 你问「模版要不要做成可选」,摸下来发现底下有三个更硬的: **① 根本没有多套模版可选。** 模板表唯一索引是 (species, stage) —— 同一个「猫 × 幼年期」只能有一套。「让用户选模版」在数据模型上做不到。 去掉唯一索引,加 Name / Summary / Sort / Enabled。 **② 今日任务和计划节点是两套,而且是我上一轮改出来的。** DailyTask 模板 kind=task 生成,ensureDayTasks 每天把昨天那份复制到今天 PlanTask 模板 kind=plan 生成,Day 偏移 计划改成月度管理 + 加了「应用到每天」之后,两者做的是同一件事, 但首页读 DailyTask、计划页管 PlanTask —— 用户在计划页加一条「每天喂药」, 首页今日任务里看不到。 DailyTask 退役。task.go 重写成 PlanTask 的一层「按日期取」的门面, /pets/:id/tasks 路径不变,小程序端不用为这次合并改调用。 PlanTask 加 Priority(首页那份要能标出哪条重要)。 顺手删掉重复的 CreateTask/UpdateTask/DeleteTask —— /plan-tasks 那套已经 覆盖了,留着两套会出现「首页管理弹层加的任务」和「计划页加的节点」进不同的表。 **③ 模板的 Day 偏移和月度计划对不上。** 月底建档的话 day=14/21/30 全落到 下个月,用户建完档打开计划页发现本月是空的。 改成重复规则:once(偏移 N 天,疫苗序列要用)/ daily / weekly(星期几) / monthly(几号)。套用 = 展开成目标月份的具体日期。 「每月 31 号」在 2 月按月末算,不然那个月直接没有。 列名用 repeat_rule 不用 repeat —— repeat 是 MySQL 保留字(和 group_key 同理)。 ## 展开成实体行,不是「存规则读时展开」 勾选状态是按天的。存规则得再开一张表记「哪天打过勾」,而且用户把 「每周二梳毛」这周挪到周三就要引入例外记录。一次展开成行,增删改和打勾全免费。 代价是套用后模板和计划断开:后台改模板不影响已套用的宠物。这是有意的—— 用户已经在自己的计划上勾了几十个勾,后台一改就被覆盖才是灾难。 ## 建档不再自动铺 seedPetDefaults 只建必要提醒(疫苗、驱虫)。漏针是真会出事的,而且它们是 「到期提醒」不占计划页的格子。其余交给用户挑方案。 自动铺的问题是:用户还没看懂这个 App 是干什么的,首页已经躺着十几条他没 答应过的任务,第一反应是「这些是什么」而不是「我该做什么」。 **改完发现还有第二条自动铺的路径**:newStagePlan 在建计划壳子时顺手把模板 铺成节点。所以我改完 seedPetDefaults 之后,建档后首页仍然躺着 5 条任务。 惰性生成尤其难查——不是建档时发生的,是第一次读计划才发生的。 ## 方案摘要按重复方式分开报数 一开始只报一个总数,「10 条」套用一整月展开成 130 条,用户会以为出 bug。 现在分成 每天 4 · 每周 0 · 每月 0 · 一次性 6 · 提醒 4。 ## 数据迁移(预生产库,已执行) 模板明细 kind=task → plan + repeat=daily 28 条 模板明细 kind=plan 补 repeat=once 44 条 8 套方案补名字和摘要 DailyTask → PlanTask 34 条,跳过 0 DailyTask 表的行**没删**,先留着当备份。 迁移第一版又踩了「列还没 AutoMigrate 出来就跑」那个坑:Count 静默返回 0, 我以为「没有需要补的」。第二版加了 HasColumn 前置检查才发现 44 条没转。 和上次 record_types.fields 回填是同一个错。 ## 验证(预生产库实跑) 今日任务/计划页今天 4 条 vs 4 条,一致 建档后 0 任务 / 0 计划节点 / 4 条必要提醒 套用本月 从今天到月末,4 条(7/30-7/31 两天) 重复套用 0 条新增(按「日期+标题」去重) 套用下月 130 条覆盖 31 天,每天 4.2 条 全部完成 5 → 0 未做 ## 还没做(下一步) 前端:建档第 4 步选方案、计划页空月份的套用入口、首页「管理」改跳计划页、 弹层删 manageTasks;后台模板页要支持多套 + 重复规则编辑。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,173 +3,96 @@ package service
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
func dayStart(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
// 「今日任务」就是「今天那一天的计划节点」。
|
||||
//
|
||||
// 原来这是两套东西:DailyTask(模板 kind=task 生成,每天把昨天那份复制到今天)
|
||||
// 和 PlanTask(模板 kind=plan 生成,Day 偏移)。计划改成月度管理、并且能
|
||||
// 「应用到每天」之后,两者做的是同一件事,但首页读 DailyTask、计划页管 PlanTask
|
||||
// —— 用户在计划页加一条「每天喂药」,首页看不到。
|
||||
//
|
||||
// 所以 DailyTask 退役,这个文件只是 PlanTask 的一层「按日期取」的门面。
|
||||
// 接口路径 /pets/:id/tasks 保留不变,小程序端不用为这次合并改调用。
|
||||
|
||||
// ensureDayTasks 惰性生成某天的任务:今天/未来若为空,则从「最近一次有任务的那天」顺延复制,
|
||||
// 无历史则用养护模板。过去的日子不补(保留历史真实)。
|
||||
func (s *Service) ensureDayTasks(pet *model.Pet, day time.Time) {
|
||||
start := dayStart(day)
|
||||
if start.Before(dayStart(time.Now())) {
|
||||
return
|
||||
}
|
||||
end := start.AddDate(0, 0, 1)
|
||||
var cnt int64
|
||||
s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ?", pet.ID, start, end).Count(&cnt)
|
||||
if cnt > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var defs []model.DailyTask
|
||||
var latest model.DailyTask
|
||||
if err := s.db.Where("pet_id = ? AND task_date < ?", pet.ID, start).Order("task_date desc, id desc").First(&latest).Error; err == nil {
|
||||
ls := dayStart(latest.TaskDate)
|
||||
var prev []model.DailyTask
|
||||
s.db.Where("pet_id = ? AND task_date >= ? AND task_date < ?", pet.ID, ls, ls.AddDate(0, 0, 1)).Order("id asc").Find(&prev)
|
||||
for _, t := range prev {
|
||||
defs = append(defs, model.DailyTask{
|
||||
PetID: pet.ID, UserID: pet.UserID, TaskDate: start,
|
||||
Title: t.Title, Description: t.Description, Priority: t.Priority, SheetType: t.SheetType,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for _, it := range s.careTemplateItems(SpeciesOf(pet.Type), pet.Stage) {
|
||||
if it.Kind == model.CareKindTask {
|
||||
defs = append(defs, model.DailyTask{
|
||||
PetID: pet.ID, UserID: pet.UserID, TaskDate: start,
|
||||
Title: it.Title, Description: it.Description, Priority: it.Priority, SheetType: it.SheetType,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(defs) > 0 {
|
||||
s.db.Create(&defs)
|
||||
}
|
||||
}
|
||||
|
||||
// ListTasks 列出宠物某天任务(date 为空则取今天);今天/未来为空会惰性生成
|
||||
func (s *Service) ListTasks(userID, petID string, date *time.Time) ([]model.DailyTask, error) {
|
||||
pet, err := s.ownedPet(userID, petID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := time.Now()
|
||||
if date != nil {
|
||||
target = *date
|
||||
}
|
||||
s.ensureDayTasks(pet, target)
|
||||
|
||||
start := dayStart(target)
|
||||
end := start.AddDate(0, 0, 1)
|
||||
var tasks []model.DailyTask
|
||||
err = s.db.Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end).
|
||||
Order("id asc").Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// TaskInput 增/改任务入参
|
||||
// TaskInput 保留给 handler 绑定用;增删改现在统一走 /plan-tasks 那套
|
||||
type TaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Priority string
|
||||
SheetType string
|
||||
Date *time.Time // 仅新增用,默认今天
|
||||
}
|
||||
|
||||
// CreateTask 为某天新增一条任务(默认今天)
|
||||
func (s *Service) CreateTask(userID, petID string, in TaskInput) (*model.DailyTask, error) {
|
||||
// ListTasks 某一天要做的事。不传日期就是今天
|
||||
func (s *Service) ListTasks(userID, petID string, date *time.Time) ([]model.PlanTask, error) {
|
||||
if _, err := s.ownedPet(userID, petID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
day := time.Now()
|
||||
if in.Date != nil {
|
||||
day = *in.Date
|
||||
plan, err := s.planOf(userID, petID)
|
||||
if err != nil {
|
||||
// 没有计划就没有任务,返回空数组而不是错误——新建档的宠物必然走这一支
|
||||
return []model.PlanTask{}, nil
|
||||
}
|
||||
t := model.DailyTask{
|
||||
PetID: petID, UserID: userID, TaskDate: dayStart(day),
|
||||
Title: in.Title, Description: in.Description, Priority: in.Priority, SheetType: in.SheetType,
|
||||
target := time.Now()
|
||||
if date != nil {
|
||||
target = *date
|
||||
}
|
||||
if err := s.db.Create(&t).Error; err != nil {
|
||||
return nil, err
|
||||
day := dayIndexOf(plan, target)
|
||||
if day < 0 {
|
||||
return []model.PlanTask{}, nil
|
||||
}
|
||||
return &t, nil
|
||||
var tasks []model.PlanTask
|
||||
s.db.Where("plan_id = ? AND day = ?", plan.ID, day).Order("id asc").Find(&tasks)
|
||||
if tasks == nil {
|
||||
tasks = []model.PlanTask{}
|
||||
}
|
||||
stamp := plan.StartDate.AddDate(0, 0, day).Format("2006-01-02")
|
||||
for i := range tasks {
|
||||
tasks[i].Date = stamp
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// UpdateTask 修改任务文案/优先级/关联弹层
|
||||
func (s *Service) UpdateTask(userID, taskID string, in TaskInput) (*model.DailyTask, error) {
|
||||
var t model.DailyTask
|
||||
if err := s.db.Where("id = ? AND user_id = ?", taskID, userID).First(&t).Error; err != nil {
|
||||
return nil, ErrNotFound
|
||||
// dayIndexOf 某个日期在这份计划里是第几天;早于计划开始日返回 -1
|
||||
func dayIndexOf(plan *model.Plan, t time.Time) int {
|
||||
if plan.StartDate == nil {
|
||||
return -1
|
||||
}
|
||||
if err := s.db.Model(&model.DailyTask{}).Where("id = ?", taskID).Updates(map[string]any{
|
||||
"title": in.Title, "description": in.Description, "priority": in.Priority, "sheet_type": in.SheetType,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
st := time.Date(plan.StartDate.Year(), plan.StartDate.Month(), plan.StartDate.Day(),
|
||||
0, 0, 0, 0, time.Local)
|
||||
d := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local)
|
||||
if d.Before(st) {
|
||||
return -1
|
||||
}
|
||||
return s.UpdateTaskReload(taskID)
|
||||
return int(d.Sub(st).Hours() / 24)
|
||||
}
|
||||
|
||||
// UpdateTaskReload 重新读取任务
|
||||
func (s *Service) UpdateTaskReload(taskID string) (*model.DailyTask, error) {
|
||||
var t model.DailyTask
|
||||
// ToggleTask 勾选/取消。和 TogglePlanTask 是同一件事,保留这个名字是为了
|
||||
// 让 /tasks/:id/toggle 这个老路径继续可用
|
||||
func (s *Service) ToggleTask(userID, taskID string) (*model.PlanTask, error) {
|
||||
if err := s.TogglePlanTask(userID, taskID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var t model.PlanTask
|
||||
if err := s.db.First(&t, "id = ?", taskID).Error; err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务
|
||||
func (s *Service) DeleteTask(userID, taskID string) error {
|
||||
res := s.db.Where("id = ? AND user_id = ?", taskID, userID).Delete(&model.DailyTask{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
// CompleteAllTasks 把今天的都勾上
|
||||
func (s *Service) CompleteAllTasks(userID, petID string) ([]model.PlanTask, error) {
|
||||
plan, err := s.planOf(userID, petID)
|
||||
if err != nil {
|
||||
return []model.PlanTask{}, nil
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrNotFound
|
||||
day := dayIndexOf(plan, time.Now())
|
||||
if day < 0 {
|
||||
return []model.PlanTask{}, nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleTask 切换任务完成状态
|
||||
func (s *Service) ToggleTask(userID, taskID string) (*model.DailyTask, error) {
|
||||
var task model.DailyTask
|
||||
if err := s.db.Where("id = ? AND user_id = ?", taskID, userID).First(&task).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
task.Done = !task.Done
|
||||
if task.Done {
|
||||
now := time.Now()
|
||||
task.CompletedAt = &now
|
||||
} else {
|
||||
task.CompletedAt = nil
|
||||
}
|
||||
if err := s.db.Save(&task).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// CompleteAllTasks 完成宠物今日全部任务
|
||||
func (s *Service) CompleteAllTasks(userID, petID string) ([]model.DailyTask, error) {
|
||||
if _, err := s.ownedPet(userID, petID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
end := start.AddDate(0, 0, 1)
|
||||
if err := s.db.Model(&model.DailyTask{}).
|
||||
Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end).
|
||||
Updates(map[string]any{"done": true, "completed_at": now}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListTasks(userID, petID, &now)
|
||||
s.db.Model(&model.PlanTask{}).
|
||||
Where("plan_id = ? AND day = ? AND done = ?", plan.ID, day, false).
|
||||
Update("done", true)
|
||||
return s.ListTasks(userID, petID, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user