Commit Graph

8 Commits

Author SHA1 Message Date
Blizzard c4c0c2b7cd 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>
2026-07-30 12:51:10 +08:00
Blizzard aefb1bc103 feat: 建档改成三步向导页,品种改成后台可配的选择器
## pages/petform 三步向导
  ① 名字 + 传头像
  ② 猫还是狗 → 选品种
  ③ 性别 / 生日 / 到家日期 / 体重 / 毛色 / 阶段

**编辑时不分步**,一屏全放开:用户是来改某一项的,不该被按顺序走一遍。
同一个页面两种形态,靠 ?id= 区分。

每一步只校验这一步的东西。三步全填完才校验的话,用户在第三步才被告知
第一步没填名字,还得翻回去。

进度条没用 seg-tabs:那是「可以随意切」的语义,这里有顺序,
点第三步跳过前两步会拿到一份没名字的档案。

## 生日和到家日期分开
原来是一个「生日 / 到家时间」字段。领养的成年猫狗生日常常是不知道的,
而「一起生活了多少天」要按到家日算——合成一个字段两件事都说不准。
验过:到家 4 个月前 → 第 121 天;按生日算会是 801 天。

模型里 Birthday 和 ArrivedAt 本来就是两个字段,只是表单没露出来。

## 品种:自由文本 → 后台可配的选择器
原来品种是个输入框,用户手打「柯基」「柯基犬」「威尔士柯基」算三个品种,
以后想按品种做体重基准、常见病提示这类事就没法做。

新增 breeds 表,预置 34 种猫 + 45 种狗,按首字母分组、右侧 A-Z 索引跳转、
首字母吸顶。留了「不确定」——领养的串串确实说不出品种,逼着选一个只会
得到假数据。换物种会清掉已选品种(原来那个一定不对了)。

**首字母存字段不运行时算**:Go 没有标准拼音库,而多音字自动转常出错——
「藏獒」按 cang 还是 zang 分组、「柴犬」的柴,人来定比库来猜靠谱。
后台新增品种时手填这一位,服务端校验必须是单个 A-Z。

已有档案在用的品种不许删(删了那些档案的品种就变成查不到的字符串),
想隐藏用 enabled=false。同物种下不许重名——品种是统计口径,重名会把
同一种猫劈成两半。

预置数据自己填错了两条,实跑时看出来的:「苏格兰柯利犬」我按「柯利」
填了 K,应该按「苏格兰」算 S;「双血统边牧」根本不是品种,是血统描述。
seed 源和库里都改了。

## 弹层继续瘦
addPet/editPet 两支删掉,连带 10 个方法、ageFromBirthday/GENDERS/STAGES
和 5 个 add* 字段。累计(从记录表单搬走那次算起):
  js   1110 → 734 行
  wxml  485 → 307 行

多宠管理里「点某只去编辑」原来是 setType('editPet'),那支没了会开一个
空白弹层——改成关弹层再跳页。8 个页面的入口全换完,grep 无残留。

## 验证(预生产库实跑)
  品种接口   猫 34 种 16 个首字母、狗 45 种 17 个首字母;非法 species 返回 []
  建档      生日 / 到家日期 分别落库,品种毛色阶段都对
  编辑      只改名字,到家日期没被冲掉
  修正      柯利犬 K→S、双血统边牧已删

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:26:51 +08:00
Blizzard 3ea50efb7d feat: 添加记录独立成页,表单字段由后端配置驱动
## 弹层里塞 24 个表单是走不通的
弹层高度受 .sheet-scroll 的 78vh 限制,字段一多就变成内滚;24 项的选择器
挤在弹层里更难看(用户原话「很丑」)。整体搬成页面。

## pages/addrecord —— 一个页面吃掉全部 24 种
形态:记录宠物 / 记录时间 / 类型专属字段 / 描述 + 照片 / 底部固定保存键。

关键在「类型专属字段」整段是后端驱动的:前端不认识「体重」「金额」这些业务词,
只认识 number / options / text 三种渲染方式,以及每个字段的值该落到哪儿。

record_types 加了 fields 列,后台用紧凑写法配(一行一个):

  number:体重:kg                    数值 → num_value
  options:状态:正常|软便|拉稀          单选 → category
  options:症状:呕吐|拉稀|精神差:-      末尾 - 表示不落 category,只进标题
  text:吃了什么                      文本 → 只进标题
  (空)                            只要 时间+描述+照片

解析放服务端不放前端:配错了要在后台保存时就报出来(第几行、错在哪),
不能等用户点开表单才发现渲染不出东西。读取时解析失败只让这一种事项没有额外
字段并打 warn,不让一条烂配置把整个记录页打空。

## 那个 - 开关不是过度设计
异常观察有两个单选(症状 + 严重程度),而 category 只有一个坑,
周报的高风险数按 category='高' 统计(report.go:46)——必须能指定谁占这个坑。
同理账单按 cost 的 category 分组聚合(report.go:134),
食便相关性排除 poop 的 category='正常'。这三处口径都得严丝合缝对上。

## 记录页瘦成纯列表
只留分组宫格。健康洞察 / 体重趋势 / 健康时间轴整页搬到 pages/history,
从报告页「记录与趋势」进——报告页才是看数据的地方,而一个「我要记一笔」的
页面上摆三块只读图表,用户每次都得先滚过去才能找到要点的东西。

**没有直接删掉那三块**:时间轴是唯一能看和删历史记录的地方,报告页只有
周报/账单这些聚合。删了用户就再也看不到自己记过什么。搬页面用的是 git 里
改动前的完整版复制,比往 report.js 里合并代码安全。

## 弹层瘦了一圈
9 个记录分支删掉,连带 17 个方法、SIMPLE_HINT、buildRecord 的 7 个 case
和一批只有它们在用的 data 字段。
  js   1089 → 857 行
  wxml  480 → 335 行
弹层现在只管「不是记一笔」的事:提醒、海报、档案、发帖、评论、导出、反馈。
buildRecord 只剩 vetSummary 一个分支(它写一条 note 留痕,不是用户填的表单)。

## 验证(预生产库实跑)
24 种的字段配置逐个核对解析结果,然后照 addrecord 的 buildBody 组装落库:
  体重  title='体重:4.6kg'        num=4.6            → 宠物档案回写成 4.6kg ✓
  记账  title='记账:128元 医疗'     num=128 cat=医疗    → 账单 total=128 分类[(医疗,128)] ✓
  异常  title='异常:呕吐 高'        cat=高             → 周报 high_risk_count=1 ✓
  排便  cat=软便                                    食便相关性口径保住
  饮食  title='饮食:幼猫粮 45g 偏少' cat=偏少
  喝水  num=180(新类型带数值,老代码没有它的分支也不影响)
  洗澡  title='洗澡'(无额外字段)
  看病  num=320(两个字段:文本+数值)
体重趋势 1 个点、值 4.6,没被其他 7 条污染。

回填踩过一次坑:fields 列是服务启动时 AutoMigrate 才建的,我在启动前就跑了
回填,Update 报错被忽略、13 行静默没写进去。第二版加了 HasColumn 前置检查
和逐行错误统计才发现。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:25:42 +08:00
Blizzard 60ee9d4bbc feat(be): 记录类型挪进数据库,后台可配 + 15 种新类型
## 为什么
9 种记录类型全是低频医疗和记账(疫苗一年一次、驱虫一季一次、体检更少),
用户今天打开 App 大概率没什么可记,时间轴是空的。竞品 35 种里有 26 种是
高频日常(洗澡/剪指甲/换猫砂/喝水/洗食盆),那才是每天打开的理由。

## 一个比预想好得多的发现
本来担心加类型会踩到分析逻辑。把那 9 个常量的引用全捞了一遍,结论是不会:

  weight       record.go:83 回写宠物体重;home/insight/report 的体重趋势与基准
  food + poop  insight.go:130 食便相关性
  symptom      insight.go:160 症状聚类;report 高风险计数
  cost         report.go:99 消费统计
  vaccine      report.go:137 疫苗完成度
  deworm/medicine/photo  没有任何特殊分支

这些分支全都是「按某个具体常量过滤」,没有一处是 switch 全枚举、也没有
default 兜底。CreateRecord 更是完全没有 type 白名单校验。所以新增类型
不可能碰到任何现存逻辑——一行分析代码都不用改。

于是分工是:
  record_types 表  负责「展示什么、能记什么」
  那 9 个常量      负责「哪几种参与分析」
两者各管一段,常量一个都没删。

## 表设计
code / label / icon / group_key / sort / enabled / form / locked。

group_key 而不是 group:group 是 MySQL 保留字,GORM 会加反引号所以能跑,
但手写 SQL 排查时很容易踩。

form 区分 full(弹层里有专用 wx:elif 分支)和 simple(时间+备注+照片)。
15 种新类型全是 simple——下一步在弹层加一个通用分支吃掉它们,不然 28 个
分支再加 15 个会把 bottom-sheet 写到 1800 行。

locked 给那 9 种:有代码分支依赖、且被 care_template_items.sheet_type 引用。

## 两条硬拦在 service 层的规则
1. locked 的删不掉
2. 已经有记录的删不掉(删了历史记录会变孤儿)
想让它从记录页消失应该用 enabled=false,历史记录仍能正常显示。

都在 service 层拦,不是靠前端隐藏按钮。改 code 同样受 2 的约束——
code 是落库值,改了历史记录会对不上任何类型。

## bootstrap 的取舍
文章和养护模板当初按你的要求从代码里挪走了,这次 24 种类型仍然放在 seed 里,
因为它不是内容而是结构:表空着记录页一个可记事项都没有,App 直接不能用。
和管理员账号一样,只在表**完全为空**时铺一次,之后一切以数据库为准。
表非空就完全不碰,否则后台删掉的类型会每次重启自己长回来。

ListRecordTypes 在表为空时返回空数组 + 打 warn,不硬编码兜底一份——
那等于把「可配置」又变回代码常量,下次改配置的人会发现改了没用。

## 图标
新增 15 个(iconfont.wxss 40 → 55),全部取自本地那份 tabler-icons-3.46.0
真源,不是凭记忆画路径。已解码抽检确认路径非空。

## 验证(生产库实跑)
  建表 + 24 条 bootstrap 一次到位,分组 daily6/health7/care6/clean5
  locked 9 个、simple 表单 15 个
  规则 1  删 weight → 「有统计和分析逻辑依赖它,不能删」
  规则 2  新增 nose 成功 → 重复 code 报错 → 删除成功
  规则 3  造 1 条 tmptest 记录 → 删类型被拦;删掉记录后再删 → 成功;残留 0
  规则 4  停用「美容」→ 小程序端洗护组只剩 5 项;恢复后回到 6 项

规则 3 一开始没真验到:库里唯一的记录类型是 weight,locked 先拦住了,
「已有记录」那个分支根本没走到。补了个造真记录的测试才算验过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:24:24 +08:00
Blizzard 4f6f0f632e refactor(be): 文章内容从代码里移出,改为直接同步数据库
几百字的正文塞进 Go 源文件,既看不清 diff 也没法在后台改完再同步
回来——内容和代码本来就是两条生命周期。

- 删掉 articles_more.go(16 篇)
- seed.go 只保留 seedAdmin,去掉 seedArticles / builtinArticles /
  backfillArticleContent(后者是给早期无正文文章补内容的一次性逻辑,
  已无意义)

数据已经在库里,删代码不影响:预生产 20 篇文章原样在,小程序端照常
读得到。上线时手动同步这张表即可。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 13:53:35 +08:00
Blizzard 7159930d9c feat(admin): 随机昵称 + 建档数统计 + 帖子发布时间 + 16 篇新文章
## 新用户昵称
微信登录拿不到昵称(要用户主动授权),原来一律叫「微信用户」,
社区里一屏全是同名,后台也没法区分。改成随机生成「铲屎官12138」
这种形式:10 个前缀 × 5 位数字,落库前查重,撞了重试。

新增 users.nickname_auto 标记这个昵称是系统生成的。原来判断该不该
用微信授权昵称覆盖,靠的是「昵称是不是等于『微信用户』」这个字符串
比较,改成随机昵称后这招就失效了。有标记位才能安全覆盖,同时保证
用户自己改过的昵称不会被冲掉(改昵称时会把标记置 false)。

## 用户管理
加「宠物档案」列,显示每个用户建了几个档。用一次 GROUP BY 查完
当页所有人,不是每行一次 count——那会是 N+1。

## 帖子审核
加发布时间列。今天的显示「今天 14:48」,昨天的显示「昨天」,
更早的显示日期——审核时关心的是「多久以前发的」而不是完整时间戳。

## 文章
新增 16 篇,连同原有 4 篇共 20 篇,每篇 400-570 字:
猫 5 篇(新猫到家第一周 / 猫砂盆 / 公猫尿闭 / 掉毛毛球 / 老年猫信号)
狗 5 篇(社会化关键期 / 定点排便 / 遛狗时长 / 刷牙 / 中暑防护)
通用 6 篇(体重趋势 / 驱虫周期 / 绝育 / 急诊清单 / 就医准备 / 养宠预算)

沿用和 AI 助手一样的安全护栏:不做诊断、不给药物剂量,凡是可能
延误病情的都写清「什么时候必须去医院」。犬用驱虫药对猫的致死风险
这类真实事故也写进去了。

seedArticles 从「库空才插」改成「按标题补缺失」,以后往内置清单加
文章,老库启动也能拿到。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:55:22 +08:00
Blizzard 3fde90e97b feat: 补齐五块缺失功能(记录丢数据/文章正文/提醒管理/我的页入口/分享/关注)
P0 修数据问题
- 记录弹层有 5 个输入框从未绑定,用户填的便便备注、饮食内容、药品名称、
  剂量、消费备注全部丢弃;用药记录因此只存下「用药记录」四个字。已全部绑定并入库
- 异常观察保存时写死「风险中」,AI 实际判定的等级被丢掉。改为使用 riskData
  的真实 risk_level,并把成因与建议存进备注

文章正文
- 早期 seed 的 4 篇文章 Content 全空,点开只是打开关联记录弹层,内容运营等于白做
- 补写 4 篇真实正文(疫苗驱虫/软便判断/换粮方案/养宠花销),遵循不诊断、
  异常必提就医的护栏;对已存在的空正文做幂等回填
- 新增文章详情页,学习页与首页卡片改为打开正文

提醒管理
- 后端 CRUD 早已具备但小程序只读,用户改不了疫苗驱虫日期。提醒弹层支持增删改
- 修复后端 UpdateReminder 漏掉 type 字段导致改类型不生效

「我的」页 7 个死入口接活
- 提醒设置、健康记录、多宠物管理、会员权益分别落到对应功能
- 数据导出:汇总档案与记录为文本,可复制发给医生
- 意见反馈:新增 feedback 表与提交接口,后台加「意见反馈」管理页
- 关于我们:应用说明与免责声明

分享
- 各页面补 onShareAppMessage/onShareTimeline
- 帖子分享按钮改用微信原生 open-type="share",删除原来点了没反应的空壳弹层

社区关注
- 新增 follows 表与关注/取关接口,「关注」tab 此前等同「推荐」,
  现只显示已关注用户的帖子;帖子头部可直接关注,标记 followed / is_self

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:11:08 +08:00
Blizzard 609f7d06cf init: 毛孩子计划 小程序 + Go 后端 + 内嵌后台
- pets-fe: 微信原生小程序(首页/计划/记录/报告/社区/引导),
  服务端驱动、无假数据;弹层改用 scroll-view,打开时隐藏自定义 tabBar
- pets-be: Gin + GORM(MySQL, sundynix_ 前缀) + MinIO,统一响应/分页,
  微信 code2session 登录,provider-neutral AI(DeepSeek),go:embed React 后台
- 修复:分段选择类型不匹配(字符串 vs 数字)导致选不中

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:36:55 +08:00