28b95a6468
后端 - 每日任务惰性生成:某天为空则从最近一天顺延复制(done 重置),首天用模板 - 任务增删改 API(POST /pets/:id/tasks、PUT/DELETE /tasks/:id),改动自动顺延 - 30 天计划到期自动归档并按当前阶段生成新一轮 - 内置养护模板重写为兽医常识向:狗每阶段含遛狗/牵引/狂犬,猫含猫砂/梳毛/饮水 - 文件 URL 改为按 object_name + 当前配置动态拼接,换 IP/域名不再有旧地址 小程序 - 首页今日任务加「管理」入口:增删改任务弹层 - 记录时间轴显示照片缩略图(可全屏预览)+ 拍照成功提示 - 社区发布条精简为单个「发布图文」按钮,去掉头像/输入框误触 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
338 lines
15 KiB
Go
338 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/sundynix/pets-be/internal/ai"
|
|
"github.com/sundynix/pets-be/internal/model"
|
|
)
|
|
|
|
// CareSpecies / CareStages 支持的物种与阶段(阶段字符串与 onboarding 一致)
|
|
var CareSpecies = []string{model.SpeciesCat, model.SpeciesDog}
|
|
var CareStages = []string{"刚到家 0-30 天", "幼年期", "成年期", "老年期"}
|
|
|
|
// SpeciesOf 由宠物类型(猫猫/狗狗)映射到模板物种键
|
|
func SpeciesOf(petType string) string {
|
|
if strings.Contains(petType, "狗") {
|
|
return model.SpeciesDog
|
|
}
|
|
return model.SpeciesCat
|
|
}
|
|
|
|
func validSpecies(s string) bool { return s == model.SpeciesCat || s == model.SpeciesDog }
|
|
func validStage(s string) bool {
|
|
for _, x := range CareStages {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// careTemplateItems 取某组合的模板明细:DB 优先,无则回退内置默认
|
|
func (s *Service) careTemplateItems(species, stage string) []model.CareTemplateItem {
|
|
var tpl model.CareTemplate
|
|
err := s.db.
|
|
Preload("Items", func(db *gorm.DB) *gorm.DB { return db.Order("kind, sort, id") }).
|
|
Where("species = ? AND stage = ?", species, stage).First(&tpl).Error
|
|
if err == nil && len(tpl.Items) > 0 {
|
|
return tpl.Items
|
|
}
|
|
return builtinCareItems(species, stage)
|
|
}
|
|
|
|
// GetCareTemplate 后台读取:返回某组合的明细(DB 优先,无则内置默认)
|
|
func (s *Service) GetCareTemplate(species, stage string) (map[string]any, error) {
|
|
if !validSpecies(species) || !validStage(stage) {
|
|
return nil, errors.New("invalid species or stage")
|
|
}
|
|
return map[string]any{
|
|
"species": species,
|
|
"stage": stage,
|
|
"items": s.careTemplateItems(species, stage),
|
|
}, nil
|
|
}
|
|
|
|
// SaveCareTemplate 后台保存:整表替换该组合的明细
|
|
func (s *Service) SaveCareTemplate(species, stage string, items []model.CareTemplateItem) error {
|
|
if !validSpecies(species) || !validStage(stage) {
|
|
return errors.New("invalid species or stage")
|
|
}
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
var tpl model.CareTemplate
|
|
err := tx.Where("species = ? AND stage = ?", species, stage).First(&tpl).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
tpl = model.CareTemplate{Species: species, Stage: stage}
|
|
if err := tx.Create(&tpl).Error; err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("template_id = ?", tpl.ID).Delete(&model.CareTemplateItem{}).Error; err != nil {
|
|
return err
|
|
}
|
|
clean := make([]model.CareTemplateItem, 0, len(items))
|
|
for i := range items {
|
|
it := items[i]
|
|
if strings.TrimSpace(it.Title) == "" {
|
|
continue
|
|
}
|
|
it.ID = ""
|
|
it.TemplateID = tpl.ID
|
|
it.Sort = i
|
|
clean = append(clean, it)
|
|
}
|
|
if len(clean) > 0 {
|
|
if err := tx.Create(&clean).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GenerateCareTemplate 后台「AI 生成草稿」:不落库,仅返回建议明细。
|
|
// AI 未开启或失败时回退内置默认,保证总有可用草稿。
|
|
func (s *Service) GenerateCareTemplate(species, stage string) ([]model.CareTemplateItem, error) {
|
|
if !validSpecies(species) || !validStage(stage) {
|
|
return nil, errors.New("invalid species or stage")
|
|
}
|
|
if !s.ai.Enabled() {
|
|
return builtinCareItems(species, stage), nil
|
|
}
|
|
spName := "猫"
|
|
if species == model.SpeciesDog {
|
|
spName = "狗"
|
|
}
|
|
system := aiSafetyPrompt + `
|
|
|
|
你现在为后台生成一套「养护模板」,用于新宠建档时自动生成任务/计划/提醒。
|
|
只输出 JSON,结构如下(不要多余文字):
|
|
{
|
|
"tasks": [{"title":"","description":"","priority":"重要或空","sheet_type":"weight|poop|food|symptom|vaccine|medicine 或空"}],
|
|
"plan": [{"day":0,"day_label":"今天","title":"","description":"","sheet_type":"weight|vaccine|medicine|report|taskDetail 或空"}],
|
|
"reminders":[{"reminder_type":"vaccine|deworm|weight|monthlyReport","title":"","offset_days":18,"frequency":"到期用天数则填0并写频率,如每周二五"}]
|
|
}
|
|
要求:tasks 3 条、plan 4 条、reminders 3-4 条;贴合该物种与阶段的真实照护节奏(如成年比幼年疫苗/驱虫间隔更长,狗驱虫比猫更频繁)。`
|
|
user := fmt.Sprintf("为【%s · %s】生成一套养护模板。", spName, stage)
|
|
|
|
out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: user}}, ai.Options{JSON: true, Temperature: -1})
|
|
if err != nil {
|
|
return builtinCareItems(species, stage), nil
|
|
}
|
|
items, ok := parseCareItems(out)
|
|
if !ok || len(items) == 0 {
|
|
return builtinCareItems(species, stage), nil
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// parseCareItems 解析 AI 返回的分组 JSON → 扁平明细
|
|
func parseCareItems(out string) ([]model.CareTemplateItem, bool) {
|
|
var raw struct {
|
|
Tasks []struct {
|
|
Title, Description, Priority, SheetType string
|
|
} `json:"tasks"`
|
|
Plan []struct {
|
|
Day int
|
|
DayLabel, Title, Description, SheetType string
|
|
} `json:"plan"`
|
|
Reminders []struct {
|
|
ReminderType, Title, Frequency string
|
|
OffsetDays int
|
|
} `json:"reminders"`
|
|
}
|
|
if err := json.Unmarshal([]byte(extractJSON(out)), &raw); err != nil {
|
|
return nil, false
|
|
}
|
|
var items []model.CareTemplateItem
|
|
for _, t := range raw.Tasks {
|
|
if t.Title == "" {
|
|
continue
|
|
}
|
|
items = append(items, careTask(t.Title, t.Description, t.Priority, t.SheetType))
|
|
}
|
|
for _, p := range raw.Plan {
|
|
if p.Title == "" {
|
|
continue
|
|
}
|
|
items = append(items, carePlan(p.Day, p.DayLabel, p.Title, p.Description, p.SheetType))
|
|
}
|
|
for _, r := range raw.Reminders {
|
|
if r.Title == "" {
|
|
continue
|
|
}
|
|
items = append(items, careReminder(r.ReminderType, r.Title, r.OffsetDays, r.Frequency))
|
|
}
|
|
return items, len(items) > 0
|
|
}
|
|
|
|
// SeedCareTemplates 启动幂等 seed:库中无任何模板时写入 8 套内置默认
|
|
func (s *Service) SeedCareTemplates() error {
|
|
var count int64
|
|
if err := s.db.Model(&model.CareTemplate{}).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
for _, sp := range CareSpecies {
|
|
for _, stage := range CareStages {
|
|
tpl := model.CareTemplate{Species: sp, Stage: stage, Items: builtinCareItems(sp, stage)}
|
|
if err := s.db.Create(&tpl).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---- 明细构造小工具 ----
|
|
|
|
func careTask(title, desc, priority, sheet string) model.CareTemplateItem {
|
|
return model.CareTemplateItem{Kind: model.CareKindTask, Title: title, Description: desc, Priority: priority, SheetType: sheet}
|
|
}
|
|
func carePlan(day int, label, title, desc, sheet string) model.CareTemplateItem {
|
|
return model.CareTemplateItem{Kind: model.CareKindPlan, Day: day, DayLabel: label, Title: title, Description: desc, SheetType: sheet}
|
|
}
|
|
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}
|
|
}
|
|
|
|
// builtinCareItems 内置默认模板(猫/狗 × 4 阶段,共 8 套)。
|
|
// 兽医常识向:狗每个阶段都含遛狗/牵引,猫含猫砂盆/梳毛/饮水;疫苗驱虫节奏按物种阶段区分。
|
|
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 "刚到家 0-30 天":
|
|
return []model.CareTemplateItem{
|
|
careTask("记录一次体重", "到家先称个底,之后每周一次看增长", "重要", "weight"),
|
|
careTask("清理猫砂盆并观察", "换环境应激常见软便,留意颜色和次数", "", "poop"),
|
|
careTask("陪玩 15 分钟", "逗猫棒建立信任,缓解应激,别强抱", "", ""),
|
|
carePlan(0, "今天", "安顿与环境适应", "猫砂盆、食盆、水碗、猫窝固定位置,给它安静空间", "taskDetail"),
|
|
carePlan(3, "第3天", "观察进食与排便", "食欲和便便恢复正常说明适应良好", "poop"),
|
|
carePlan(7, "第7天", "第一次体重记录", "作为后续趋势基线", "weight"),
|
|
carePlan(14, "第14天", "预约首次疫苗/体检", "确认医院、驱虫和免疫计划", "vaccine"),
|
|
careReminder("vaccine", "首次疫苗接种", 14, ""),
|
|
careReminder("deworm", "首次体内外驱虫", 7, ""),
|
|
careReminder("weight", "体重记录", 0, "每周一次"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
case "幼年期":
|
|
return []model.CareTemplateItem{
|
|
careTask("记录一次体重", "幼猫长得快,每周记 2 次", "重要", "weight"),
|
|
careTask("梳毛并陪玩", "每天梳毛+两次互动,帮助社会化、减少毛球", "", ""),
|
|
careTask("检查疫苗计划", "幼猫三针联苗按时打,别漏针", "重要", "vaccine"),
|
|
carePlan(0, "今天", "确认免疫与驱虫计划", "对照月龄核对疫苗针次和驱虫时间", "vaccine"),
|
|
carePlan(3, "第3天", "观察饮水与排便", "换粮期留意软便,7 天过渡换粮", "poop"),
|
|
carePlan(7, "第7天", "体重趋势检查", "每周称重,稳定增长是关键指标", "weight"),
|
|
carePlan(14, "第14天", "驱虫与饮食复盘", "确认驱虫到期、复盘换粮情况", "medicine"),
|
|
careReminder("vaccine", "第 2 针疫苗", 18, ""),
|
|
careReminder("deworm", "体内外驱虫", 30, ""),
|
|
careReminder("weight", "体重记录", 0, "每周二、周五"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
case "老年期":
|
|
return []model.CareTemplateItem{
|
|
careTask("记录一次体重", "老年猫消瘦要警惕,别忽视", "重要", "weight"),
|
|
careTask("关注饮水量与排尿", "多饮多尿常是肾病/糖尿信号,异常尽快就医", "重要", "symptom"),
|
|
careTask("清理猫砂盆并观察", "关注尿量、尿色和排尿是否困难", "", "poop"),
|
|
carePlan(0, "今天", "老年健康自查", "精神、食欲、饮水、体重整体过一遍", "taskDetail"),
|
|
carePlan(7, "第7天", "体重与饮水记录", "两项一起看,慢病早发现", "weight"),
|
|
carePlan(14, "第14天", "用药与慢病回顾", "整理长期用药和体检结果", "medicine"),
|
|
carePlan(30, "第30天", "月度报告 / 建议体检", "老年猫建议每半年一次体检", "report"),
|
|
careReminder("vaccine", "年度疫苗加强", 30, ""),
|
|
careReminder("deworm", "体内外驱虫", 90, ""),
|
|
careReminder("weight", "体重记录", 0, "每两周一次"),
|
|
careReminder("monthlyReport", "月度报告 / 半年体检", 0, "每月 1 日"),
|
|
}
|
|
default: // 成年期
|
|
return []model.CareTemplateItem{
|
|
careTask("记录一次体重", "成年猫每月一次,防止肥胖", "", "weight"),
|
|
careTask("清理猫砂盆并观察", "泌尿问题早发现,公猫尤其注意尿闭", "", "poop"),
|
|
careTask("梳毛与口腔检查", "关注毛球、牙结石和皮肤状态", "", ""),
|
|
carePlan(0, "今天", "全面健康自查", "体重、精神、食欲、饮水、被毛过一遍", "taskDetail"),
|
|
carePlan(7, "第7天", "体重趋势检查", "对比上月,稳定为宜", "weight"),
|
|
carePlan(14, "第14天", "驱虫记录复盘", "确认体内外驱虫是否到期", "medicine"),
|
|
carePlan(30, "第30天", "生成月度报告", "回顾本月记录,导出成长卡片", "report"),
|
|
careReminder("vaccine", "年度疫苗加强", 30, ""),
|
|
careReminder("deworm", "体内外驱虫", 90, ""),
|
|
careReminder("weight", "体重记录", 0, "每月一次"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
}
|
|
}
|
|
|
|
func builtinDog(stage string) []model.CareTemplateItem {
|
|
switch stage {
|
|
case "刚到家 0-30 天":
|
|
return []model.CareTemplateItem{
|
|
careTask("记录一次体重", "到家先称个底,之后每周一次", "重要", "weight"),
|
|
careTask("定点排便训练", "固定厕所位置,成功就表扬,别惩罚", "", "poop"),
|
|
careTask("室内活动 / 自家院子", "疫苗未打全前少去人多狗多的地方,先在家熟悉环境", "", ""),
|
|
carePlan(0, "今天", "安顿与环境适应", "食盆、水碗、狗窝、厕所固定位置", "taskDetail"),
|
|
carePlan(3, "第3天", "观察进食与排便", "应激期常见软便,留意精神状态", "poop"),
|
|
carePlan(7, "第7天", "第一次体重记录", "作为后续趋势基线", "weight"),
|
|
carePlan(14, "第14天", "预约疫苗 / 体检", "确认联苗、狂犬和驱虫计划", "vaccine"),
|
|
careReminder("vaccine", "首针疫苗接种", 14, ""),
|
|
careReminder("deworm", "首次体内外驱虫", 7, ""),
|
|
careReminder("weight", "体重记录", 0, "每周一次"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
case "幼年期":
|
|
return []model.CareTemplateItem{
|
|
careTask("遛狗与基础训练", "每天 2 次,牵引、坐下、召回,社会化关键期", "重要", ""),
|
|
careTask("记录一次体重", "幼犬长得快,每周记 2 次", "重要", "weight"),
|
|
careTask("观察饮水与排便", "换粮、驱虫期留意软便", "", "poop"),
|
|
carePlan(0, "今天", "确认免疫与驱虫计划", "联苗+狂犬按月龄打,别漏针", "vaccine"),
|
|
carePlan(1, "明天", "定点排便 + 牵引训练", "打好如厕和牵引基础", "taskDetail"),
|
|
carePlan(7, "第7天", "体重趋势检查", "每周称重,稳定增长", "weight"),
|
|
carePlan(14, "第14天", "驱虫与社会化复盘", "确认驱虫到期、多接触人和环境", "medicine"),
|
|
careReminder("vaccine", "下一针联苗", 18, ""),
|
|
careReminder("deworm", "体内外驱虫", 30, ""),
|
|
careReminder("weight", "体重记录", 0, "每周二、周五"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
case "老年期":
|
|
return []model.CareTemplateItem{
|
|
careTask("温和遛狗", "缩短时长、避免剧烈跳跃,保护关节", "重要", ""),
|
|
careTask("记录一次体重", "骤增骤减都要重视", "重要", "weight"),
|
|
careTask("观察关节 / 精神 / 饮水", "后腿无力、多饮多尿要及时就医", "", "symptom"),
|
|
carePlan(0, "今天", "老年健康自查", "精神、食欲、饮水、关节活动一起看", "taskDetail"),
|
|
carePlan(7, "第7天", "体重与饮水记录", "慢病早发现", "weight"),
|
|
carePlan(14, "第14天", "用药与慢病回顾", "整理长期用药和体检结果", "medicine"),
|
|
carePlan(30, "第30天", "月度报告 / 建议体检", "老年犬建议每半年一次体检", "report"),
|
|
careReminder("vaccine", "年度疫苗 + 狂犬加强", 30, ""),
|
|
careReminder("deworm", "体内外驱虫", 60, ""),
|
|
careReminder("weight", "体重记录", 0, "每两周一次"),
|
|
careReminder("monthlyReport", "月度报告 / 半年体检", 0, "每月 1 日"),
|
|
}
|
|
default: // 成年期
|
|
return []model.CareTemplateItem{
|
|
careTask("遛狗", "每天 2 次共 30-60 分钟,消耗精力、防拆家", "重要", ""),
|
|
careTask("记录一次体重", "每月一次,控制体重防肥胖", "", "weight"),
|
|
careTask("刷牙 / 口腔检查", "每周 2-3 次,狗牙结石高发", "", ""),
|
|
carePlan(0, "今天", "全面健康自查", "体重、精神、食欲、被毛、指甲过一遍", "taskDetail"),
|
|
carePlan(7, "第7天", "体重趋势检查", "对比上月,稳定为宜", "weight"),
|
|
carePlan(14, "第14天", "驱虫记录复盘", "遛狗接触多,确认驱虫是否到期", "medicine"),
|
|
carePlan(30, "第30天", "生成月度报告", "回顾本月记录,导出成长卡片", "report"),
|
|
careReminder("vaccine", "年度疫苗 + 狂犬加强", 30, ""),
|
|
careReminder("deworm", "体内外驱虫", 60, ""),
|
|
careReminder("weight", "体重记录", 0, "每月一次"),
|
|
careReminder("monthlyReport", "月度报告", 0, "每月 1 日"),
|
|
}
|
|
}
|
|
}
|