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:
@@ -13,41 +13,13 @@ import (
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
// 模板分组。注意 StageNewHome 不是年龄阶段,而是「刚到家」叠加包:
|
||||
// 建档时先按年龄阶段取一套,若到家不足 30 天,再把这一套叠上去。
|
||||
const (
|
||||
StageNewHome = "刚到家 0-30 天"
|
||||
StageJuvenile = "幼年期"
|
||||
StageAdult = "成年期"
|
||||
StageSenior = "老年期"
|
||||
)
|
||||
// 阶段常量、CareStages 和判定逻辑都搬到 life_stage.go 了:
|
||||
// 那边一张表既驱动判定又直接吐给前端当「划分依据」,两个用途一份数据。
|
||||
// 阶段从 3 段(幼年/成年/老年)细分成 5 段,狗还按体型走不同阈值。
|
||||
|
||||
// CareSpecies / CareStages 支持的物种与分组(字符串与 onboarding 一致)
|
||||
// CareSpecies 支持的物种(字符串与 onboarding 一致)
|
||||
var CareSpecies = []string{model.SpeciesCat, model.SpeciesDog}
|
||||
var CareStages = []string{StageNewHome, StageJuvenile, StageAdult, StageSenior}
|
||||
|
||||
// AgeStages 只含年龄阶段,用于推断和校验(不含叠加包)
|
||||
var AgeStages = []string{StageJuvenile, StageAdult, StageSenior}
|
||||
|
||||
// StageFromBirthday 由生日推断年龄阶段。
|
||||
// 猫:12 月龄成年、7 岁进入老年。
|
||||
// 犬:12 月龄成年(大型犬实际 18-24 月,这里取通用值)、7 岁进入老年。
|
||||
// 拿不到生日就回落到成年期——它的模板最保守,不会给幼崽排年度疫苗、
|
||||
// 也不会给成年犬排"每周两次称重"。
|
||||
func StageFromBirthday(species string, birthday *time.Time) string {
|
||||
if birthday == nil || birthday.IsZero() {
|
||||
return StageAdult
|
||||
}
|
||||
months := monthsSince(*birthday)
|
||||
switch {
|
||||
case months < 12:
|
||||
return StageJuvenile
|
||||
case months >= 7*12:
|
||||
return StageSenior
|
||||
default:
|
||||
return StageAdult
|
||||
}
|
||||
}
|
||||
|
||||
// monthsSince 从某天到今天的整月数(不足一月算 0)
|
||||
func monthsSince(t time.Time) int {
|
||||
@@ -62,17 +34,6 @@ 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 {
|
||||
|
||||
Reference in New Issue
Block a user