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:
@@ -147,98 +147,51 @@ func (s *Service) Onboarding(userID string, in PetInput) (*model.Pet, error) {
|
||||
}
|
||||
|
||||
// seedPetDefaults 为新宠按「物种 × 阶段」养护模板生成今日任务、提醒、30 天计划
|
||||
// seedPetDefaults 建档时只做一件事:建「必须有」的到期提醒(疫苗、驱虫)。
|
||||
//
|
||||
// 原来它会自动把整套模板铺开成今日任务 + 30 天计划。改成不自动铺了——
|
||||
// 用户在建档最后一步自己挑方案,或者选「先空着我自己排」。
|
||||
// 自动铺的问题是:用户还没看懂这个 App 是干什么的,首页已经躺着十几条
|
||||
// 他没答应过的任务,第一反应是「这些是什么」而不是「我该做什么」。
|
||||
//
|
||||
// 疫苗驱虫这两条例外,因为漏针是真会出事的,而且它们是「到期提醒」不是
|
||||
// 「今天要做的事」——不占计划页的格子,只在到期时冒出来。
|
||||
func (s *Service) seedPetDefaults(tx *gorm.DB, pet *model.Pet) error {
|
||||
today := time.Now()
|
||||
species := SpeciesOf(pet.Type)
|
||||
items := s.careTemplateItems(species, pet.Stage)
|
||||
// 模板是后台配的,代码里没有兜底。配空了新用户会拿到一个什么都没有的
|
||||
// 首页,而且不报错——所以这里必须留下痕迹,否则没人能发现。
|
||||
if len(items) == 0 {
|
||||
log.Printf("care: 养护模板为空 species=%s stage=%s,新宠 %s 不会生成任务/计划/提醒,请到后台配置",
|
||||
log.Printf("care: 养护方案为空 species=%s stage=%s,新宠 %s 建档后没有可选方案,请到后台配置",
|
||||
species, pet.Stage, pet.ID)
|
||||
}
|
||||
|
||||
// 刚到家不到 30 天,把安置期那套的任务和计划叠加上来。
|
||||
// 「刚到家」和「幼年/成年/老年」是两个正交的维度,原来挤在一个四选一里,
|
||||
// 结果刚接回家的成年猫只能二选一,要么丢应激期照护、要么丢年龄段照护。
|
||||
if isNewHome(pet.ArrivedAt, today) && pet.Stage != StageNewHome {
|
||||
seen := map[string]bool{}
|
||||
for _, it := range items {
|
||||
seen[it.Kind+"|"+it.Title] = true
|
||||
}
|
||||
for _, it := range s.careTemplateItems(species, StageNewHome) {
|
||||
if it.Kind != model.CareKindTask && it.Kind != model.CareKindPlan {
|
||||
continue
|
||||
}
|
||||
// 两套模板会撞题(比如都有「清理猫砂盆并观察」),撞了就跳过,
|
||||
// 否则用户第一天会看到两条一模一样的任务
|
||||
if seen[it.Kind+"|"+it.Title] {
|
||||
continue
|
||||
}
|
||||
seen[it.Kind+"|"+it.Title] = true
|
||||
items = append(items, it)
|
||||
}
|
||||
}
|
||||
|
||||
goals := petGoals(pet)
|
||||
|
||||
var tasks []model.DailyTask
|
||||
now := time.Now()
|
||||
var reminders []model.Reminder
|
||||
var planTasks []model.PlanTask
|
||||
seen := map[string]bool{}
|
||||
for _, it := range items {
|
||||
switch it.Kind {
|
||||
case model.CareKindTask:
|
||||
tasks = append(tasks, model.DailyTask{
|
||||
PetID: pet.ID, UserID: pet.UserID, TaskDate: today,
|
||||
Title: it.Title, Description: it.Description, Priority: it.Priority, SheetType: it.SheetType,
|
||||
})
|
||||
case model.CareKindPlan:
|
||||
planTasks = append(planTasks, model.PlanTask{
|
||||
Day: it.Day, DayLabel: it.DayLabel, Title: it.Title, Description: it.Description, SheetType: it.SheetType,
|
||||
})
|
||||
case model.CareKindReminder:
|
||||
// 建档时勾的目标决定要不要建这条提醒。原来这几个勾选框收上来只是
|
||||
// 存进 pets.goals,一处都没用过,等于让用户点了个寂寞。
|
||||
if !goalWantsReminder(goals, it.ReminderType) {
|
||||
continue
|
||||
}
|
||||
r := model.Reminder{
|
||||
PetID: pet.ID, UserID: pet.UserID, Type: it.ReminderType,
|
||||
Title: reminderTitle(it, pet), Frequency: it.Frequency,
|
||||
}
|
||||
if days := dueDaysFor(it, pet, today); days > 0 {
|
||||
due := today.AddDate(0, 0, days)
|
||||
r.NextDueDate = &due
|
||||
}
|
||||
reminders = append(reminders, r)
|
||||
if it.Kind != model.CareKindReminder || seen[it.Title] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if wantsGoal(goals, "开销") {
|
||||
tasks = append(tasks, model.DailyTask{
|
||||
PetID: pet.ID, UserID: pet.UserID, TaskDate: today,
|
||||
Title: "记一笔养宠开销", Description: "粮、猫砂、医疗都记上,月底能看到花在哪", SheetType: "cost",
|
||||
})
|
||||
}
|
||||
|
||||
if len(tasks) > 0 {
|
||||
if err := tx.Create(&tasks).Error; err != nil {
|
||||
return err
|
||||
if !goalWantsReminder(goals, it.ReminderType) {
|
||||
continue
|
||||
}
|
||||
seen[it.Title] = true
|
||||
r := model.Reminder{
|
||||
PetID: pet.ID, UserID: pet.UserID, Type: it.ReminderType,
|
||||
Title: it.Title, Frequency: it.Frequency,
|
||||
}
|
||||
if it.OffsetDays > 0 {
|
||||
due := now.AddDate(0, 0, dueDaysFor(it, pet, now))
|
||||
r.NextDueDate = &due
|
||||
}
|
||||
reminders = append(reminders, r)
|
||||
}
|
||||
if len(reminders) > 0 {
|
||||
if err := tx.Create(&reminders).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
start := today
|
||||
end := today.AddDate(0, 0, 30)
|
||||
plan := model.Plan{
|
||||
PetID: pet.ID, UserID: pet.UserID, Kind: model.PlanThirtyDay, Stage: pet.Stage,
|
||||
StartDate: &start, EndDate: &end, CompletionPct: 0, Status: "active",
|
||||
Tasks: planTasks,
|
||||
}
|
||||
return tx.Create(&plan).Error
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- 建档默认值的几个判断 ----
|
||||
|
||||
Reference in New Issue
Block a user