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 {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
// 生命阶段的划分标准。
|
||||
//
|
||||
// 这张表既用来判定阶段,也直接吐给前端当「划分依据」展示 —— 一份数据两个用途,
|
||||
// 而不是「代码里判定 + 文档里写标准」。后者一定会漂:改了阈值忘了改文档,
|
||||
// 用户看到的说明和实际行为对不上,比没有说明更糟。
|
||||
//
|
||||
// ── 为什么猫狗不能共用阈值 ──
|
||||
//
|
||||
// 猫的发育在品种间差别不大。狗完全取决于体型:
|
||||
// 吉娃娃 8 个月已经性成熟、可以换成犬粮;大丹犬 8 个月还在长骨头,
|
||||
// 这时候喂成犬粮(钙磷比和热量密度都不同)会加重关节负担。
|
||||
// 吉娃娃 7 岁还在成年期(8 岁才进中年,能活 13-16 年);
|
||||
// 大丹犬 7 岁已经是老年(平均寿命 8-10 年)。
|
||||
//
|
||||
// 原来代码里是一刀切「<12 月幼年,>=7 岁老年」,对猫勉强,对狗两头都错。
|
||||
//
|
||||
// 体型不能靠当前体重判断 —— 幼犬的体重说明不了它成年后多大。只能靠品种,
|
||||
// 所以 breeds 表带 size_class。
|
||||
|
||||
// 五个阶段。标签故意做成物种中立(不分「幼猫期/幼犬期」):
|
||||
// 物种本来就是另一个字段,分开会让 (species, stage) 的组合和模板配置都翻倍。
|
||||
const (
|
||||
StageJuvenile = "幼年期"
|
||||
StageYoung = "青年期"
|
||||
StageAdult = "成年期"
|
||||
StageMature = "中年期"
|
||||
StageSenior = "老年期"
|
||||
|
||||
// StageNewHome 和年龄正交:刚接回家的成年猫两套照护都需要。
|
||||
// 它不参与年龄判定,是叠加上去的一层。
|
||||
StageNewHome = "刚到家 0-30 天"
|
||||
)
|
||||
|
||||
// CareStages 后台配方案时可选的阶段。刚到家放最前面,它是所有宠物都会经历的
|
||||
var CareStages = []string{StageNewHome, StageJuvenile, StageYoung, StageAdult, StageMature, StageSenior}
|
||||
|
||||
// 犬类体型。分界取自成年体重,是行业通行的分法
|
||||
const (
|
||||
SizeSmall = "small" // < 10kg
|
||||
SizeMedium = "medium" // 10-25kg
|
||||
SizeLarge = "large" // 25-40kg
|
||||
SizeGiant = "giant" // > 40kg
|
||||
)
|
||||
|
||||
// SizeLabels 体型的中文名和依据,后台和小程序都用它
|
||||
var SizeLabels = map[string]string{
|
||||
SizeSmall: "小型犬(成年 <10kg)",
|
||||
SizeMedium: "中型犬(成年 10-25kg)",
|
||||
SizeLarge: "大型犬(成年 25-40kg)",
|
||||
SizeGiant: "巨型犬(成年 >40kg)",
|
||||
}
|
||||
|
||||
// StageRule 一个阶段的月龄区间和依据
|
||||
type StageRule struct {
|
||||
Stage string `json:"stage"`
|
||||
FromMon int `json:"from_month"` // 含
|
||||
ToMon int `json:"to_month"` // 不含;0 = 无上限
|
||||
Range string `json:"range"` // 展示用,如「6-12 月」
|
||||
Basis string `json:"basis"` // 划分依据,直接展示给用户
|
||||
Focus string `json:"focus"` // 这个阶段的养护重点
|
||||
}
|
||||
|
||||
// 猫:AAFP/AAHA 猫生命阶段指南那套,在「成年」里细分出中年
|
||||
var catStages = []StageRule{
|
||||
{StageJuvenile, 0, 6, "0-6 月", "断奶到六月龄是生长最快的阶段,三针核心疫苗和首次驱虫都在这里完成",
|
||||
"疫苗序列、驱虫、社会化、少量多餐"},
|
||||
{StageYoung, 6, 12, "6-12 月", "六月龄前后性成熟,绝育窗口;一岁前后换成猫粮",
|
||||
"绝育安排、换成猫粮、开始控体重"},
|
||||
{StageAdult, 12, 84, "1-7 岁", "身体状态最稳定的时期,重点是把基线数据记下来",
|
||||
"每月称重、年度体检、口腔护理"},
|
||||
{StageMature, 84, 132, "7-11 岁", "七岁起代谢变慢,年度血检开始能查出早期问题",
|
||||
"年度血检、体重和牙结石重点看"},
|
||||
{StageSenior, 132, 0, "11 岁以上", "十一岁后肾脏和甲状腺问题高发,体检频率要加倍",
|
||||
"半年一次体检、肾和甲状腺、饮水量重点记"},
|
||||
}
|
||||
|
||||
// 狗:同样五段,阈值按体型走。越大的犬种生长期越长、老得越早
|
||||
var dogStages = map[string][]StageRule{
|
||||
SizeSmall: {
|
||||
{StageJuvenile, 0, 8, "0-8 月", "小型犬生长期短,八月龄基本定型", "疫苗序列、驱虫、社会化"},
|
||||
{StageYoung, 8, 12, "8-12 月", "性成熟早,绝育窗口也早", "绝育安排、换成犬粮"},
|
||||
{StageAdult, 12, 96, "1-8 岁", "小型犬寿命长,成年期也长", "每月称重、年度体检、牙结石"},
|
||||
{StageMature, 96, 132, "8-11 岁", "八岁起进入中年,心脏和牙齿是重点", "年度血检、心脏听诊、洁牙"},
|
||||
{StageSenior, 132, 0, "11 岁以上", "小型犬普遍能活到 13-16 岁,十一岁后算老年", "半年一次体检、关节和心脏"},
|
||||
},
|
||||
SizeMedium: {
|
||||
{StageJuvenile, 0, 12, "0-12 月", "一岁前后完成生长", "疫苗序列、驱虫、社会化"},
|
||||
{StageYoung, 12, 18, "12-18 月", "骨骼刚闭合,可以开始正常运动量", "换成犬粮、运动量上调"},
|
||||
{StageAdult, 18, 84, "1.5-7 岁", "维持期", "每月称重、年度体检、牙结石"},
|
||||
{StageMature, 84, 120, "7-10 岁", "七岁起体重和关节要重点看", "年度血检、体重控制、关节"},
|
||||
{StageSenior, 120, 0, "10 岁以上", "中型犬平均寿命 10-13 岁", "半年一次体检、关节和肿瘤筛查"},
|
||||
},
|
||||
SizeLarge: {
|
||||
{StageJuvenile, 0, 15, "0-15 月", "大型犬生长期长到 15 个月,这期间不能过量补钙或过早剧烈运动",
|
||||
"控制生长速度、疫苗序列、避免跳跃"},
|
||||
{StageYoung, 15, 24, "15-24 月", "两岁前骨骼才完全闭合", "换成犬粮、逐步加运动量"},
|
||||
{StageAdult, 24, 72, "2-6 岁", "维持期较短", "每月称重、年度体检、髋关节"},
|
||||
{StageMature, 72, 96, "6-8 岁", "六岁起就要按中年管,比中小型犬早", "年度血检、髋关节、体重控制"},
|
||||
{StageSenior, 96, 0, "8 岁以上", "大型犬平均寿命 8-12 岁,八岁进老年", "半年一次体检、关节和心脏"},
|
||||
},
|
||||
SizeGiant: {
|
||||
{StageJuvenile, 0, 18, "0-18 月", "巨型犬生长期最长,18 个月还在长;这阶段最忌过度补钙",
|
||||
"严格控制生长速度、疫苗序列、限制跳跃"},
|
||||
{StageYoung, 18, 24, "18-24 月", "两岁才算成年", "换成犬粮、关节保养开始"},
|
||||
{StageAdult, 24, 60, "2-5 岁", "成年期很短", "每月称重、年度体检、心脏和关节"},
|
||||
{StageMature, 60, 84, "5-7 岁", "五岁就要按中年管 —— 巨型犬老得最快", "半年一次血检、心脏、体重"},
|
||||
{StageSenior, 84, 0, "7 岁以上", "巨型犬平均寿命 7-10 岁", "每半年体检、心脏和骨科肿瘤"},
|
||||
},
|
||||
}
|
||||
|
||||
// StageRules 某个物种(狗还要看体型)的完整划分标准,给前端展示用
|
||||
func StageRules(species, sizeClass string) []StageRule {
|
||||
if species == model.SpeciesDog {
|
||||
if rs, ok := dogStages[sizeClass]; ok {
|
||||
return rs
|
||||
}
|
||||
return dogStages[SizeMedium] // 品种没标体型时按中型算,最不容易错得离谱
|
||||
}
|
||||
return catStages
|
||||
}
|
||||
|
||||
// stageOfMonths 按月龄在规则表里落位
|
||||
func stageOfMonths(rules []StageRule, months int) string {
|
||||
for _, r := range rules {
|
||||
if months >= r.FromMon && (r.ToMon == 0 || months < r.ToMon) {
|
||||
return r.Stage
|
||||
}
|
||||
}
|
||||
return StageAdult
|
||||
}
|
||||
|
||||
// StageFromAge 由物种、体型、月龄推断阶段
|
||||
func StageFromAge(species, sizeClass string, months int) string {
|
||||
if months < 0 {
|
||||
return StageAdult // 没有生日只能按成年算,它是最不容易出错的默认
|
||||
}
|
||||
return stageOfMonths(StageRules(species, sizeClass), months)
|
||||
}
|
||||
|
||||
// sizeClassOfBreed 查品种的体型。查不到返回空,上层按中型兜底
|
||||
func (s *Service) sizeClassOfBreed(species, breed string) string {
|
||||
if species != model.SpeciesDog || breed == "" {
|
||||
return ""
|
||||
}
|
||||
var b model.Breed
|
||||
if err := s.db.Where("species = ? AND name = ?", species, breed).First(&b).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
return b.SizeClass
|
||||
}
|
||||
|
||||
// StageOf 由生日推断阶段(狗会查品种体型)
|
||||
func (s *Service) StageOf(species, breed string, birthday *time.Time) string {
|
||||
if birthday == nil || birthday.IsZero() {
|
||||
return StageAdult
|
||||
}
|
||||
return StageFromAge(species, s.sizeClassOfBreed(species, breed), monthsSince(*birthday))
|
||||
}
|
||||
|
||||
// NormalizeStage 定下最终使用的阶段。
|
||||
// 用户手选的优先(领养的猫生日是估的,主人按体型判断更准;而模型里的月龄阈值
|
||||
// 对混种和边界体型都是一刀切);没选或选了不认识的值才按生日推。
|
||||
func (s *Service) NormalizeStage(species, breed, picked string, birthday *time.Time) string {
|
||||
for _, v := range CareStages {
|
||||
if picked == v {
|
||||
return picked
|
||||
}
|
||||
}
|
||||
return s.StageOf(species, breed, birthday)
|
||||
}
|
||||
|
||||
// AgeLabel 月龄的中文展示。不入库 —— 存下来第二天就过期了
|
||||
func AgeLabel(birthday *time.Time) string {
|
||||
if birthday == nil || birthday.IsZero() {
|
||||
return ""
|
||||
}
|
||||
m := monthsSince(*birthday)
|
||||
if m < 0 {
|
||||
return ""
|
||||
}
|
||||
if m < 24 {
|
||||
return fmt.Sprintf("%d个月", m)
|
||||
}
|
||||
if m%12 == 0 {
|
||||
return fmt.Sprintf("%d岁", m/12)
|
||||
}
|
||||
return fmt.Sprintf("%d岁%d个月", m/12, m%12)
|
||||
}
|
||||
|
||||
// StageDrift 「档案里存的阶段」和「按生日算出来的阶段」不一致。
|
||||
//
|
||||
// 为什么不自动改:
|
||||
// 1. 用户可能手改过 —— 领养的猫生日是估的,主人按体型判断比月龄阈值准
|
||||
// 2. 已经排好的这个月计划不该被推翻。套用方案是「往里加」,不是「替换」
|
||||
// 3. 偷偷换会让用户莫名发现计划变了,找不到原因
|
||||
//
|
||||
// 所以系统只负责发现,用户负责决定:小程序端提示「它已经进入成年期了,
|
||||
// 要不要换一套方案」,点了才套用。
|
||||
type StageDrift struct {
|
||||
Drifted bool `json:"drifted"`
|
||||
Stored string `json:"stored"` // 档案里现在存的
|
||||
Computed string `json:"computed"` // 按生日算出来的
|
||||
AgeLabel string `json:"age_label"` // 「1岁3个月」
|
||||
Basis string `json:"basis"` // 新阶段的划分依据,一并给出来
|
||||
Focus string `json:"focus"` // 新阶段的养护重点
|
||||
Size string `json:"size"` // 犬类的体型说明,猫为空
|
||||
}
|
||||
|
||||
// CheckStageDrift 检查某只宠物的阶段有没有漂
|
||||
func (s *Service) CheckStageDrift(pet *model.Pet) *StageDrift {
|
||||
out := &StageDrift{Stored: pet.Stage, AgeLabel: AgeLabel(pet.Birthday)}
|
||||
if pet.Birthday == nil || pet.Birthday.IsZero() {
|
||||
return out // 没生日算不了,不提示
|
||||
}
|
||||
// 「刚到家」是叠加层不是年龄段,处在这个状态时不谈漂移
|
||||
if pet.Stage == StageNewHome {
|
||||
return out
|
||||
}
|
||||
species := SpeciesOf(pet.Type)
|
||||
size := s.sizeClassOfBreed(species, pet.Breed)
|
||||
out.Computed = StageFromAge(species, size, monthsSince(*pet.Birthday))
|
||||
if species == model.SpeciesDog {
|
||||
if l, ok := SizeLabels[size]; ok {
|
||||
out.Size = l
|
||||
} else {
|
||||
out.Size = SizeLabels[SizeMedium] + "(品种未标体型,按中型算)"
|
||||
}
|
||||
}
|
||||
if out.Computed == out.Stored || out.Computed == "" {
|
||||
return out
|
||||
}
|
||||
out.Drifted = true
|
||||
for _, r := range StageRules(species, size) {
|
||||
if r.Stage == out.Computed {
|
||||
out.Basis = r.Basis
|
||||
out.Focus = r.Focus
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -51,6 +51,11 @@ func (s *Service) ListPets(userID string) ([]model.Pet, error) {
|
||||
urls := s.fileURLs(ids)
|
||||
for i := range pets {
|
||||
pets[i].AvatarURL = urls[pets[i].AvatarFileID]
|
||||
// Age 和 Stage 都是「会过期」的值。Age 直接现算(不入库);
|
||||
// Stage 存着但要校准 —— 半年前建档时是幼年期,现在可能已经进青年期了。
|
||||
// 校准只改返回值不写库:用户手改过的阶段不该被静默冲掉,
|
||||
// 「该换一套方案了」这件事要提示他、由他决定(见 StageDrift)
|
||||
pets[i].Age = AgeLabel(pets[i].Birthday)
|
||||
}
|
||||
return pets, err
|
||||
}
|
||||
@@ -60,6 +65,7 @@ func (s *Service) GetPet(userID, petID string) (*model.Pet, error) {
|
||||
p, err := s.ownedPet(userID, petID)
|
||||
if err == nil && p != nil {
|
||||
p.AvatarURL = s.fileURL(p.AvatarFileID)
|
||||
p.Age = AgeLabel(p.Birthday)
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -91,6 +97,9 @@ func (s *Service) CreatePet(userID string, in PetInput) (*model.Pet, error) {
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Age 是计算字段,创建后的返回也得填 —— 不填前端拿到的是空字符串
|
||||
pet.Age = AgeLabel(pet.Birthday)
|
||||
pet.AvatarURL = s.fileURL(pet.AvatarFileID)
|
||||
return &pet, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,5 +46,8 @@ func (s *Service) ownedPet(userID, petID string) (*model.Pet, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Age 是计算字段(gorm:"-")。在这里填而不是在每个调用点填 ——
|
||||
// ownedPet 是所有「取一只宠物」的路径的收口,漏一个就会返回空年龄
|
||||
pet.Age = AgeLabel(pet.Birthday)
|
||||
return &pet, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user