package service import ( "encoding/json" "errors" "fmt" "strings" "gorm.io/gorm" "github.com/sundynix/pets-be/internal/ai" "github.com/sundynix/pets-be/internal/model" ) // CareSpecies / CareStages 支持的物种与阶段(阶段字符串与 onboarding 一致) var CareSpecies = []string{model.SpeciesCat, model.SpeciesDog} var CareStages = []string{"刚到家 0-30 天", "幼年期", "成年期", "老年期"} // SpeciesOf 由宠物类型(猫猫/狗狗)映射到模板物种键 func SpeciesOf(petType string) string { if strings.Contains(petType, "狗") { return model.SpeciesDog } return model.SpeciesCat } func validSpecies(s string) bool { return s == model.SpeciesCat || s == model.SpeciesDog } func validStage(s string) bool { for _, x := range CareStages { if x == s { return true } } return false } // careTemplateItems 取某组合的模板明细:DB 优先,无则回退内置默认 func (s *Service) careTemplateItems(species, stage string) []model.CareTemplateItem { var tpl model.CareTemplate err := s.db. Preload("Items", func(db *gorm.DB) *gorm.DB { return db.Order("kind, sort, id") }). Where("species = ? AND stage = ?", species, stage).First(&tpl).Error if err == nil && len(tpl.Items) > 0 { return tpl.Items } return builtinCareItems(species, stage) } // GetCareTemplate 后台读取:返回某组合的明细(DB 优先,无则内置默认) func (s *Service) GetCareTemplate(species, stage string) (map[string]any, error) { if !validSpecies(species) || !validStage(stage) { return nil, errors.New("invalid species or stage") } return map[string]any{ "species": species, "stage": stage, "items": s.careTemplateItems(species, stage), }, nil } // SaveCareTemplate 后台保存:整表替换该组合的明细 func (s *Service) SaveCareTemplate(species, stage string, items []model.CareTemplateItem) error { if !validSpecies(species) || !validStage(stage) { return errors.New("invalid species or stage") } return s.db.Transaction(func(tx *gorm.DB) error { var tpl model.CareTemplate err := tx.Where("species = ? AND stage = ?", species, stage).First(&tpl).Error if errors.Is(err, gorm.ErrRecordNotFound) { tpl = model.CareTemplate{Species: species, Stage: stage} if err := tx.Create(&tpl).Error; err != nil { return err } } else if err != nil { return err } if err := tx.Where("template_id = ?", tpl.ID).Delete(&model.CareTemplateItem{}).Error; err != nil { return err } clean := make([]model.CareTemplateItem, 0, len(items)) for i := range items { it := items[i] if strings.TrimSpace(it.Title) == "" { continue } it.ID = "" it.TemplateID = tpl.ID it.Sort = i clean = append(clean, it) } if len(clean) > 0 { if err := tx.Create(&clean).Error; err != nil { return err } } return nil }) } // GenerateCareTemplate 后台「AI 生成草稿」:不落库,仅返回建议明细。 // AI 未开启或失败时回退内置默认,保证总有可用草稿。 func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTemplateItem, error) { if !validSpecies(species) || !validStage(stage) { return nil, errors.New("invalid species or stage") } if !s.ai.Enabled() { return builtinCareItems(species, stage), nil } spName := "猫" if species == model.SpeciesDog { spName = "狗" } system := aiSafetyPrompt + ` 你现在为后台生成一套「养护模板」,用于新宠建档时自动生成任务/计划/提醒。 只输出 JSON,结构如下(不要多余文字): { "tasks": [{"title":"","description":"","priority":"重要或空","sheet_type":"weight|poop|food|symptom|vaccine|medicine 或空"}], "plan": [{"day":0,"day_label":"今天","title":"","description":"","sheet_type":"weight|vaccine|medicine|report|taskDetail 或空"}], "reminders":[{"reminder_type":"vaccine|deworm|weight|monthlyReport","title":"","offset_days":18,"frequency":"到期用天数则填0并写频率,如每周二五"}] } 要求:tasks 3 条、plan 4 条、reminders 3-4 条;贴合该物种与阶段的真实照护节奏(如成年比幼年疫苗/驱虫间隔更长,狗驱虫比猫更频繁)。` user := fmt.Sprintf("为【%s · %s】生成一套养护模板。", spName, stage) out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: user}}, ai.Options{JSON: true, Temperature: -1}) if err != nil { return builtinCareItems(species, stage), nil } items, ok := parseCareItems(out) if !ok || len(items) == 0 { return builtinCareItems(species, stage), nil } return items, nil } // parseCareItems 解析 AI 返回的分组 JSON → 扁平明细 func parseCareItems(out string) ([]model.CareTemplateItem, bool) { var raw struct { Tasks []struct { Title, Description, Priority, SheetType string } `json:"tasks"` Plan []struct { Day int DayLabel, Title, Description, SheetType string } `json:"plan"` Reminders []struct { ReminderType, Title, Frequency string OffsetDays int } `json:"reminders"` } if err := json.Unmarshal([]byte(extractJSON(out)), &raw); err != nil { return nil, false } var items []model.CareTemplateItem for _, t := range raw.Tasks { if t.Title == "" { continue } items = append(items, careTask(t.Title, t.Description, t.Priority, t.SheetType)) } for _, p := range raw.Plan { if p.Title == "" { continue } items = append(items, carePlan(p.Day, p.DayLabel, p.Title, p.Description, p.SheetType)) } for _, r := range raw.Reminders { if r.Title == "" { continue } items = append(items, careReminder(r.ReminderType, r.Title, r.OffsetDays, r.Frequency)) } return items, len(items) > 0 } // SeedCareTemplates 启动幂等 seed:库中无任何模板时写入 8 套内置默认 func (s *Service) SeedCareTemplates() error { var count int64 if err := s.db.Model(&model.CareTemplate{}).Count(&count).Error; err != nil { return err } if count > 0 { return nil } for _, sp := range CareSpecies { for _, stage := range CareStages { tpl := model.CareTemplate{Species: sp, Stage: stage, Items: builtinCareItems(sp, stage)} if err := s.db.Create(&tpl).Error; err != nil { return err } } } return nil } // ---- 明细构造小工具 ---- func careTask(title, desc, priority, sheet string) model.CareTemplateItem { return model.CareTemplateItem{Kind: model.CareKindTask, Title: title, Description: desc, Priority: priority, SheetType: sheet} } func carePlan(day int, label, title, desc, sheet string) model.CareTemplateItem { return model.CareTemplateItem{Kind: model.CareKindPlan, Day: day, DayLabel: label, Title: title, Description: desc, SheetType: sheet} } func careReminder(rtype, title string, offset int, freq string) model.CareTemplateItem { return model.CareTemplateItem{Kind: model.CareKindReminder, ReminderType: rtype, Title: title, OffsetDays: offset, Frequency: freq} } // builtinCareItems 内置默认模板(8 套),既作开箱数据也作兜底 func builtinCareItems(species, stage string) []model.CareTemplateItem { isDog := species == model.SpeciesDog dewormAdult := 90 if isDog { dewormAdult = 60 // 狗驱虫间隔通常比猫短 } switch stage { case "刚到家 0-30 天": return []model.CareTemplateItem{ careTask("记录一次体重", "到家后每周记录,建立基线", "重要", "weight"), careTask("观察饮水与排便", "换环境应激常见软便,连续观察几天", "", "poop"), careTask("布置安静休息角", "固定饮食、厕所与休息区,减少应激", "", ""), carePlan(0, "今天", "安顿与环境适应", "固定饮食、厕所、休息区位置", "taskDetail"), carePlan(3, "第3天", "观察排便与食欲", "记录应激缓解情况", "poop"), carePlan(7, "第7天", "第一次体重记录", "作为后续趋势基线", "weight"), carePlan(14, "第14天", "预约首次体检/疫苗", "确认医院与时间", "vaccine"), careReminder("vaccine", "首次疫苗接种", 14, ""), careReminder("deworm", "首次体内外驱虫", 7, ""), careReminder("weight", "体重记录", 0, "每周一次"), careReminder("monthlyReport", "月度报告", 0, "每月 1 日"), } case "幼年期": vaccineTitle := "第 2 针疫苗" if isDog { vaccineTitle = "下一针联苗" } return []model.CareTemplateItem{ careTask("记录一次体重", "幼年期每周至少记录 2 次", "重要", "weight"), careTask("观察饮水与排便", "换粮、应激都可能影响排便状态", "", "poop"), careTask("检查疫苗预约", "按免疫程序别漏针", "重要", "vaccine"), carePlan(0, "今天", "观察排便状态", "记录颜色、形态、次数", "taskDetail"), carePlan(1, "明天", "检查疫苗预约", "确认下一针医院与时间", "vaccine"), carePlan(7, "第7天", "体重趋势检查", "每周称重,观察稳定增长", "weight"), carePlan(14, "第14天", "复盘饮食与便便", "换粮记录与异常一起看", "taskDetail"), careReminder("vaccine", vaccineTitle, 18, ""), careReminder("deworm", "体内外驱虫", 12, ""), careReminder("weight", "体重记录", 0, "每周二、周五"), careReminder("monthlyReport", "月度报告", 0, "每月 1 日"), } case "老年期": return []model.CareTemplateItem{ careTask("记录一次体重", "老年期体重波动要重视", "重要", "weight"), careTask("观察精神与食欲", "明显变差要尽快就医", "重要", "symptom"), careTask("关注饮水量", "多饮多尿可能是慢病信号", "", ""), carePlan(0, "今天", "老年健康自查", "精神、食欲、饮水一起看", "taskDetail"), carePlan(7, "第7天", "体重与饮水记录", "留意慢病早期信号", "weight"), carePlan(14, "第14天", "用药与体检回顾", "慢病管理与用药记录", "medicine"), carePlan(30, "第30天", "生成月度报告", "整理给医生看的记录", "report"), careReminder("vaccine", "年度疫苗加强", 30, ""), careReminder("deworm", "体内外驱虫", dewormAdult, ""), careReminder("weight", "体重记录", 0, "每两周一次"), careReminder("monthlyReport", "月度报告 / 建议体检", 0, "每月 1 日"), } default: // 成年期 return []model.CareTemplateItem{ careTask("记录一次体重", "成年期每月称重一次即可", "", "weight"), careTask("观察饮水与排便", "发现软便可连续观察几天", "", "poop"), careTask("梳毛与口腔检查", "关注牙结石与皮肤状态", "", ""), carePlan(0, "今天", "全面健康自查", "体重、精神、食欲、被毛过一遍", "taskDetail"), carePlan(7, "第7天", "体重趋势检查", "对比上月,观察是否稳定", "weight"), carePlan(14, "第14天", "驱虫记录复盘", "确认体内外驱虫是否到期", "medicine"), carePlan(30, "第30天", "生成月度报告", "回顾本月记录,导出成长卡片", "report"), careReminder("vaccine", "年度疫苗加强", 30, ""), careReminder("deworm", "体内外驱虫", dewormAdult, ""), careReminder("weight", "体重记录", 0, "每月一次"), careReminder("monthlyReport", "月度报告", 0, "每月 1 日"), } } }