refactor(be): 养护模板也从代码里移出,只从数据库读
和文章同一个理由:模板是运营内容,不是代码。留一套内置默认还额外带来 一个坏处——库里配的和代码里写的会悄悄分叉,出问题时分不清用户拿到的 到底是哪一套。 删掉: - builtinCat / builtinDog / builtinCareItems(8 套 × 96 条) - SeedCareTemplates(启动时写内置默认) - ResetCareTemplate + 后台「恢复默认」按钮 —— 已经没有默认可恢复 careTemplateItems 现在查不到就返回空。这意味着后台把某个组合配空了, 新用户会拿到一个什么都没有的首页而且不报错,所以 seedPetDefaults 里 加了一条 log:模板为空时打出 species/stage/petID,否则没人能发现。 「AI 生成草稿」原来在 AI 未开启或调用失败时静默返回内置默认,看起来 像是 AI 生成的。现在直接报错说清原因,让管理员知道要么开 AI 要么手填。 数据在库里,删代码不影响:预生产 8 套 96 条原样在,建档端到端复测通过 (3 月龄幼猫仍拿到「下一针疫苗·21 天后」,成年犬仍有疫苗驱虫提醒)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,9 +40,6 @@ func main() {
|
|||||||
log.Printf("AI provider: %s", engine.ProviderName())
|
log.Printf("AI provider: %s", engine.ProviderName())
|
||||||
|
|
||||||
svc := service.New(db, st, cfg, engine)
|
svc := service.New(db, st, cfg, engine)
|
||||||
if err := svc.SeedCareTemplates(); err != nil {
|
|
||||||
log.Fatalf("seed care templates: %v", err)
|
|
||||||
}
|
|
||||||
if err := svc.SeedCommunityBot(); err != nil {
|
if err := svc.SeedCommunityBot(); err != nil {
|
||||||
log.Fatalf("seed community bot: %v", err)
|
log.Fatalf("seed community bot: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>admin</title>
|
<title>admin</title>
|
||||||
<script type="module" crossorigin src="/admin/assets/index-nel7hMHL.js"></script>
|
<script type="module" crossorigin src="/admin/assets/index-GGI_ie1V.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BjLxu16U.css">
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-BjLxu16U.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -325,23 +325,6 @@ func (h *Handler) AdminSaveCareTemplate(c *gin.Context) {
|
|||||||
response.OK(c, data)
|
response.OK(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminResetCareTemplate POST /api/admin/care-templates/reset
|
|
||||||
// 把某组合恢复成内置默认。启动时的 seed 只补缺失、不覆盖已有,
|
|
||||||
// 想把新版内置模板推到线上必须走这个显式动作。
|
|
||||||
func (h *Handler) AdminResetCareTemplate(c *gin.Context) {
|
|
||||||
var req careGenReq
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.FailParams(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items, err := h.svc.ResetCareTemplate(req.Species, req.Stage)
|
|
||||||
if err != nil {
|
|
||||||
response.FailErr(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.OK(c, gin.H{"species": req.Species, "stage": req.Stage, "items": items})
|
|
||||||
}
|
|
||||||
|
|
||||||
type careGenReq struct {
|
type careGenReq struct {
|
||||||
Species string `json:"species"`
|
Species string `json:"species"`
|
||||||
Stage string `json:"stage"`
|
Stage string `json:"stage"`
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ func registerAdminAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manag
|
|||||||
g.GET("/care-templates", h.AdminGetCareTemplate)
|
g.GET("/care-templates", h.AdminGetCareTemplate)
|
||||||
g.PUT("/care-templates", h.AdminSaveCareTemplate)
|
g.PUT("/care-templates", h.AdminSaveCareTemplate)
|
||||||
g.POST("/care-templates/generate", h.AdminGenerateCareTemplate)
|
g.POST("/care-templates/generate", h.AdminGenerateCareTemplate)
|
||||||
g.POST("/care-templates/reset", h.AdminResetCareTemplate)
|
|
||||||
|
|
||||||
g.GET("/community-bot", h.AdminGetCommunityBot)
|
g.GET("/community-bot", h.AdminGetCommunityBot)
|
||||||
g.PUT("/community-bot", h.AdminSaveCommunityBot)
|
g.PUT("/community-bot", h.AdminSaveCommunityBot)
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ func monthsSince(t time.Time) int {
|
|||||||
return m
|
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 由宠物类型(猫猫/狗狗)映射到模板物种键
|
// SpeciesOf 由宠物类型(猫猫/狗狗)映射到模板物种键
|
||||||
func SpeciesOf(petType string) string {
|
func SpeciesOf(petType string) string {
|
||||||
if strings.Contains(petType, "狗") {
|
if strings.Contains(petType, "狗") {
|
||||||
@@ -80,16 +92,17 @@ func validStage(s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// careTemplateItems 取某组合的模板明细:DB 优先,无则回退内置默认
|
// careTemplateItems 取某组合的模板明细。模板是后台配的内容数据,代码里不再留
|
||||||
|
// 一套内置默认——没配就是没配,这里返回空,由调用方决定怎么处理(见 seedPetDefaults)。
|
||||||
func (s *Service) careTemplateItems(species, stage string) []model.CareTemplateItem {
|
func (s *Service) careTemplateItems(species, stage string) []model.CareTemplateItem {
|
||||||
var tpl model.CareTemplate
|
var tpl model.CareTemplate
|
||||||
err := s.db.
|
err := s.db.
|
||||||
Preload("Items", func(db *gorm.DB) *gorm.DB { return db.Order("kind, sort, id") }).
|
Preload("Items", func(db *gorm.DB) *gorm.DB { return db.Order("kind, sort, id") }).
|
||||||
Where("species = ? AND stage = ?", species, stage).First(&tpl).Error
|
Where("species = ? AND stage = ?", species, stage).First(&tpl).Error
|
||||||
if err == nil && len(tpl.Items) > 0 {
|
if err != nil {
|
||||||
return tpl.Items
|
return nil
|
||||||
}
|
}
|
||||||
return builtinCareItems(species, stage)
|
return tpl.Items
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCareTemplate 后台读取:返回某组合的明细(DB 优先,无则内置默认)
|
// GetCareTemplate 后台读取:返回某组合的明细(DB 优先,无则内置默认)
|
||||||
@@ -150,7 +163,7 @@ func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTempl
|
|||||||
return nil, errors.New("invalid species or stage")
|
return nil, errors.New("invalid species or stage")
|
||||||
}
|
}
|
||||||
if !s.ai.Enabled() {
|
if !s.ai.Enabled() {
|
||||||
return builtinCareItems(species, stage), nil
|
return nil, errors.New("AI 未开启,无法生成草稿;请手动添加明细")
|
||||||
}
|
}
|
||||||
spName := "猫"
|
spName := "猫"
|
||||||
if species == model.SpeciesDog {
|
if species == model.SpeciesDog {
|
||||||
@@ -170,11 +183,11 @@ func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTempl
|
|||||||
|
|
||||||
out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: user}}, ai.Options{JSON: true, Temperature: -1})
|
out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: user}}, ai.Options{JSON: true, Temperature: -1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return builtinCareItems(species, stage), nil
|
return nil, fmt.Errorf("AI 生成失败:%w", err)
|
||||||
}
|
}
|
||||||
items, ok := parseCareItems(out)
|
items, ok := parseCareItems(out)
|
||||||
if !ok || len(items) == 0 {
|
if !ok || len(items) == 0 {
|
||||||
return builtinCareItems(species, stage), nil
|
return nil, errors.New("AI 返回的内容解析不出可用明细,请重试或手动添加")
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
@@ -219,42 +232,7 @@ func parseCareItems(out string) ([]model.CareTemplateItem, bool) {
|
|||||||
return items, len(items) > 0
|
return items, len(items) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedCareTemplates 启动幂等 seed:库中无任何模板时写入 8 套内置默认
|
// ---- 明细构造小工具(供 AI 草稿解析用)----
|
||||||
// 按组合补齐:只创建缺失的那几套,已有的(可能被管理员改过)一律不动。
|
|
||||||
// 想把内置默认推到已有组合上,走后台的「恢复默认」——那是显式操作,不该由启动时偷偷覆盖。
|
|
||||||
func (s *Service) SeedCareTemplates() error {
|
|
||||||
for _, sp := range CareSpecies {
|
|
||||||
for _, stage := range CareStages {
|
|
||||||
var count int64
|
|
||||||
if err := s.db.Model(&model.CareTemplate{}).
|
|
||||||
Where("species = ? AND stage = ?", sp, stage).Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tpl := model.CareTemplate{Species: sp, Stage: stage, Items: builtinCareItems(sp, stage)}
|
|
||||||
if err := s.db.Create(&tpl).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetCareTemplate 把某个组合恢复成内置默认(后台按钮触发)
|
|
||||||
func (s *Service) ResetCareTemplate(species, stage string) ([]model.CareTemplateItem, error) {
|
|
||||||
if !validSpecies(species) || !validStage(stage) {
|
|
||||||
return nil, errors.New("物种或阶段不合法")
|
|
||||||
}
|
|
||||||
items := builtinCareItems(species, stage)
|
|
||||||
if err := s.SaveCareTemplate(species, stage, items); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s.careTemplateItems(species, stage), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 明细构造小工具 ----
|
|
||||||
|
|
||||||
func careTask(title, desc, priority, sheet string) model.CareTemplateItem {
|
func careTask(title, desc, priority, sheet string) model.CareTemplateItem {
|
||||||
return model.CareTemplateItem{Kind: model.CareKindTask, Title: title, Description: desc, Priority: priority, SheetType: sheet}
|
return model.CareTemplateItem{Kind: model.CareKindTask, Title: title, Description: desc, Priority: priority, SheetType: sheet}
|
||||||
@@ -265,166 +243,3 @@ func carePlan(day int, label, title, desc, sheet string) model.CareTemplateItem
|
|||||||
func careReminder(rtype, title string, offset int, freq string) model.CareTemplateItem {
|
func careReminder(rtype, title string, offset int, freq string) model.CareTemplateItem {
|
||||||
return model.CareTemplateItem{Kind: model.CareKindReminder, ReminderType: rtype, Title: title, OffsetDays: offset, Frequency: freq}
|
return model.CareTemplateItem{Kind: model.CareKindReminder, ReminderType: rtype, Title: title, OffsetDays: offset, Frequency: freq}
|
||||||
}
|
}
|
||||||
|
|
||||||
// builtinCareItems 内置默认模板(猫/狗 × 4 阶段,共 8 套)。
|
|
||||||
// 兽医常识向:狗每个阶段都含遛狗/牵引,猫含猫砂盆/梳毛/饮水;疫苗驱虫节奏按物种阶段区分。
|
|
||||||
// builtinCareItems 内置默认养护模板(猫/狗 × 4 组,共 8 套)。
|
|
||||||
//
|
|
||||||
// 内容依据主流小动物临床共识(WSAVA 疫苗指南、ESCCAP/CAPC 驱虫建议、AAHA
|
|
||||||
// 生命阶段指南)与国内实际情况整理:
|
|
||||||
// - 猫三联(FVRCP) 幼猫共 3 针,8-9 周龄起,每针间隔 3-4 周,末针不早于 16 周龄
|
|
||||||
// - 犬联苗幼犬 2-3 针,6-8 周龄起,间隔 3-4 周,末针不早于 16 周龄;
|
|
||||||
// 狂犬 3 月龄首针、其后每年一次(国内为法定要求)
|
|
||||||
// - 体内驱虫:6 月龄前每月一次,之后每 3 个月一次;体外每月一次(随环境调整)
|
|
||||||
// - 猫 12 月龄成年、7 岁进入老年;犬 12 月龄成年(大型犬 18-24 月)、7 岁进入老年
|
|
||||||
//
|
|
||||||
// 「刚到家 0-30 天」不是年龄阶段,而是叠加包:建档时按年龄阶段取一套,
|
|
||||||
// 若到家不足 30 天,再把这一套的任务和计划叠加上去(见 seedPetDefaults)。
|
|
||||||
func builtinCareItems(species, stage string) []model.CareTemplateItem {
|
|
||||||
if species == model.SpeciesDog {
|
|
||||||
return builtinDog(stage)
|
|
||||||
}
|
|
||||||
return builtinCat(stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func builtinCat(stage string) []model.CareTemplateItem {
|
|
||||||
switch stage {
|
|
||||||
case StageNewHome:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("记录一次体重", "到家当天称一次做基线,之后每周一次看趋势", "重要", "weight"),
|
|
||||||
careTask("保持原来的猫粮", "换环境已经够折腾了,粮先别换;要换也要 7 天过渡", "重要", "food"),
|
|
||||||
careTask("清理猫砂盆并观察", "应激性软便很常见,超过 3 天不好转要就医", "", "poop"),
|
|
||||||
careTask("陪玩 15 分钟", "用逗猫棒隔着距离玩,头两天别强行抱它", "", ""),
|
|
||||||
carePlan(0, "今天", "布置一个能躲的角落", "猫砂盆、食盆、水碗分开放;纸箱或猫窝给它安全感", ""),
|
|
||||||
carePlan(1, "第2天", "先只开放一个房间", "空间由小到大,全屋乱窜会加重应激", ""),
|
|
||||||
carePlan(3, "第3天", "观察食欲与排便", "肯吃、肯拉、肯出来走动,就是适应良好", "poop"),
|
|
||||||
carePlan(7, "第7天", "第一次正式称重", "作为之后所有体重趋势的基线", "weight"),
|
|
||||||
carePlan(14, "第14天", "预约首次体检与驱虫", "带一份新鲜粪便去,可以顺便查体内寄生虫", "vaccine"),
|
|
||||||
carePlan(30, "第30天", "确认免疫计划已排好", "把三针联苗的时间点定下来,别拖到成年", "vaccine"),
|
|
||||||
}
|
|
||||||
case StageJuvenile:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("记录一次体重", "幼猫每周称 1-2 次,稳定增重比单次数字重要", "重要", "weight"),
|
|
||||||
careTask("定时定量喂食", "3-6 月龄每天 3-4 餐,6 月龄后 2-3 餐,自由采食容易过胖", "", "food"),
|
|
||||||
careTask("检查免疫进度", "猫三联共 3 针,每针间隔 3-4 周,末针不早于 16 周龄", "重要", "vaccine"),
|
|
||||||
careTask("陪玩与社会化", "每天 2 次各 10-15 分钟;多摸爪子和耳朵,长大好剪指甲", "", ""),
|
|
||||||
carePlan(0, "今天", "对照月龄核免疫和驱虫", "算清楚还差几针、下次驱虫什么时候", "vaccine"),
|
|
||||||
carePlan(3, "第3天", "换粮观察", "换粮按 7 天过渡,每天替换约 1/7,软便就放慢", "food"),
|
|
||||||
carePlan(7, "第7天", "体重趋势检查", "掉秤或长期不增重要查球虫、寄生虫", "weight"),
|
|
||||||
carePlan(14, "第14天", "体内外驱虫", "6 月龄前体内每月一次,体外按环境每月一次", "medicine"),
|
|
||||||
carePlan(21, "第21天", "养成梳毛和剪指甲的习惯", "从小做,成年后不抗拒;顺便摸一遍有没有硬块", ""),
|
|
||||||
carePlan(30, "第30天", "月度复盘 + 绝育咨询", "6-8 月龄可以和医生聊绝育时间", "report"),
|
|
||||||
careReminder("vaccine", "下一针猫三联", 21, "幼猫共 3 针,间隔 3-4 周"),
|
|
||||||
careReminder("deworm", "体内外驱虫", 30, "6 月龄前每月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每周 1-2 次"),
|
|
||||||
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
case StageSenior:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("记录一次体重", "老年猫掉秤常是甲亢或慢性肾病的第一个信号", "重要", "weight"),
|
|
||||||
careTask("关注饮水量与尿量", "又喝又尿是慢性肾病的典型表现,比食欲变化更早出现", "重要", "symptom"),
|
|
||||||
careTask("观察进食与口腔", "牙口疼会让它挑软食甚至不吃,别只当成挑嘴", "", "food"),
|
|
||||||
carePlan(0, "今天", "老年状态整体自查", "精神、食欲、饮水、体重、被毛过一遍", ""),
|
|
||||||
carePlan(7, "第7天", "体重与饮水一起记", "两条曲线放在一起看,慢病能早发现几个月", "weight"),
|
|
||||||
carePlan(14, "第14天", "整理用药与体检报告", "长期药别断,检查单留好方便对比", "medicine"),
|
|
||||||
carePlan(21, "第21天", "关节与活动观察", "跳不上高处、理毛变少、猫砂盆边缘迈不过去都要留意", "symptom"),
|
|
||||||
carePlan(30, "第30天", "安排半年一次体检", "老年猫建议每 6-12 个月一次,含血检和尿检", "report"),
|
|
||||||
careReminder("vaccine", "年度疫苗加强", 0, "每年一次"),
|
|
||||||
careReminder("deworm", "体内驱虫", 90, "每 3 个月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每两周一次"),
|
|
||||||
careReminder("monthlyReport", "月度报告 / 半年体检", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
default: // 成年期
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("清理猫砂盆并观察", "公猫尿闭是急症,24 小时排不出尿必须马上就医", "重要", "poop"),
|
|
||||||
careTask("检查饮水情况", "吃干粮更要多喝水;流动水或多放几个水碗都管用", "", "symptom"),
|
|
||||||
careTask("梳毛", "每周 2-3 次,减少毛球,也是摸有没有硬块的机会", "", ""),
|
|
||||||
carePlan(0, "今天", "全面健康自查", "体重、精神、食欲、饮水、被毛、口腔过一遍", ""),
|
|
||||||
carePlan(7, "第7天", "体重记录", "成年猫每月一次即可;超重是绝大多数慢病的起点", "weight"),
|
|
||||||
carePlan(14, "第14天", "口腔检查", "牙龈红肿、口臭明显,该洗牙了", "symptom"),
|
|
||||||
carePlan(21, "第21天", "体外驱虫", "只在室内也可能被带进来的跳蚤找上", "medicine"),
|
|
||||||
carePlan(30, "第30天", "生成月度报告", "回顾本月记录,导出成长卡片", "report"),
|
|
||||||
careReminder("vaccine", "年度疫苗加强", 0, "每年一次"),
|
|
||||||
careReminder("deworm", "体内驱虫", 90, "每 3 个月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每月一次"),
|
|
||||||
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func builtinDog(stage string) []model.CareTemplateItem {
|
|
||||||
switch stage {
|
|
||||||
case StageNewHome:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("记录一次体重", "到家当天称一次做基线,之后每周一次", "重要", "weight"),
|
|
||||||
careTask("保持原来的狗粮", "换粮和换环境别撞在一起;要换按 7 天过渡", "重要", "food"),
|
|
||||||
careTask("定点排便训练", "固定位置、成功就立刻表扬;事后惩罚只会让它躲着拉", "", "poop"),
|
|
||||||
careTask("先在室内熟悉环境", "疫苗没打全之前不要去公共遛狗区和宠物店", "", ""),
|
|
||||||
carePlan(0, "今天", "安顿:食盆、水碗、窝、厕所", "位置固定下来,别频繁挪动", ""),
|
|
||||||
carePlan(1, "第2天", "适应牵引绳", "先在家戴着走两圈,出门前就该习惯", ""),
|
|
||||||
carePlan(3, "第3天", "观察进食与排便", "应激期软便常见,精神差、带血要就医", "poop"),
|
|
||||||
carePlan(7, "第7天", "第一次正式称重", "作为之后所有体重趋势的基线", "weight"),
|
|
||||||
carePlan(14, "第14天", "预约疫苗与体检", "确认联苗针次,3 月龄以上要安排狂犬", "vaccine"),
|
|
||||||
carePlan(30, "第30天", "免疫打全后开始户外散步", "从安静路段开始,逐步接触人、车、其它狗", ""),
|
|
||||||
}
|
|
||||||
case StageJuvenile:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("牵引散步与社会化", "3-16 周是社会化关键期;疫苗未全时可抱着或在自家院子接触外界", "重要", ""),
|
|
||||||
careTask("记录一次体重", "幼犬长得快,每周称 1-2 次", "重要", "weight"),
|
|
||||||
careTask("定时定量喂食", "3-6 月龄每天 3 餐,6 月龄后 2 餐", "", "food"),
|
|
||||||
careTask("基础训练 10 分钟", "坐下、召回、不咬手;短时高频比一次练很久有效", "", ""),
|
|
||||||
carePlan(0, "今天", "核对免疫进度", "联苗 2-3 针间隔 3-4 周,3 月龄安排狂犬", "vaccine"),
|
|
||||||
carePlan(3, "第3天", "换粮观察", "7 天过渡,软便就放慢速度", "food"),
|
|
||||||
carePlan(7, "第7天", "体重趋势检查", "增长停滞要查寄生虫和喂食量", "weight"),
|
|
||||||
carePlan(14, "第14天", "体内外驱虫", "6 月龄前体内每月一次,体外每月一次", "medicine"),
|
|
||||||
carePlan(21, "第21天", "巩固定点排便与独处训练", "从 5 分钟开始拉长独处时间,预防分离焦虑", ""),
|
|
||||||
carePlan(30, "第30天", "月度复盘 + 绝育咨询", "小型犬 6-9 月龄,大型犬建议 12-18 月龄再做,等骨骼长好", "report"),
|
|
||||||
careReminder("vaccine", "下一针联苗 / 狂犬", 21, "联苗间隔 3-4 周,3 月龄起狂犬"),
|
|
||||||
careReminder("deworm", "体内外驱虫", 30, "6 月龄前每月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每周 1-2 次"),
|
|
||||||
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
case StageSenior:
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("温和遛狗", "缩短单次时长、增加次数;避免跳跃和频繁爬楼梯", "重要", ""),
|
|
||||||
careTask("记录一次体重", "骤增骤减都要重视,老年犬掉秤要查甲状腺和肿瘤", "重要", "weight"),
|
|
||||||
careTask("观察关节与精神", "起身困难、后腿打滑、不愿上车,都是关节问题的早期表现", "", "symptom"),
|
|
||||||
carePlan(0, "今天", "老年状态整体自查", "精神、食欲、饮水、关节活动一起看", ""),
|
|
||||||
carePlan(7, "第7天", "体重与饮水一起记", "多饮多尿要查肾和血糖", "weight"),
|
|
||||||
carePlan(14, "第14天", "整理用药与体检报告", "长期药别断,检查单留好方便对比", "medicine"),
|
|
||||||
carePlan(21, "第21天", "口腔与体表触诊", "牙结石、乳腺和体表肿物,摸一遍心里有数", "symptom"),
|
|
||||||
carePlan(30, "第30天", "安排半年一次体检", "老年犬建议每 6 个月一次,含血检和胸腹部检查", "report"),
|
|
||||||
careReminder("vaccine", "年度疫苗 + 狂犬加强", 0, "每年一次,狂犬为法定要求"),
|
|
||||||
careReminder("deworm", "体内驱虫", 90, "每 3 个月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每两周一次"),
|
|
||||||
careReminder("monthlyReport", "月度报告 / 半年体检", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
default: // 成年期
|
|
||||||
return []model.CareTemplateItem{
|
|
||||||
careTask("遛狗", "每天 2 次共 30-60 分钟;运动量不够是拆家和吠叫的头号原因", "重要", ""),
|
|
||||||
careTask("刷牙", "每周 2-3 次。3 岁以上的犬多数已有牙周问题", "", ""),
|
|
||||||
careTask("记录一次体重", "每月一次。摸得到肋骨但看不见,是标准体型", "", "weight"),
|
|
||||||
carePlan(0, "今天", "全面健康自查", "体重、精神、食欲、被毛、指甲、耳道过一遍", ""),
|
|
||||||
carePlan(7, "第7天", "体重记录", "对比上月,稳定为宜", "weight"),
|
|
||||||
carePlan(14, "第14天", "体外驱虫", "遛狗接触草地,蜱虫和跳蚤风险比猫高得多", "medicine"),
|
|
||||||
carePlan(21, "第21天", "指甲与耳道检查", "指甲过长会影响步态;垂耳犬更容易耳道发炎", "symptom"),
|
|
||||||
carePlan(30, "第30天", "生成月度报告", "回顾本月记录,导出成长卡片", "report"),
|
|
||||||
careReminder("vaccine", "年度疫苗 + 狂犬加强", 0, "每年一次,狂犬为法定要求"),
|
|
||||||
careReminder("deworm", "体内驱虫", 90, "每 3 个月一次"),
|
|
||||||
careReminder("weight", "体重记录", 0, "每月一次"),
|
|
||||||
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NormalizeStage 定下最终使用的年龄阶段。
|
|
||||||
// 用户手选优先(他可能比生日更清楚,比如领养来的估算年龄),
|
|
||||||
// 但只认三个年龄阶段;传了「刚到家」或空值时,一律按生日推断。
|
|
||||||
func NormalizeStage(species, picked string, birthday *time.Time) string {
|
|
||||||
for _, s := range AgeStages {
|
|
||||||
if picked == s {
|
|
||||||
return picked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return StageFromBirthday(species, birthday)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -150,6 +151,12 @@ func (s *Service) seedPetDefaults(tx *gorm.DB, pet *model.Pet) error {
|
|||||||
today := time.Now()
|
today := time.Now()
|
||||||
species := SpeciesOf(pet.Type)
|
species := SpeciesOf(pet.Type)
|
||||||
items := s.careTemplateItems(species, pet.Stage)
|
items := s.careTemplateItems(species, pet.Stage)
|
||||||
|
// 模板是后台配的,代码里没有兜底。配空了新用户会拿到一个什么都没有的
|
||||||
|
// 首页,而且不报错——所以这里必须留下痕迹,否则没人能发现。
|
||||||
|
if len(items) == 0 {
|
||||||
|
log.Printf("care: 养护模板为空 species=%s stage=%s,新宠 %s 不会生成任务/计划/提醒,请到后台配置",
|
||||||
|
species, pet.Stage, pet.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// 刚到家不到 30 天,把安置期那套的任务和计划叠加上来。
|
// 刚到家不到 30 天,把安置期那套的任务和计划叠加上来。
|
||||||
// 「刚到家」和「幼年/成年/老年」是两个正交的维度,原来挤在一个四选一里,
|
// 「刚到家」和「幼年/成年/老年」是两个正交的维度,原来挤在一个四选一里,
|
||||||
|
|||||||
@@ -127,11 +127,6 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
saveCareTemplate: (species: string, stage: string, items: any[]) =>
|
saveCareTemplate: (species: string, stage: string, items: any[]) =>
|
||||||
http.put<any, { items: any[] }>('/admin/care-templates', { species, stage, items }),
|
http.put<any, { items: any[] }>('/admin/care-templates', { species, stage, items }),
|
||||||
resetCareTemplate: (species: string, stage: string) =>
|
|
||||||
http.post<any, { species: string; stage: string; items: any[] }>('/admin/care-templates/reset', {
|
|
||||||
species,
|
|
||||||
stage,
|
|
||||||
}),
|
|
||||||
genCareTemplate: (species: string, stage: string) =>
|
genCareTemplate: (species: string, stage: string) =>
|
||||||
http.post<any, { species: string; stage: string; items: any[] }>('/admin/care-templates/generate', {
|
http.post<any, { species: string; stage: string; items: any[] }>('/admin/care-templates/generate', {
|
||||||
species,
|
species,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Cat, Dog, Wand2, Plus, Trash2, Save, Info, RotateCcw } from 'lucide-react'
|
import { Cat, Dog, Wand2, Plus, Trash2, Save, Info } from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -106,24 +106,6 @@ export default function CareTemplates() {
|
|||||||
setDirty(true)
|
setDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复内置默认。启动时的 seed 只补缺失、不覆盖已有,
|
|
||||||
// 所以内置模板更新后必须从这里显式推一次
|
|
||||||
async function reset() {
|
|
||||||
if (!confirm(`把【${species === 'dog' ? '狗' : '猫'} · ${stage}】恢复成内置默认?当前改动会被覆盖。`)) return
|
|
||||||
setGen(true)
|
|
||||||
setMsg('')
|
|
||||||
try {
|
|
||||||
const data = await api.resetCareTemplate(species, stage)
|
|
||||||
setItems(normalize(data.items))
|
|
||||||
setDirty(false)
|
|
||||||
setMsg('已恢复为内置默认模板')
|
|
||||||
} catch (e: any) {
|
|
||||||
setMsg(e.message || '恢复失败')
|
|
||||||
} finally {
|
|
||||||
setGen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generate() {
|
async function generate() {
|
||||||
setGen(true)
|
setGen(true)
|
||||||
setMsg('')
|
setMsg('')
|
||||||
@@ -167,9 +149,6 @@ export default function CareTemplates() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" onClick={reset} disabled={gen}>
|
|
||||||
<RotateCcw className="h-4 w-4" /> 恢复默认
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={generate} disabled={gen}>
|
<Button variant="outline" onClick={generate} disabled={gen}>
|
||||||
<Wand2 className="h-4 w-4" /> {gen ? '生成中…' : 'AI 生成草稿'}
|
<Wand2 className="h-4 w-4" /> {gen ? '生成中…' : 'AI 生成草稿'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user