feat: 生命阶段按猫狗真实生理规律细分,划分标准和判定用同一份数据
## 你问的「养了半年会不会自动切换」
原来的答案是「都不会,而且比这更糟」:
age「2个月」 建档时算好存进库,养半年后首页还显示 2个月
stage「幼年期」同上,一年后还是幼年期
养护方案 按 stage 挑的,所以永远给幼猫那套
NormalizeStage 只在建档那一次调用(handler/pet.go:51),之后就是两个死字符串。
现在:
age 改成计算字段(gorm:"-"),读取时现算。它没有手改需求,存下来只会过期。
填在 ownedPet 这个收口里,不是逐个函数补——那样漏一个就返回空年龄。
stage 保留存储(有正当的手改需求:领养的猫生日是估的,主人按体型判断更准),
但加了漂移检测 GET /pets/:id/stage-drift。
**系统负责发现,用户负责决定。** 不自动改的三个理由:
1. 用户可能手改过
2. 已经排好的这个月计划不该被推翻(套用是「往里加」不是「替换」)
3. 偷偷换会让用户莫名发现计划变了,找不到原因
## 猫狗不能共用阈值 —— 这是这次最实质的发现
原来一刀切「<12月幼年,>=7岁老年」,对猫勉强,对狗两头都错:
吉娃娃 8 个月已性成熟、可换成犬粮;大丹犬 8 个月还在长骨头,
这时喂成犬粮(钙磷比和热量密度都不同)会加重关节负担。
吉娃娃 7 岁还在成年期(8 岁才进中年,能活 13-16 年);
大丹犬 7 岁已是老年(平均寿命 7-10 年)。
所以狗的阶段判定要看体型。体型不能靠当前体重(幼犬体重说明不了成年后多大),
只能靠品种 —— breeds 表加 size_class,45 个犬种逐个标了。
实测:同样 10 个月大 → 吉娃娃青年期、柴犬幼年期、金毛幼年期、大丹犬幼年期;
同样 7 岁 → 吉娃娃成年期、大丹犬老年期。
## 阶段 3 段 → 5 段
幼年期 / 青年期 / 成年期 / 中年期 / 老年期。
标签做成物种中立(不分幼猫期/幼犬期)——物种本来是另一个字段,
分开会让 (species,stage) 的组合和方案配置都翻倍。
「刚到家 0-30 天」不进这五段:它和年龄正交(刚接回家的成年猫两套都需要),
代码里本来就是叠加处理的。
猫按 AAFP/AAHA 猫生命阶段指南那套,在成年里细分出中年。
狗五段的阈值按体型走四组,越大的犬种生长期越长、老得越早。
## 划分标准和判定用同一张表
StageRules 那张表既驱动判定,也直接吐给前端当依据展示
(GET /api/life-stages?species=dog&size=giant)。
不是「代码里判定 + 文档里写标准」——后者一定会漂:改了阈值忘了改文档,
用户看到的说明和实际行为对不上,比没有说明更糟。
建档第三步的阶段选择器现在会显示「0-18 月 · 巨型犬生长期最长,18 个月还在长;
这阶段最忌过度补钙」,还会标出「巨型犬(成年 >40kg)——阶段阈值按体型算」。
前端和后台的 STAGES 硬编码列表都删了,改成读后端。
## 10 套方案,内容按各阶段真实重点配
不是把同一份换个名字:
幼年期 疫苗序列 + 驱虫 + 社会化,大型犬还要控制生长速度
青年期 绝育窗口 + 换成年粮
成年期 维持期,把基线数据记下来
中年期 年度血检开始有意义,体重和牙结石重点看
老年期 体检半年一次;猫记饮水量(多饮多尿是慢性肾病最典型的早期表现)、
狗记精神食欲,都设成「重要」
老的 8 套(按 3 段配的)改名停用不删:万一新内容有问题还能翻回去看;
而且已套用出去的计划早就和模板断开了,停用不影响任何用户。
## 验证(预生产库实跑)
体型判定 10 个月 / 7 岁两组对照,四个体型都落在预期阶段
age '13个月' 现算,不入库
漂移检测 手改成幼年期后 drifted=true,连带给出新阶段的依据和重点
划分标准 猫 5 段、狗 4 个体型各 5 段,老年起点 7/8/10/11 岁
全链路 3 个月大丹犬建档 → 幼年期 → 0 任务 + 4 条必要提醒 →
挑「幼犬标准照护」→ 套用下月 104 条节点,
周四那天正确出现每周项(检查爪垫和耳朵)
迁移 45 个犬种体型回填;现有 5 只宠物阶段重算后无变化(原本都对)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,10 @@ func Seed(db *gorm.DB, cfg *config.Config) error {
|
||||
if err := seedRecordTypes(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return seedBreeds(db)
|
||||
if err := seedBreeds(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return seedCareTemplates(db)
|
||||
}
|
||||
|
||||
// seedBreeds 品种预置。同 record_types:只在表完全为空时铺一次。
|
||||
@@ -76,51 +79,51 @@ func seedBreeds(db *gorm.DB) error {
|
||||
{Species: model.SpeciesCat, Initial: "Y", Name: "英国短毛猫", Sort: 330},
|
||||
{Species: model.SpeciesCat, Initial: "Z", Name: "中华田园猫", Sort: 340},
|
||||
// 狗 46 种
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "阿拉斯加雪橇犬", Sort: 10},
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "阿富汗猎犬", Sort: 20},
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "澳洲牧羊犬", Sort: 30},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "比熊犬", Sort: 40},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "比格犬", Sort: 50},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "边境牧羊犬", Sort: 60},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "巴哥犬", Sort: 70},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "博美犬", Sort: 80},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "巴吉度猎犬", Sort: 90},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "北京犬", Sort: 100},
|
||||
{Species: model.SpeciesDog, Initial: "C", Name: "柴犬", Sort: 110},
|
||||
{Species: model.SpeciesDog, Initial: "C", Name: "川东猎犬", Sort: 120},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "德国牧羊犬", Sort: 130},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "杜宾犬", Sort: 140},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "大丹犬", Sort: 150},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "斗牛犬", Sort: 160},
|
||||
{Species: model.SpeciesDog, Initial: "F", Name: "法国斗牛犬", Sort: 170},
|
||||
{Species: model.SpeciesDog, Initial: "G", Name: "贵宾犬(泰迪)", Sort: 180},
|
||||
{Species: model.SpeciesDog, Initial: "G", Name: "古代牧羊犬", Sort: 190},
|
||||
{Species: model.SpeciesDog, Initial: "H", Name: "哈士奇", Sort: 200},
|
||||
{Species: model.SpeciesDog, Initial: "H", Name: "蝴蝶犬", Sort: 210},
|
||||
{Species: model.SpeciesDog, Initial: "J", Name: "金毛寻回犬", Sort: 220},
|
||||
{Species: model.SpeciesDog, Initial: "J", Name: "吉娃娃", Sort: 230},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "柯基犬", Sort: 240},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "可卡犬", Sort: 250},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "苏格兰柯利犬", Sort: 260},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "拉布拉多寻回犬", Sort: 270},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "罗威纳犬", Sort: 280},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "灵缇犬", Sort: 290},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "腊肠犬", Sort: 300},
|
||||
{Species: model.SpeciesDog, Initial: "M", Name: "马尔济斯犬", Sort: 310},
|
||||
{Species: model.SpeciesDog, Initial: "M", Name: "迷你雪纳瑞", Sort: 320},
|
||||
{Species: model.SpeciesDog, Initial: "N", Name: "牛头梗", Sort: 330},
|
||||
{Species: model.SpeciesDog, Initial: "Q", Name: "秋田犬", Sort: 340},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "萨摩耶", Sort: 350},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "松狮犬", Sort: 360},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "圣伯纳犬", Sort: 370},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "雪纳瑞", Sort: 390},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "西高地白梗", Sort: 400},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "喜乐蒂牧羊犬", Sort: 410},
|
||||
{Species: model.SpeciesDog, Initial: "Y", Name: "约克夏梗", Sort: 420},
|
||||
{Species: model.SpeciesDog, Initial: "Y", Name: "银狐犬", Sort: 430},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "中华田园犬", Sort: 440},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "中亚牧羊犬", Sort: 450},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "藏獒", Sort: 460},
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "阿拉斯加雪橇犬", SizeClass: "large", Sort: 10},
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "阿富汗猎犬", SizeClass: "large", Sort: 20},
|
||||
{Species: model.SpeciesDog, Initial: "A", Name: "澳洲牧羊犬", SizeClass: "medium", Sort: 30},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "比熊犬", SizeClass: "small", Sort: 40},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "比格犬", SizeClass: "medium", Sort: 50},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "边境牧羊犬", SizeClass: "medium", Sort: 60},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "巴哥犬", SizeClass: "small", Sort: 70},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "博美犬", SizeClass: "small", Sort: 80},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "巴吉度猎犬", SizeClass: "medium", Sort: 90},
|
||||
{Species: model.SpeciesDog, Initial: "B", Name: "北京犬", SizeClass: "small", Sort: 100},
|
||||
{Species: model.SpeciesDog, Initial: "C", Name: "柴犬", SizeClass: "medium", Sort: 110},
|
||||
{Species: model.SpeciesDog, Initial: "C", Name: "川东猎犬", SizeClass: "medium", Sort: 120},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "德国牧羊犬", SizeClass: "large", Sort: 130},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "杜宾犬", SizeClass: "large", Sort: 140},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "大丹犬", SizeClass: "giant", Sort: 150},
|
||||
{Species: model.SpeciesDog, Initial: "D", Name: "斗牛犬", SizeClass: "medium", Sort: 160},
|
||||
{Species: model.SpeciesDog, Initial: "F", Name: "法国斗牛犬", SizeClass: "medium", Sort: 170},
|
||||
{Species: model.SpeciesDog, Initial: "G", Name: "贵宾犬(泰迪)", SizeClass: "small", Sort: 180},
|
||||
{Species: model.SpeciesDog, Initial: "G", Name: "古代牧羊犬", SizeClass: "medium", Sort: 190},
|
||||
{Species: model.SpeciesDog, Initial: "H", Name: "哈士奇", SizeClass: "large", Sort: 200},
|
||||
{Species: model.SpeciesDog, Initial: "H", Name: "蝴蝶犬", SizeClass: "small", Sort: 210},
|
||||
{Species: model.SpeciesDog, Initial: "J", Name: "金毛寻回犬", SizeClass: "large", Sort: 220},
|
||||
{Species: model.SpeciesDog, Initial: "J", Name: "吉娃娃", SizeClass: "small", Sort: 230},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "柯基犬", SizeClass: "small", Sort: 240},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "可卡犬", SizeClass: "medium", Sort: 250},
|
||||
{Species: model.SpeciesDog, Initial: "K", Name: "苏格兰柯利犬", SizeClass: "medium", Sort: 260},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "拉布拉多寻回犬", SizeClass: "large", Sort: 270},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "罗威纳犬", SizeClass: "large", Sort: 280},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "灵缇犬", SizeClass: "large", Sort: 290},
|
||||
{Species: model.SpeciesDog, Initial: "L", Name: "腊肠犬", SizeClass: "small", Sort: 300},
|
||||
{Species: model.SpeciesDog, Initial: "M", Name: "马尔济斯犬", SizeClass: "small", Sort: 310},
|
||||
{Species: model.SpeciesDog, Initial: "M", Name: "迷你雪纳瑞", SizeClass: "small", Sort: 320},
|
||||
{Species: model.SpeciesDog, Initial: "N", Name: "牛头梗", SizeClass: "medium", Sort: 330},
|
||||
{Species: model.SpeciesDog, Initial: "Q", Name: "秋田犬", SizeClass: "large", Sort: 340},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "萨摩耶", SizeClass: "large", Sort: 350},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "松狮犬", SizeClass: "medium", Sort: 360},
|
||||
{Species: model.SpeciesDog, Initial: "S", Name: "圣伯纳犬", SizeClass: "giant", Sort: 370},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "雪纳瑞", SizeClass: "medium", Sort: 390},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "西高地白梗", SizeClass: "small", Sort: 400},
|
||||
{Species: model.SpeciesDog, Initial: "X", Name: "喜乐蒂牧羊犬", SizeClass: "small", Sort: 410},
|
||||
{Species: model.SpeciesDog, Initial: "Y", Name: "约克夏梗", SizeClass: "small", Sort: 420},
|
||||
{Species: model.SpeciesDog, Initial: "Y", Name: "银狐犬", SizeClass: "medium", Sort: 430},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "中华田园犬", SizeClass: "medium", Sort: 440},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "中亚牧羊犬", SizeClass: "large", Sort: 450},
|
||||
{Species: model.SpeciesDog, Initial: "Z", Name: "藏獒", SizeClass: "giant", Sort: 460},
|
||||
}
|
||||
for i := range breeds {
|
||||
breeds[i].Enabled = true
|
||||
|
||||
Reference in New Issue
Block a user