c4c0c2b7cd
## 你问的「养了半年会不会自动切换」
原来的答案是「都不会,而且比这更糟」:
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>
125 lines
6.2 KiB
Plaintext
125 lines
6.2 KiB
Plaintext
<nav-bar title="{{editing ? '编辑档案' : '建一份档案'}}" show-back="{{true}}"></nav-bar>
|
||
|
||
<!-- 三步进度。编辑时不分步(用户是来改某一项的,不该被按顺序走一遍) -->
|
||
<view wx:if="{{!editing}}" class="pf-steps">
|
||
<view wx:for="{{steps}}" wx:key="*this" class="pf-step {{index <= step ? 'on' : ''}}">
|
||
<view class="pf-dot">{{index < step ? '✓' : index + 1}}</view>
|
||
<text class="pf-slabel">{{item}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||
<view class="page-body pf-body">
|
||
|
||
<!-- ① 名字 + 头像 -->
|
||
<block wx:if="{{editing || step === 0}}">
|
||
<view class="card">
|
||
<view class="pf-h">先给它起个名字</view>
|
||
<view class="pf-av-wrap" bindtap="pickAvatar">
|
||
<view class="pf-av">
|
||
<image wx:if="{{form.avatar_url}}" class="pf-av-img" src="{{form.avatar_url}}" mode="aspectFill"></image>
|
||
<block wx:else><pt-icon name="photo" size="{{56}}"></pt-icon></block>
|
||
</view>
|
||
<view class="pf-av-tip">{{form.avatar_url ? '换一张' : '传张照片'}}</view>
|
||
</view>
|
||
<view class="field"><label>名字</label>
|
||
<input class="input" placeholder="例如:布丁" placeholder-class="placeholder"
|
||
value="{{form.name}}" maxlength="16" bindinput="onName"/></view>
|
||
</view>
|
||
</block>
|
||
|
||
<!-- ② 猫还是狗 + 品种 -->
|
||
<block wx:if="{{editing || step === 1}}">
|
||
<view class="card">
|
||
<view class="pf-h">它是猫还是狗?</view>
|
||
<view class="pf-species">
|
||
<view class="pf-sp {{form.species === 'cat' ? 'on' : ''}}" data-sp="cat" bindtap="pickSpecies">
|
||
<pt-icon name="cat" size="{{64}}"></pt-icon><text>猫猫</text>
|
||
</view>
|
||
<view class="pf-sp {{form.species === 'dog' ? 'on' : ''}}" data-sp="dog" bindtap="pickSpecies">
|
||
<pt-icon name="dog" size="{{64}}"></pt-icon><text>狗狗</text>
|
||
</view>
|
||
</view>
|
||
<view class="field"><label>品种</label>
|
||
<view class="picker-box {{form.breed ? '' : 'pf-empty'}}" bindtap="openBreed">
|
||
{{form.breed || '选一个品种'}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
|
||
<!-- ③ 其他基本信息 -->
|
||
<block wx:if="{{editing || step === 2}}">
|
||
<view class="card">
|
||
<view class="pf-h">再补几项,之后都能改</view>
|
||
<view class="field"><label>性别</label><view class="seg">
|
||
<view wx:for="{{genders}}" wx:key="*this" class="seg-btn {{form.gender === item ? 'selected' : ''}}"
|
||
data-v="{{item}}" bindtap="pickGender">{{item}}</view>
|
||
</view></view>
|
||
|
||
<!-- 生日和到家日分开:领养的成年猫狗生日常常是不知道的,
|
||
而「一起生活了多少天」要按到家日算。合成一个字段两件事都说不准 -->
|
||
<view class="field"><label>生日</label>
|
||
<picker mode="date" value="{{form.birthday}}" end="{{today}}" bindchange="onBirthday">
|
||
<view class="picker-box {{form.birthday ? '' : 'pf-empty'}}">{{form.birthday || '不知道可以先跳过'}}</view>
|
||
</picker></view>
|
||
<view class="field"><label>到家日期</label>
|
||
<picker mode="date" value="{{form.arrived_at}}" end="{{today}}" bindchange="onArrived">
|
||
<view class="picker-box {{form.arrived_at ? '' : 'pf-empty'}}">{{form.arrived_at || '哪天接回家的'}}</view>
|
||
</picker></view>
|
||
|
||
<view class="field"><label>当前体重(kg)</label>
|
||
<input class="input" type="digit" placeholder="例如:2.8" placeholder-class="placeholder"
|
||
value="{{form.weight}}" bindinput="onWeight"/></view>
|
||
<view class="field"><label>毛色</label>
|
||
<input class="input" placeholder="例如:橘白 / 奶牛 / 黑" placeholder-class="placeholder"
|
||
value="{{form.color}}" maxlength="16" bindinput="onColor"/></view>
|
||
<view class="field"><label>当前阶段</label><view class="mini-options">
|
||
<view wx:for="{{stages}}" wx:key="*this" class="option {{form.stage === item ? 'selected' : ''}}"
|
||
data-v="{{item}}" bindtap="pickStage">{{item}}</view>
|
||
</view>
|
||
<!-- 把划分标准摆出来。用户不该猜「幼年期」是几个月到几个月,
|
||
狗还得知道自己这个体型的阈值和别的体型不一样 -->
|
||
<view wx:if="{{stageBasis}}" class="pf-basis">{{stageBasis}}</view>
|
||
<view wx:if="{{sizeLabel}}" class="tiny">{{sizeLabel}}——阶段阈值按体型算</view>
|
||
<view wx:if="{{stageAuto && stageAuto !== form.stage}}" class="tiny">按生日推算是「{{stageAuto}}」,可以手改</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部按钮固定 -->
|
||
<view class="pf-foot">
|
||
<view wx:if="{{!editing && step > 0}}" class="btn btn-ghost pf-back" bindtap="prev">上一步</view>
|
||
<view class="btn btn-primary pf-next {{saving ? 'pf-busy' : ''}}" bindtap="next">
|
||
{{saving ? '保存中…' : (editing ? '保存' : (step < 2 ? '下一步' : '完成建档'))}}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 品种选择器:左侧列表 + 右侧 A-Z 索引 -->
|
||
<view wx:if="{{breedShow}}" class="pf-mask" bindtap="closeBreed">
|
||
<view class="pf-sheet" catchtap="noop">
|
||
<view class="pf-sheet-h">
|
||
<text>选择品种</text>
|
||
<view class="link" catchtap="clearBreed">不确定</view>
|
||
</view>
|
||
<view class="pf-breed-wrap">
|
||
<scroll-view class="pf-breed-list" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}"
|
||
scroll-into-view="{{breedAnchor}}" scroll-with-animation="{{true}}">
|
||
<block wx:for="{{breedGroups}}" wx:key="initial">
|
||
<view class="pf-bi" id="bi-{{item.initial}}">{{item.initial}}</view>
|
||
<view wx:for="{{item.items}}" wx:for-item="b" wx:key="id"
|
||
class="pf-brow {{form.breed === b.name ? 'on' : ''}}"
|
||
data-name="{{b.name}}" bindtap="pickBreed">{{b.name}}</view>
|
||
</block>
|
||
<view wx:if="{{!breedGroups.length}}" class="empty">还没有配置品种</view>
|
||
</scroll-view>
|
||
<view class="pf-index">
|
||
<view wx:for="{{breedGroups}}" wx:key="initial" class="pf-ichar"
|
||
data-i="{{item.initial}}" bindtap="jumpInitial">{{item.initial}}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|