@@ -62,6 +62,18 @@ func monthsSince(t time.Time) int {
return m
}
// NormalizeStage 定下最终使用的年龄阶段。
// 用户手选优先(他可能比生日更清楚,比如领养来的估算年龄),
// 但只认三个年龄阶段;传了「刚到家」或空值时,一律按生日推断。
func NormalizeStage ( species , picked string , birthday * time . Time ) string {
for _ , s := range AgeStages {
if picked == s {
return picked
}
}
return StageFromBirthday ( species , birthday )
}
// SpeciesOf 由宠物类型(猫猫/狗狗)映射到模板物种键
func SpeciesOf ( petType string ) string {
if strings . Contains ( petType , "狗" ) {
@@ -80,16 +92,17 @@ func validStage(s string) bool {
return false
}
// careTemplateItems 取某组合的模板明细: DB 优先,无则回退内置默认
// careTemplateItems 取某组合的模板明细。模板是后台配的内容数据,代码里不再留
// 一套内置默认——没配就是没配,这里返回空,由调用方决定怎么处理(见 seedPetDefaults)。
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
if err ! = nil {
return nil
}
return builtinCareItems ( species , stage )
return tpl . Items
}
// GetCareTemplate 后台读取:返回某组合的明细(DB 优先,无则内置默认)
@@ -150,7 +163,7 @@ func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTempl
return nil , errors . New ( "invalid species or stage" )
}
if ! s . ai . Enabled ( ) {
return builtinCareItems ( species , stage ) , nil
return nil , errors . New ( "AI 未开启,无法生成草稿;请手动添加明细" )
}
spName := "猫"
if species == model . SpeciesDog {
@@ -170,11 +183,11 @@ func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTempl
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
return nil , fmt . Errorf ( "AI 生成失败:%w" , err )
}
items , ok := parseCareItems ( out )
if ! ok || len ( items ) == 0 {
return builtinCareItems ( species , stage ) , nil
return nil , errors . New ( "AI 返回的内容解析不出可用明细,请重试或手动添加" )
}
return items , nil
}
@@ -219,42 +232,7 @@ func parseCareItems(out string) ([]model.CareTemplateItem, bool) {
return items , len ( items ) > 0
}
// SeedCareTemplates 启动幂等 seed:库中无任何模板时写入 8 套内置默认
// 按组合补齐:只创建缺失的那几套,已有的(可能被管理员改过)一律不动。
// 想把内置默认推到已有组合上,走后台的「恢复默认」——那是显式操作,不该由启动时偷偷覆盖。
func ( s * Service ) SeedCareTemplates ( ) error {
for _ , sp := range CareSpecies {
for _ , stage := range CareStages {
var count int64
if err := s . db . Model ( & model . CareTemplate { } ) .
Where ( "species = ? AND stage = ?" , sp , stage ) . Count ( & count ) . Error ; err != nil {
return err
}
if count > 0 {
continue
}
tpl := model . CareTemplate { Species : sp , Stage : stage , Items : builtinCareItems ( sp , stage ) }
if err := s . db . Create ( & tpl ) . Error ; err != nil {
return err
}
}
}
return nil
}
// ResetCareTemplate 把某个组合恢复成内置默认(后台按钮触发)
func ( s * Service ) ResetCareTemplate ( species , stage string ) ( [ ] model . CareTemplateItem , error ) {
if ! validSpecies ( species ) || ! validStage ( stage ) {
return nil , errors . New ( "物种或阶段不合法" )
}
items := builtinCareItems ( species , stage )
if err := s . SaveCareTemplate ( species , stage , items ) ; err != nil {
return nil , err
}
return s . careTemplateItems ( species , stage ) , nil
}
// ---- 明细构造小工具 ----
// ---- 明细构造小工具(供 AI 草稿解析用)----
func careTask ( title , desc , priority , sheet string ) model . CareTemplateItem {
return model . CareTemplateItem { Kind : model . CareKindTask , Title : title , Description : desc , Priority : priority , SheetType : sheet }
@@ -265,166 +243,3 @@ func carePlan(day int, label, title, desc, sheet string) model.CareTemplateItem
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 内置默认模板(猫/狗 × 4 阶段,共 8 套)。
// 兽医常识向:狗每个阶段都含遛狗/牵引,猫含猫砂盆/梳毛/饮水;疫苗驱虫节奏按物种阶段区分。
// builtinCareItems 内置默认养护模板(猫/狗 × 4 组,共 8 套)。
//
// 内容依据主流小动物临床共识(WSAVA 疫苗指南、ESCCAP/CAPC 驱虫建议、AAHA
// 生命阶段指南)与国内实际情况整理:
// - 猫三联(FVRCP) 幼猫共 3 针,8-9 周龄起,每针间隔 3-4 周,末针不早于 16 周龄
// - 犬联苗幼犬 2-3 针,6-8 周龄起,间隔 3-4 周,末针不早于 16 周龄;
// 狂犬 3 月龄首针、其后每年一次(国内为法定要求)
// - 体内驱虫:6 月龄前每月一次,之后每 3 个月一次;体外每月一次(随环境调整)
// - 猫 12 月龄成年、7 岁进入老年;犬 12 月龄成年(大型犬 18-24 月)、7 岁进入老年
//
// 「刚到家 0-30 天」不是年龄阶段,而是叠加包:建档时按年龄阶段取一套,
// 若到家不足 30 天,再把这一套的任务和计划叠加上去(见 seedPetDefaults)。
func builtinCareItems ( species , stage string ) [ ] model . CareTemplateItem {
if species == model . SpeciesDog {
return builtinDog ( stage )
}
return builtinCat ( stage )
}
func builtinCat ( stage string ) [ ] model . CareTemplateItem {
switch stage {
case StageNewHome :
return [ ] model . CareTemplateItem {
careTask ( "记录一次体重" , "到家当天称一次做基线,之后每周一次看趋势" , "重要" , "weight" ) ,
careTask ( "保持原来的猫粮" , "换环境已经够折腾了,粮先别换;要换也要 7 天过渡" , "重要" , "food" ) ,
careTask ( "清理猫砂盆并观察" , "应激性软便很常见,超过 3 天不好转要就医" , "" , "poop" ) ,
careTask ( "陪玩 15 分钟" , "用逗猫棒隔着距离玩,头两天别强行抱它" , "" , "" ) ,
carePlan ( 0 , "今天" , "布置一个能躲的角落" , "猫砂盆、食盆、水碗分开放;纸箱或猫窝给它安全感" , "" ) ,
carePlan ( 1 , "第2天" , "先只开放一个房间" , "空间由小到大,全屋乱窜会加重应激" , "" ) ,
carePlan ( 3 , "第3天" , "观察食欲与排便" , "肯吃、肯拉、肯出来走动,就是适应良好" , "poop" ) ,
carePlan ( 7 , "第7天" , "第一次正式称重" , "作为之后所有体重趋势的基线" , "weight" ) ,
carePlan ( 14 , "第14天" , "预约首次体检与驱虫" , "带一份新鲜粪便去,可以顺便查体内寄生虫" , "vaccine" ) ,
carePlan ( 30 , "第30天" , "确认免疫计划已排好" , "把三针联苗的时间点定下来,别拖到成年" , "vaccine" ) ,
}
case StageJuvenile :
return [ ] model . CareTemplateItem {
careTask ( "记录一次体重" , "幼猫每周称 1-2 次,稳定增重比单次数字重要" , "重要" , "weight" ) ,
careTask ( "定时定量喂食" , "3-6 月龄每天 3-4 餐,6 月龄后 2-3 餐,自由采食容易过胖" , "" , "food" ) ,
careTask ( "检查免疫进度" , "猫三联共 3 针,每针间隔 3-4 周,末针不早于 16 周龄" , "重要" , "vaccine" ) ,
careTask ( "陪玩与社会化" , "每天 2 次各 10-15 分钟;多摸爪子和耳朵,长大好剪指甲" , "" , "" ) ,
carePlan ( 0 , "今天" , "对照月龄核免疫和驱虫" , "算清楚还差几针、下次驱虫什么时候" , "vaccine" ) ,
carePlan ( 3 , "第3天" , "换粮观察" , "换粮按 7 天过渡,每天替换约 1/7,软便就放慢" , "food" ) ,
carePlan ( 7 , "第7天" , "体重趋势检查" , "掉秤或长期不增重要查球虫、寄生虫" , "weight" ) ,
carePlan ( 14 , "第14天" , "体内外驱虫" , "6 月龄前体内每月一次,体外按环境每月一次" , "medicine" ) ,
carePlan ( 21 , "第21天" , "养成梳毛和剪指甲的习惯" , "从小做,成年后不抗拒;顺便摸一遍有没有硬块" , "" ) ,
carePlan ( 30 , "第30天" , "月度复盘 + 绝育咨询" , "6-8 月龄可以和医生聊绝育时间" , "report" ) ,
careReminder ( "vaccine" , "下一针猫三联" , 21 , "幼猫共 3 针,间隔 3-4 周" ) ,
careReminder ( "deworm" , "体内外驱虫" , 30 , "6 月龄前每月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每周 1-2 次" ) ,
careReminder ( "monthlyReport" , "月度报告" , 0 , "每月 1 日" ) ,
}
case StageSenior :
return [ ] model . CareTemplateItem {
careTask ( "记录一次体重" , "老年猫掉秤常是甲亢或慢性肾病的第一个信号" , "重要" , "weight" ) ,
careTask ( "关注饮水量与尿量" , "又喝又尿是慢性肾病的典型表现,比食欲变化更早出现" , "重要" , "symptom" ) ,
careTask ( "观察进食与口腔" , "牙口疼会让它挑软食甚至不吃,别只当成挑嘴" , "" , "food" ) ,
carePlan ( 0 , "今天" , "老年状态整体自查" , "精神、食欲、饮水、体重、被毛过一遍" , "" ) ,
carePlan ( 7 , "第7天" , "体重与饮水一起记" , "两条曲线放在一起看,慢病能早发现几个月" , "weight" ) ,
carePlan ( 14 , "第14天" , "整理用药与体检报告" , "长期药别断,检查单留好方便对比" , "medicine" ) ,
carePlan ( 21 , "第21天" , "关节与活动观察" , "跳不上高处、理毛变少、猫砂盆边缘迈不过去都要留意" , "symptom" ) ,
carePlan ( 30 , "第30天" , "安排半年一次体检" , "老年猫建议每 6-12 个月一次,含血检和尿检" , "report" ) ,
careReminder ( "vaccine" , "年度疫苗加强" , 0 , "每年一次" ) ,
careReminder ( "deworm" , "体内驱虫" , 90 , "每 3 个月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每两周一次" ) ,
careReminder ( "monthlyReport" , "月度报告 / 半年体检" , 0 , "每月 1 日" ) ,
}
default : // 成年期
return [ ] model . CareTemplateItem {
careTask ( "清理猫砂盆并观察" , "公猫尿闭是急症,24 小时排不出尿必须马上就医" , "重要" , "poop" ) ,
careTask ( "检查饮水情况" , "吃干粮更要多喝水;流动水或多放几个水碗都管用" , "" , "symptom" ) ,
careTask ( "梳毛" , "每周 2-3 次,减少毛球,也是摸有没有硬块的机会" , "" , "" ) ,
carePlan ( 0 , "今天" , "全面健康自查" , "体重、精神、食欲、饮水、被毛、口腔过一遍" , "" ) ,
carePlan ( 7 , "第7天" , "体重记录" , "成年猫每月一次即可;超重是绝大多数慢病的起点" , "weight" ) ,
carePlan ( 14 , "第14天" , "口腔检查" , "牙龈红肿、口臭明显,该洗牙了" , "symptom" ) ,
carePlan ( 21 , "第21天" , "体外驱虫" , "只在室内也可能被带进来的跳蚤找上" , "medicine" ) ,
carePlan ( 30 , "第30天" , "生成月度报告" , "回顾本月记录,导出成长卡片" , "report" ) ,
careReminder ( "vaccine" , "年度疫苗加强" , 0 , "每年一次" ) ,
careReminder ( "deworm" , "体内驱虫" , 90 , "每 3 个月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每月一次" ) ,
careReminder ( "monthlyReport" , "月度报告" , 0 , "每月 1 日" ) ,
}
}
}
func builtinDog ( stage string ) [ ] model . CareTemplateItem {
switch stage {
case StageNewHome :
return [ ] model . CareTemplateItem {
careTask ( "记录一次体重" , "到家当天称一次做基线,之后每周一次" , "重要" , "weight" ) ,
careTask ( "保持原来的狗粮" , "换粮和换环境别撞在一起;要换按 7 天过渡" , "重要" , "food" ) ,
careTask ( "定点排便训练" , "固定位置、成功就立刻表扬;事后惩罚只会让它躲着拉" , "" , "poop" ) ,
careTask ( "先在室内熟悉环境" , "疫苗没打全之前不要去公共遛狗区和宠物店" , "" , "" ) ,
carePlan ( 0 , "今天" , "安顿:食盆、水碗、窝、厕所" , "位置固定下来,别频繁挪动" , "" ) ,
carePlan ( 1 , "第2天" , "适应牵引绳" , "先在家戴着走两圈,出门前就该习惯" , "" ) ,
carePlan ( 3 , "第3天" , "观察进食与排便" , "应激期软便常见,精神差、带血要就医" , "poop" ) ,
carePlan ( 7 , "第7天" , "第一次正式称重" , "作为之后所有体重趋势的基线" , "weight" ) ,
carePlan ( 14 , "第14天" , "预约疫苗与体检" , "确认联苗针次,3 月龄以上要安排狂犬" , "vaccine" ) ,
carePlan ( 30 , "第30天" , "免疫打全后开始户外散步" , "从安静路段开始,逐步接触人、车、其它狗" , "" ) ,
}
case StageJuvenile :
return [ ] model . CareTemplateItem {
careTask ( "牵引散步与社会化" , "3-16 周是社会化关键期;疫苗未全时可抱着或在自家院子接触外界" , "重要" , "" ) ,
careTask ( "记录一次体重" , "幼犬长得快,每周称 1-2 次" , "重要" , "weight" ) ,
careTask ( "定时定量喂食" , "3-6 月龄每天 3 餐,6 月龄后 2 餐" , "" , "food" ) ,
careTask ( "基础训练 10 分钟" , "坐下、召回、不咬手;短时高频比一次练很久有效" , "" , "" ) ,
carePlan ( 0 , "今天" , "核对免疫进度" , "联苗 2-3 针间隔 3-4 周,3 月龄安排狂犬" , "vaccine" ) ,
carePlan ( 3 , "第3天" , "换粮观察" , "7 天过渡,软便就放慢速度" , "food" ) ,
carePlan ( 7 , "第7天" , "体重趋势检查" , "增长停滞要查寄生虫和喂食量" , "weight" ) ,
carePlan ( 14 , "第14天" , "体内外驱虫" , "6 月龄前体内每月一次,体外每月一次" , "medicine" ) ,
carePlan ( 21 , "第21天" , "巩固定点排便与独处训练" , "从 5 分钟开始拉长独处时间,预防分离焦虑" , "" ) ,
carePlan ( 30 , "第30天" , "月度复盘 + 绝育咨询" , "小型犬 6-9 月龄,大型犬建议 12-18 月龄再做,等骨骼长好" , "report" ) ,
careReminder ( "vaccine" , "下一针联苗 / 狂犬" , 21 , "联苗间隔 3-4 周,3 月龄起狂犬" ) ,
careReminder ( "deworm" , "体内外驱虫" , 30 , "6 月龄前每月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每周 1-2 次" ) ,
careReminder ( "monthlyReport" , "月度报告" , 0 , "每月 1 日" ) ,
}
case StageSenior :
return [ ] model . CareTemplateItem {
careTask ( "温和遛狗" , "缩短单次时长、增加次数;避免跳跃和频繁爬楼梯" , "重要" , "" ) ,
careTask ( "记录一次体重" , "骤增骤减都要重视,老年犬掉秤要查甲状腺和肿瘤" , "重要" , "weight" ) ,
careTask ( "观察关节与精神" , "起身困难、后腿打滑、不愿上车,都是关节问题的早期表现" , "" , "symptom" ) ,
carePlan ( 0 , "今天" , "老年状态整体自查" , "精神、食欲、饮水、关节活动一起看" , "" ) ,
carePlan ( 7 , "第7天" , "体重与饮水一起记" , "多饮多尿要查肾和血糖" , "weight" ) ,
carePlan ( 14 , "第14天" , "整理用药与体检报告" , "长期药别断,检查单留好方便对比" , "medicine" ) ,
carePlan ( 21 , "第21天" , "口腔与体表触诊" , "牙结石、乳腺和体表肿物,摸一遍心里有数" , "symptom" ) ,
carePlan ( 30 , "第30天" , "安排半年一次体检" , "老年犬建议每 6 个月一次,含血检和胸腹部检查" , "report" ) ,
careReminder ( "vaccine" , "年度疫苗 + 狂犬加强" , 0 , "每年一次,狂犬为法定要求" ) ,
careReminder ( "deworm" , "体内驱虫" , 90 , "每 3 个月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每两周一次" ) ,
careReminder ( "monthlyReport" , "月度报告 / 半年体检" , 0 , "每月 1 日" ) ,
}
default : // 成年期
return [ ] model . CareTemplateItem {
careTask ( "遛狗" , "每天 2 次共 30-60 分钟;运动量不够是拆家和吠叫的头号原因" , "重要" , "" ) ,
careTask ( "刷牙" , "每周 2-3 次。3 岁以上的犬多数已有牙周问题" , "" , "" ) ,
careTask ( "记录一次体重" , "每月一次。摸得到肋骨但看不见,是标准体型" , "" , "weight" ) ,
carePlan ( 0 , "今天" , "全面健康自查" , "体重、精神、食欲、被毛、指甲、耳道过一遍" , "" ) ,
carePlan ( 7 , "第7天" , "体重记录" , "对比上月,稳定为宜" , "weight" ) ,
carePlan ( 14 , "第14天" , "体外驱虫" , "遛狗接触草地,蜱虫和跳蚤风险比猫高得多" , "medicine" ) ,
carePlan ( 21 , "第21天" , "指甲与耳道检查" , "指甲过长会影响步态;垂耳犬更容易耳道发炎" , "symptom" ) ,
carePlan ( 30 , "第30天" , "生成月度报告" , "回顾本月记录,导出成长卡片" , "report" ) ,
careReminder ( "vaccine" , "年度疫苗 + 狂犬加强" , 0 , "每年一次,狂犬为法定要求" ) ,
careReminder ( "deworm" , "体内驱虫" , 90 , "每 3 个月一次" ) ,
careReminder ( "weight" , "体重记录" , 0 , "每月一次" ) ,
careReminder ( "monthlyReport" , "月度报告" , 0 , "每月 1 日" ) ,
}
}
}
// NormalizeStage 定下最终使用的年龄阶段。
// 用户手选优先(他可能比生日更清楚,比如领养来的估算年龄),
// 但只认三个年龄阶段;传了「刚到家」或空值时,一律按生日推断。
func NormalizeStage ( species , picked string , birthday * time . Time ) string {
for _ , s := range AgeStages {
if picked == s {
return picked
}
}
return StageFromBirthday ( species , birthday )
}