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:
Blizzard
2026-07-30 11:50:54 +08:00
parent 69bd56671c
commit 8b1b4cde02
9 changed files with 449 additions and 306 deletions
+48 -19
View File
@@ -1,7 +1,9 @@
package model
// 养护模板:按「物种 × 阶段」配置建档时生成的今日任务 / 30天计划 / 提醒
// 管理员可 AI 生成草稿后修改保存;建档时按宠物物种阶段套用,查不到回退内置默认。
// 养护方案:一套「这个物种、这个阶段该做哪些事」的配置
// 后台可以给同一个 (species, stage) 配多套,建档时让用户挑一套套用,
// 也可以不套用、自己排。原来是一个组合只能有一套(唯一索引卡着),
// 「让用户选模版」这件事在数据模型上做不到。
// 物种取值
const (
@@ -9,37 +11,64 @@ const (
SpeciesDog = "dog"
)
// 模板明细的类别
// 明细的类别。原来还有个 task(今日任务),已经和 plan 合并——
// 「今天要做的事」就是「今天那一天的计划节点」,两套并存的结果是
// 用户在计划页加的「每天喂药」在首页看不到。
const (
CareKindTask = "task" // 今日任务
CareKindPlan = "plan" // 30天计划节点
CareKindReminder = "reminder" // 提醒
CareKindPlan = "plan" // 计划节点(含每天/每周/每月重复)
CareKindReminder = "reminder" // 到期提醒(疫苗、驱虫这类有固定周期的)
)
// CareTemplate 一套模板 = 一个 (species, stage) 组合
// 重复规则。月度计划要的是「多久做一次」,不是「第几天做」——
// 原来只有 Day 偏移,月底建档的话 day=14/21/30 全落到下个月,
// 用户建完档打开计划页发现本月是空的。
const (
RepeatOnce = "once" // 一次性,按 Day 偏移(到家第 N 天,疫苗序列要用)
RepeatDaily = "daily" // 每天
RepeatWeekly = "weekly" // 每周某几天,看 Weekdays
RepeatMonthly = "monthly" // 每月某天,看 MonthDay
)
// CareTemplate 一套方案
type CareTemplate struct {
Base
Species string `gorm:"size:16;uniqueIndex:idx_species_stage" json:"species"` // cat | dog
Stage string `gorm:"size:48;uniqueIndex:idx_species_stage" json:"stage"` // 与 onboarding 阶段一致
Items []CareTemplateItem `gorm:"foreignKey:TemplateID" json:"items"`
Species string `gorm:"size:16;index" json:"species"` // cat | dog
Stage string `gorm:"size:48;index" json:"stage"` // 与 onboarding 阶段一致
// Name 方案名。建档时用户看到的就是这个,要能一眼分辨
// (「幼猫标准照护」/「幼猫省心版」)
Name string `gorm:"size:64" json:"name"`
// Summary 一句话摘要,选择列表里展示
Summary string `gorm:"size:255" json:"summary"`
Sort int `json:"sort"`
Enabled bool `gorm:"default:true" json:"enabled"`
Items []CareTemplateItem `gorm:"foreignKey:TemplateID" json:"items"`
}
// CareTemplateItem 模板明细,kind 区分类;不同类只用到部分字段
// CareTemplateItem 明细,kind 区分类;不同类只用到部分字段
type CareTemplateItem struct {
Base
TemplateID string `gorm:"size:24;index" json:"template_id"`
Kind string `gorm:"size:16;index" json:"kind"` // task | plan | reminder
TemplateID string `gorm:"size:24;index" json:"template_id"`
Kind string `gorm:"size:16;index" json:"kind"` // plan | reminder
Sort int `json:"sort"`
Title string `gorm:"size:128" json:"title"`
Description string `gorm:"size:512" json:"description"`
// task 专用
Priority string `gorm:"size:16" json:"priority"` // 重要 / 普通(空)
SheetType string `gorm:"size:32" json:"sheet_type"` // 关联记录弹层:weight/poop/food/vaccine...
// plan 专用
Day int `json:"day"` // 第 N 天(0=今天
DayLabel string `gorm:"size:32" json:"day_label"` // 展示标签
Priority string `gorm:"size:16" json:"priority"` // 重要 / 普通(空
SheetType string `gorm:"size:32" json:"sheet_type"` // 关联记录类型 codeweight/poop/bath...
// Repeat 列名用 repeat_rulerepeat 是 MySQL 保留字,GORM 虽然会加反引号,
// 但手写 SQL 排查时很容易踩(和 group_key 同样的理由)
Repeat string `gorm:"column:repeat_rule;size:16" json:"repeat"` // once | daily | weekly | monthly
Day int `json:"day"` // once:从套用日起第 N 天(0=当天)
// DayLabel 留着:后台的 AI 生成模板那条链路在产出它。月度计划里日期是绝对的
// 不需要「第 N 天」标签,但删它要连着改 AI 解析,不在这次范围内
DayLabel string `gorm:"size:32" json:"day_label"`
// Weekdays weekly 用:逗号分隔的 1-7(1=周一)。
// 存字符串不另开表——一条模板明细最多 7 个数字,开表换来的只是查询灵活性,
// 而这份数据从来只整条读出来展开
Weekdays string `gorm:"size:16" json:"weekdays"`
MonthDay int `json:"month_day"` // monthly 用:几号(1-31),超出当月天数按月末算
// reminder 专用
ReminderType string `gorm:"size:32" json:"reminder_type"` // vaccine/deworm/weight/monthlyReport
+3 -1
View File
@@ -37,6 +37,8 @@ type PlanTask struct {
Title string `gorm:"size:128" json:"title"`
Description string `gorm:"size:512" json:"description"`
SheetType string `gorm:"size:32" json:"sheet_type"`
Done bool `json:"done"`
// Priority 从 DailyTask 合并过来的:首页那份「今天要做的事」要能标出哪条重要
Priority string `gorm:"size:16" json:"priority"`
Done bool `json:"done"`
Date string `gorm:"-" json:"date"` // 计算字段:计划开始日 + Day
}