feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
// Admin 后台管理员
|
||||
type Admin struct {
|
||||
Base
|
||||
Username string `gorm:"size:64;uniqueIndex" json:"username"`
|
||||
PasswordHash string `gorm:"size:128" json:"-"`
|
||||
Role string `gorm:"size:32;default:admin" json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package model
|
||||
|
||||
// AIMessage AI 聊天消息
|
||||
type AIMessage struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
PetID *string `gorm:"size:24" json:"pet_id"`
|
||||
Session string `gorm:"size:64;index" json:"session"`
|
||||
Role string `gorm:"size:8" json:"role"` // user / ai
|
||||
Text string `gorm:"type:text" json:"text"`
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package model
|
||||
|
||||
// AIQuotaConfig 每日 AI 次数上限(单例,固定 id=1)。
|
||||
// AI 调用是真金白银,不设上限等于把钱包交给用户。
|
||||
type AIQuotaConfig struct {
|
||||
Base
|
||||
Enabled bool `json:"enabled"` // 关掉即不限次
|
||||
DailyChat int `json:"daily_chat"` // 问问 AI,每人每天
|
||||
DailySymptom int `json:"daily_symptom"` // 异常观察评估
|
||||
DailyPlan int `json:"daily_plan"` // AI 生成计划
|
||||
}
|
||||
|
||||
// AIUsage 某人某天某类 AI 的用量。按天存,天然过期,不需要清理任务。
|
||||
type AIUsage struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;uniqueIndex:idx_user_day_kind" json:"user_id"`
|
||||
Day string `gorm:"size:10;uniqueIndex:idx_user_day_kind" json:"day"` // YYYY-MM-DD
|
||||
Kind string `gorm:"size:16;uniqueIndex:idx_user_day_kind" json:"kind"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package model
|
||||
|
||||
// Article 新手知识文章
|
||||
type Article struct {
|
||||
Base
|
||||
Icon string `gorm:"size:16" json:"icon"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Category string `gorm:"size:32" json:"category"`
|
||||
RelatedSheetType string `gorm:"size:32" json:"related_sheet_type"`
|
||||
Published bool `gorm:"default:true" json:"published"`
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/sundynix/pets-be/pkg/idgen"
|
||||
)
|
||||
|
||||
// Base 所有模型的公共字段。主键为雪花算法字符串 ID(非自增)。
|
||||
type Base struct {
|
||||
ID string `gorm:"primaryKey;size:24" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate 未显式赋值时自动生成雪花 ID(通过嵌入对所有模型生效)。
|
||||
func (b *Base) BeforeCreate(tx *gorm.DB) error {
|
||||
if b.ID == "" {
|
||||
b.ID = idgen.Next()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllModels 需要 AutoMigrate 的模型清单(按依赖顺序)
|
||||
func AllModels() []any {
|
||||
return []any{
|
||||
&User{},
|
||||
&Pet{},
|
||||
&HealthRecord{},
|
||||
&RecordType{},
|
||||
&Breed{},
|
||||
&DailyTask{},
|
||||
&Plan{},
|
||||
&PlanTask{},
|
||||
&Reminder{},
|
||||
&Post{},
|
||||
&Comment{},
|
||||
&PostLike{},
|
||||
&Article{},
|
||||
&AIMessage{},
|
||||
&ProMembership{},
|
||||
&Admin{},
|
||||
&DailyAdvice{},
|
||||
&CareTemplate{},
|
||||
&CareTemplateItem{},
|
||||
&CommunityBotConfig{},
|
||||
&File{},
|
||||
&Feedback{},
|
||||
&Follow{},
|
||||
&RefreshToken{},
|
||||
&AIQuotaConfig{},
|
||||
&AIUsage{},
|
||||
&MediaCheck{},
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package model
|
||||
|
||||
// Breed 品种。原来建档时品种是个自由文本输入框,用户手打「柯基」「柯基犬」
|
||||
// 「威尔士柯基」都算不同品种,后面想按品种做体重基准、常见病提示这类事就没法做。
|
||||
type Breed struct {
|
||||
Base
|
||||
Species string `gorm:"size:16;index" json:"species"` // cat | dog
|
||||
Name string `gorm:"size:64" json:"name"`
|
||||
|
||||
// Initial 首字母,A-Z,用来做侧边索引。
|
||||
//
|
||||
// 存字段而不是运行时算:Go 标准库没有拼音,引一个拼音库只为了取首字母不值得,
|
||||
// 而且多音字(「柴犬」的柴、「藏獒」的藏)自动转常出错——「藏獒」按 cang
|
||||
// 还是 zang 分组,人来定比库来猜靠谱。后台新增品种时手填这一位。
|
||||
Initial string `gorm:"size:1;index" json:"initial"`
|
||||
|
||||
// SizeClass 成年体型,只有犬类用:small(<10kg) / medium(10-25) / large(25-40) / giant(>40)。
|
||||
//
|
||||
// 生命阶段的判定要靠它。狗的发育完全取决于体型——大丹犬 8 个月还在长骨头,
|
||||
// 吉娃娃 8 个月已经性成熟;大丹犬 7 岁进老年,吉娃娃 7 岁还是中年。
|
||||
// 不能靠当前体重判断(幼犬体重说明不了成年后多大),只能靠品种。
|
||||
SizeClass string `gorm:"size:16;index" json:"size_class"`
|
||||
|
||||
Sort int `json:"sort"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package model
|
||||
|
||||
// 养护方案:一套「这个物种、这个阶段该做哪些事」的配置。
|
||||
// 后台可以给同一个 (species, stage) 配多套,建档时让用户挑一套套用,
|
||||
// 也可以不套用、自己排。原来是一个组合只能有一套(唯一索引卡着),
|
||||
// 「让用户选模版」这件事在数据模型上做不到。
|
||||
|
||||
// 物种取值
|
||||
const (
|
||||
SpeciesCat = "cat"
|
||||
SpeciesDog = "dog"
|
||||
)
|
||||
|
||||
// 明细的类别。原来还有个 task(今日任务),已经和 plan 合并——
|
||||
// 「今天要做的事」就是「今天那一天的计划节点」,两套并存的结果是
|
||||
// 用户在计划页加的「每天喂药」在首页看不到。
|
||||
const (
|
||||
CareKindPlan = "plan" // 计划节点(含每天/每周/每月重复)
|
||||
CareKindReminder = "reminder" // 到期提醒(疫苗、驱虫这类有固定周期的)
|
||||
)
|
||||
|
||||
// 重复规则。月度计划要的是「多久做一次」,不是「第几天做」——
|
||||
// 原来只有 Day 偏移,月底建档的话 day=14/21/30 全落到下个月,
|
||||
// 用户建完档打开计划页发现本月是空的。
|
||||
const (
|
||||
RepeatOnce = "once" // 一次性,按 Day 偏移(到家第 N 天,疫苗序列要用)
|
||||
RepeatDaily = "daily" // 每天
|
||||
RepeatWeekly = "weekly" // 每周某几天,看 Weekdays
|
||||
RepeatMonthly = "monthly" // 每月某天,看 MonthDay
|
||||
)
|
||||
|
||||
// CareTemplate 一套方案
|
||||
type CareTemplate struct {
|
||||
Base
|
||||
Species string `gorm:"size:16;index" json:"species"` // cat | dog
|
||||
Stage string `gorm:"size:48;index" json:"stage"` // 与 onboarding 阶段一致
|
||||
// Name 方案名。建档时用户看到的就是这个,要能一眼分辨
|
||||
// (「幼猫标准照护」/「幼猫省心版」)
|
||||
Name string `gorm:"size:64" json:"name"`
|
||||
// Summary 一句话摘要,选择列表里展示
|
||||
Summary string `gorm:"size:255" json:"summary"`
|
||||
Sort int `json:"sort"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
|
||||
Items []CareTemplateItem `gorm:"foreignKey:TemplateID" json:"items"`
|
||||
}
|
||||
|
||||
// CareTemplateItem 明细,kind 区分两类;不同类只用到部分字段
|
||||
type CareTemplateItem struct {
|
||||
Base
|
||||
TemplateID string `gorm:"size:24;index" json:"template_id"`
|
||||
Kind string `gorm:"size:16;index" json:"kind"` // plan | reminder
|
||||
Sort int `json:"sort"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
|
||||
// plan 专用
|
||||
Priority string `gorm:"size:16" json:"priority"` // 重要 / 普通(空)
|
||||
SheetType string `gorm:"size:32" json:"sheet_type"` // 关联记录类型 code:weight/poop/bath...
|
||||
// Repeat 列名用 repeat_rule:repeat 是 MySQL 保留字,GORM 虽然会加反引号,
|
||||
// 但手写 SQL 排查时很容易踩(和 group_key 同样的理由)
|
||||
Repeat string `gorm:"column:repeat_rule;size:16" json:"repeat"` // once | daily | weekly | monthly
|
||||
Day int `json:"day"` // once:从套用日起第 N 天(0=当天)
|
||||
// DayLabel 留着:后台的 AI 生成模板那条链路在产出它。月度计划里日期是绝对的
|
||||
// 不需要「第 N 天」标签,但删它要连着改 AI 解析,不在这次范围内
|
||||
DayLabel string `gorm:"size:32" json:"day_label"`
|
||||
// Weekdays weekly 用:逗号分隔的 1-7(1=周一)。
|
||||
// 存字符串不另开表——一条模板明细最多 7 个数字,开表换来的只是查询灵活性,
|
||||
// 而这份数据从来只整条读出来展开
|
||||
Weekdays string `gorm:"size:16" json:"weekdays"`
|
||||
MonthDay int `json:"month_day"` // monthly 用:几号(1-31),超出当月天数按月末算
|
||||
|
||||
// reminder 专用
|
||||
ReminderType string `gorm:"size:32" json:"reminder_type"` // vaccine/deworm/weight/monthlyReport
|
||||
OffsetDays int `json:"offset_days"` // 到期 = 今天 + N 天;<=0 表示无固定到期,用频率
|
||||
Frequency string `gorm:"size:64" json:"frequency"` // 频率描述,如「每周二、周五」
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
// Comment 帖子评论
|
||||
type Comment struct {
|
||||
Base
|
||||
PostID string `gorm:"size:24;index" json:"post_id"`
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
AuthorName string `gorm:"size:64" json:"author_name"`
|
||||
Content string `gorm:"size:512" json:"content"`
|
||||
Images datatypes.JSON `json:"images"`
|
||||
// 两级评论:ParentID 为空是一级评论,否则是某条一级评论下的回复。
|
||||
// 不做无限层级——手机上层层缩进读到第四层就没法看了,
|
||||
// 主流产品(微信、小红书、B站)都是两级 + @某人 表达指向。
|
||||
ParentID string `gorm:"size:24;index" json:"parent_id"`
|
||||
ReplyToName string `gorm:"size:64" json:"reply_to_name"`
|
||||
Status string `gorm:"size:16;default:published" json:"status"`
|
||||
|
||||
IsSelf bool `gorm:"-" json:"is_self"` // 计算字段:是不是我自己发的
|
||||
Replies []Comment `gorm:"-" json:"replies"` // 计算字段:一级评论下挂的回复
|
||||
ReplyN int `gorm:"-" json:"reply_n"` // 回复总数(可能多于返回的条数)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
// CommunityBotConfig 社区 AI 运营配置(单例,固定 id=1)
|
||||
type CommunityBotConfig struct {
|
||||
Base
|
||||
Enabled bool `json:"enabled"` // 是否开启每日自动生成
|
||||
DailyCount int `json:"daily_count"` // 每天生成条数
|
||||
StartHour int `json:"start_hour"` // 白天投放窗口起(含)
|
||||
EndHour int `json:"end_hour"` // 白天投放窗口止(不含)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package model
|
||||
|
||||
// DailyAdvice 首页「今日建议」每宠每天缓存一条,避免重复调用模型
|
||||
type DailyAdvice struct {
|
||||
Base
|
||||
PetID string `gorm:"size:24;uniqueIndex:idx_pet_day" json:"pet_id"`
|
||||
Day string `gorm:"size:10;uniqueIndex:idx_pet_day" json:"day"` // YYYY-MM-DD
|
||||
Text string `gorm:"type:text" json:"text"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// DailyTask 今日任务
|
||||
type DailyTask struct {
|
||||
Base
|
||||
PetID string `gorm:"size:24;index" json:"pet_id"`
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
TaskDate time.Time `gorm:"size:24;index" json:"task_date"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
Priority string `gorm:"size:16" json:"priority"` // "" / 重要
|
||||
SheetType string `gorm:"size:32" json:"sheet_type"` // 点击打开的弹层类型
|
||||
Done bool `json:"done"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package model
|
||||
|
||||
// Feedback 用户意见反馈
|
||||
type Feedback struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
Nickname string `gorm:"size:64" json:"nickname"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Contact string `gorm:"size:128" json:"contact"` // 可选,方便回访
|
||||
Handled bool `gorm:"index" json:"handled"`
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package model
|
||||
|
||||
// File 上传文件表:图片/头像等统一入库,按 MD5 去重,使用处以 file_id 关联。
|
||||
type File struct {
|
||||
Base
|
||||
MD5 string `gorm:"size:32;uniqueIndex" json:"md5"` // 内容 MD5,唯一,用于去重
|
||||
ObjectName string `gorm:"size:255" json:"object_name"` // MinIO 对象名
|
||||
URL string `gorm:"size:512" json:"url"` // 可访问 URL
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `gorm:"size:128" json:"content_type"`
|
||||
Ext string `gorm:"size:16" json:"ext"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package model
|
||||
|
||||
// Follow 关注关系:FollowerID 关注了 FolloweeID
|
||||
type Follow struct {
|
||||
Base
|
||||
FollowerID string `gorm:"size:24;uniqueIndex:idx_follower_followee" json:"follower_id"`
|
||||
FolloweeID string `gorm:"size:24;uniqueIndex:idx_follower_followee;index" json:"followee_id"`
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// 健康记录类型
|
||||
const (
|
||||
RecordWeight = "weight"
|
||||
RecordPoop = "poop"
|
||||
RecordFood = "food"
|
||||
RecordSymptom = "symptom"
|
||||
RecordMedicine = "medicine"
|
||||
RecordVaccine = "vaccine"
|
||||
RecordDeworm = "deworm"
|
||||
RecordCost = "cost"
|
||||
RecordPhoto = "photo"
|
||||
)
|
||||
|
||||
// HealthRecord 统一健康/时间轴记录表,type 区分 9 类。
|
||||
// NumValue 存体重/金额,Category 存消费类别/便便状态等,Extra 存各类型专有字段。
|
||||
type HealthRecord struct {
|
||||
Base
|
||||
PetID string `gorm:"size:24;index" json:"pet_id"`
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
Type string `gorm:"size:16;index" json:"type"`
|
||||
Icon string `gorm:"size:16" json:"icon"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
NumValue float64 `gorm:"type:decimal(10,2)" json:"num_value"`
|
||||
Category string `gorm:"size:32" json:"category"`
|
||||
ImageURL string `gorm:"size:512" json:"image_url"`
|
||||
ImageFileID string `gorm:"size:24" json:"image_file_id"` // 关联 files 表:记录附图
|
||||
Extra datatypes.JSON `json:"extra"`
|
||||
OccurredAt time.Time `gorm:"size:24;index" json:"occurred_at"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
// 图片异步安全检测的一次提交。mediaCheckAsync 返回 trace_id,
|
||||
// 结果稍后由微信 push 回调,用 trace_id 找回它属于哪条帖子/评论。
|
||||
const (
|
||||
MediaPending = "pending"
|
||||
MediaPass = "pass"
|
||||
MediaRisky = "risky"
|
||||
)
|
||||
|
||||
type MediaCheck struct {
|
||||
Base
|
||||
TraceID string `gorm:"size:64;index" json:"trace_id"`
|
||||
TargetType string `gorm:"size:16;index" json:"target_type"` // post | comment
|
||||
TargetID string `gorm:"size:24;index" json:"target_id"`
|
||||
Status string `gorm:"size:16" json:"status"` // pending | pass | risky
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Base gives every persisted model a PostgreSQL uuid primary key.
|
||||
// GORM's naming strategy maps Go fields to snake_case columns.
|
||||
type Base struct {
|
||||
ID string `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (b *Base) BeforeCreate(_ *gorm.DB) error {
|
||||
if b.ID == "" {
|
||||
b.ID = uuid.NewString()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Base
|
||||
Bucket string `gorm:"size:128;not null" json:"bucket"`
|
||||
ObjectKey string `gorm:"size:512;uniqueIndex;not null" json:"object_key"`
|
||||
MD5 string `gorm:"column:md5;size:32;uniqueIndex;not null" json:"md5"`
|
||||
URL string `gorm:"size:1024;not null" json:"url"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
ContentType string `gorm:"size:128;not null" json:"content_type"`
|
||||
Size int64 `gorm:"not null" json:"size"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Base
|
||||
OpenID string `gorm:"size:128;uniqueIndex;not null" json:"-"`
|
||||
Nickname string `gorm:"size:64;not null" json:"nickname"`
|
||||
AvatarFileID *string `gorm:"type:uuid;index" json:"avatar_file_id"`
|
||||
Disabled bool `gorm:"not null;default:false" json:"disabled"`
|
||||
}
|
||||
|
||||
type Admin struct {
|
||||
Base
|
||||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
|
||||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||||
}
|
||||
|
||||
type Pet struct {
|
||||
Base
|
||||
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Species string `gorm:"size:16;not null;default:dog" json:"species"`
|
||||
Breed string `gorm:"size:64" json:"breed"`
|
||||
Gender string `gorm:"size:16" json:"gender"`
|
||||
BirthDate *time.Time `gorm:"type:date" json:"birth_date"`
|
||||
WeightKg float64 `gorm:"type:numeric(6,2)" json:"weight_kg"`
|
||||
AvatarFileID *string `gorm:"type:uuid;index" json:"avatar_file_id"`
|
||||
Coat string `gorm:"size:64" json:"coat"`
|
||||
Allergies string `gorm:"size:255" json:"allergies"`
|
||||
CareNotes string `gorm:"size:1000" json:"care_notes"`
|
||||
Archived bool `gorm:"not null;default:false" json:"archived"`
|
||||
}
|
||||
|
||||
type CareTask struct {
|
||||
Base
|
||||
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
|
||||
PetID string `gorm:"type:uuid;index;not null" json:"pet_id"`
|
||||
Title string `gorm:"size:128;not null" json:"title"`
|
||||
Category string `gorm:"size:32;not null" json:"category"`
|
||||
TimeOfDay string `gorm:"size:5;not null" json:"time_of_day"`
|
||||
RepeatRule string `gorm:"size:16;not null;default:daily" json:"repeat_rule"`
|
||||
ReminderEnabled bool `gorm:"not null;default:false" json:"reminder_enabled"`
|
||||
}
|
||||
|
||||
type TaskCompletion struct {
|
||||
Base
|
||||
TaskID string `gorm:"type:uuid;uniqueIndex:idx_task_date;not null" json:"task_id"`
|
||||
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
|
||||
Date time.Time `gorm:"type:date;uniqueIndex:idx_task_date;not null" json:"date"`
|
||||
CompletedAt time.Time `gorm:"not null" json:"completed_at"`
|
||||
}
|
||||
|
||||
type Walk struct {
|
||||
Base
|
||||
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
|
||||
Status string `gorm:"size:16;index;not null" json:"status"`
|
||||
StartedAt time.Time `gorm:"not null" json:"started_at"`
|
||||
SegmentStartedAt time.Time `gorm:"not null" json:"segment_started_at"`
|
||||
PausedAt *time.Time `json:"paused_at"`
|
||||
PausedSeconds int `gorm:"not null;default:0" json:"paused_seconds"`
|
||||
EndedAt *time.Time `json:"ended_at"`
|
||||
DurationSeconds int `gorm:"not null;default:0" json:"duration_seconds"`
|
||||
DistanceMeters float64 `gorm:"type:numeric(12,2);not null;default:0" json:"distance_meters"`
|
||||
ShareEnabled bool `gorm:"not null;default:false" json:"share_enabled"`
|
||||
ShareToken string `gorm:"type:uuid;uniqueIndex" json:"-"`
|
||||
ShareExpiresAt *time.Time `json:"share_expires_at"`
|
||||
}
|
||||
|
||||
type WalkPet struct {
|
||||
Base
|
||||
WalkID string `gorm:"type:uuid;uniqueIndex:idx_walk_pet;not null" json:"walk_id"`
|
||||
PetID string `gorm:"type:uuid;uniqueIndex:idx_walk_pet;index;not null" json:"pet_id"`
|
||||
}
|
||||
|
||||
type WalkPoint struct {
|
||||
Base
|
||||
WalkID string `gorm:"type:uuid;index;not null" json:"walk_id"`
|
||||
Latitude float64 `gorm:"type:numeric(10,7);not null" json:"latitude"`
|
||||
Longitude float64 `gorm:"type:numeric(10,7);not null" json:"longitude"`
|
||||
RecordedAt time.Time `gorm:"index;not null" json:"recorded_at"`
|
||||
}
|
||||
|
||||
type Meetup struct {
|
||||
Base
|
||||
HostUserID string `gorm:"type:uuid;index;not null" json:"host_user_id"`
|
||||
Title string `gorm:"size:128;not null" json:"title"`
|
||||
Description string `gorm:"size:1000" json:"description"`
|
||||
PlaceName string `gorm:"size:255;not null" json:"place_name"`
|
||||
Latitude float64 `gorm:"type:numeric(10,7)" json:"latitude"`
|
||||
Longitude float64 `gorm:"type:numeric(10,7)" json:"longitude"`
|
||||
StartsAt time.Time `gorm:"index;not null" json:"starts_at"`
|
||||
MaxPeople int `gorm:"not null;default:8" json:"max_people"`
|
||||
Status string `gorm:"size:16;index;not null;default:open" json:"status"`
|
||||
CoverFileID *string `gorm:"type:uuid;index" json:"cover_file_id"`
|
||||
}
|
||||
|
||||
type MeetupParticipant struct {
|
||||
Base
|
||||
MeetupID string `gorm:"type:uuid;uniqueIndex:idx_meetup_user;not null" json:"meetup_id"`
|
||||
UserID string `gorm:"type:uuid;uniqueIndex:idx_meetup_user;index;not null" json:"user_id"`
|
||||
}
|
||||
|
||||
type MeetupPet struct {
|
||||
Base
|
||||
ParticipantID string `gorm:"type:uuid;uniqueIndex:idx_participant_pet;not null" json:"participant_id"`
|
||||
PetID string `gorm:"type:uuid;uniqueIndex:idx_participant_pet;index;not null" json:"pet_id"`
|
||||
}
|
||||
|
||||
func All() []any {
|
||||
return []any{
|
||||
&File{}, &User{}, &Admin{}, &Pet{}, &CareTask{}, &TaskCompletion{},
|
||||
&Walk{}, &WalkPet{}, &WalkPoint{}, &Meetup{}, &MeetupParticipant{}, &MeetupPet{},
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// Pet 宠物,属于 User,支持多宠
|
||||
type Pet struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
Name string `gorm:"size:64" json:"name"`
|
||||
Emoji string `gorm:"size:16" json:"emoji"`
|
||||
AvatarFileID string `gorm:"size:24" json:"avatar_file_id"` // 关联 files 表:宠物照片
|
||||
AvatarURL string `gorm:"-" json:"avatar_url"` // 计算字段:由 AvatarFileID 解析
|
||||
Type string `gorm:"size:16" json:"type"` // 猫猫 / 狗狗
|
||||
Gender string `gorm:"size:16" json:"gender"` // 男孩 / 女孩 / 不确定
|
||||
Birthday *time.Time `json:"birthday"`
|
||||
ArrivedAt *time.Time `json:"arrived_at"` // 到家日期。30 天内会额外叠加一套安置期任务
|
||||
Weight string `gorm:"size:16" json:"weight"` // 如 "2.8kg"
|
||||
Stage string `gorm:"size:32" json:"stage"` // 年龄阶段:幼年期 / 成年期 / 老年期(由生日推断,可手改)
|
||||
// Age 计算字段,不入库。原来是建档时算好存下来的字符串,
|
||||
// 养半年后首页还显示「2个月」—— 存一个会过期的展示值本身就是 bug
|
||||
Age string `gorm:"-" json:"age"`
|
||||
Color string `gorm:"size:32" json:"color"` // 毛色,如 橘白 / 奶牛
|
||||
Breed string `gorm:"size:64" json:"breed"` // 品种
|
||||
HealthStatus string `gorm:"size:16;default:正常" json:"health_status"`
|
||||
Goals datatypes.JSON `json:"goals"` // onboarding 目标多选
|
||||
|
||||
// —— 身份卡用的补充信息 ——
|
||||
Personality string `gorm:"size:64" json:"personality"` // 性格标签,逗号分隔,如 "活泼,亲人,贪吃"
|
||||
Allergy string `gorm:"size:128" json:"allergy"` // 过敏情况,没有就填「无」
|
||||
Notes string `gorm:"size:255" json:"notes"` // 备注信息(照护提醒)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// 计划类型
|
||||
const (
|
||||
PlanThirtyDay = "thirty_day"
|
||||
PlanAI = "ai"
|
||||
)
|
||||
|
||||
// Plan 养宠计划(30 天路线图 / AI 计划)
|
||||
type Plan struct {
|
||||
Base
|
||||
PetID string `gorm:"size:24;index" json:"pet_id"`
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
Kind string `gorm:"size:16;index" json:"kind"` // thirty_day / ai
|
||||
Stage string `gorm:"size:32" json:"stage"`
|
||||
StartDate *time.Time `json:"start_date"`
|
||||
EndDate *time.Time `json:"end_date"`
|
||||
UserInput string `gorm:"size:512" json:"user_input"`
|
||||
Extracted datatypes.JSON `json:"extracted"` // AI 提取信息
|
||||
CompletionPct int `json:"completion_pct"`
|
||||
Status string `gorm:"size:16;default:active" json:"status"` // active / completed / archived
|
||||
Tasks []PlanTask `gorm:"foreignKey:PlanID" json:"tasks"`
|
||||
}
|
||||
|
||||
// PlanTask 计划明细项
|
||||
type PlanTask struct {
|
||||
Base
|
||||
PlanID string `gorm:"size:24;index" json:"plan_id"`
|
||||
Day int `json:"day"`
|
||||
DayLabel string `gorm:"size:32" json:"day_label"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
SheetType string `gorm:"size:32" json:"sheet_type"`
|
||||
// Priority 从 DailyTask 合并过来的:首页那份「今天要做的事」要能标出哪条重要
|
||||
Priority string `gorm:"size:16" json:"priority"`
|
||||
Done bool `json:"done"`
|
||||
Date string `gorm:"-" json:"date"` // 计算字段:计划开始日 + Day
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
// 帖子状态
|
||||
const (
|
||||
PostPending = "pending" // 待审核:内容安全未通过/待人工,前端 feed 不展示
|
||||
PostPublished = "published" // 已过审,正常展示
|
||||
PostRejected = "rejected" // 内容安全判定风险,打回
|
||||
PostHidden = "hidden"
|
||||
PostDeleted = "deleted"
|
||||
)
|
||||
|
||||
// Post 社区帖子
|
||||
type Post struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
PetID *string `gorm:"size:24" json:"pet_id"`
|
||||
AuthorName string `gorm:"size:64" json:"author_name"`
|
||||
AuthorEmoji string `gorm:"size:16" json:"author_emoji"`
|
||||
Identity string `gorm:"size:16" json:"identity"` // petName / anonymous / official
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Tags datatypes.JSON `json:"tags"`
|
||||
Images datatypes.JSON `json:"images"` // 展示用:URL 数组(真实图)或 emoji 数组(机器人)
|
||||
ImageFileIDs datatypes.JSON `json:"image_file_ids"` // 关联 files 表的 id 数组,读取时解析为 Images
|
||||
LikeCount int `json:"like_count"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
Status string `gorm:"size:16;default:published;index" json:"status"`
|
||||
IsAI bool `gorm:"size:24;index" json:"is_ai"` // AI 运营生成的帖子,前端打「AI」标
|
||||
|
||||
// 以下为计算字段,按当前登录用户填充
|
||||
Followed bool `gorm:"-" json:"followed"` // 我是否已关注该作者
|
||||
IsSelf bool `gorm:"-" json:"is_self"` // 是不是我自己发的
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package model
|
||||
|
||||
// PostLike 帖子点赞,(post_id, user_id) 唯一
|
||||
type PostLike struct {
|
||||
Base
|
||||
PostID string `gorm:"size:24;uniqueIndex:idx_post_user" json:"post_id"`
|
||||
UserID string `gorm:"size:24;uniqueIndex:idx_post_user" json:"user_id"`
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Pro 会员状态
|
||||
const (
|
||||
ProActive = "active"
|
||||
ProExpired = "expired"
|
||||
ProNone = "none"
|
||||
)
|
||||
|
||||
// ProMembership 会员,1:1 于 User
|
||||
type ProMembership struct {
|
||||
Base
|
||||
UserID string `gorm:"size:24;uniqueIndex" json:"user_id"`
|
||||
Status string `gorm:"size:16;default:none" json:"status"`
|
||||
PlanType string `gorm:"size:16" json:"plan_type"` // yearly / monthly
|
||||
Price float64 `gorm:"type:decimal(10,2)" json:"price"`
|
||||
StartDate *time.Time `json:"start_date"`
|
||||
EndDate *time.Time `json:"end_date"`
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package model
|
||||
|
||||
// 记录类型。原来这 9 种是写在 health_record.go 里的常量,加一种就得发版;
|
||||
// 挪到表里之后后台可以直接增删改。
|
||||
//
|
||||
// 注意这里**没有**替换掉那 9 个常量:weight / poop / food / symptom / cost /
|
||||
// vaccine 在 record.go、insight.go、report.go、home.go 里有真实的代码分支
|
||||
// (体重回写宠物档案、食便相关性分析、消费统计、疫苗完成度……),那些分支
|
||||
// 按具体常量过滤,不是 switch 全枚举。所以:
|
||||
// - 表负责「展示什么、能记什么」
|
||||
// - 常量负责「哪几种参与分析」
|
||||
// 两者各管一段,新增类型不会碰到任何分析逻辑。
|
||||
|
||||
// 记录类型分组
|
||||
const (
|
||||
RecordGroupDaily = "daily" // 日常
|
||||
RecordGroupHealth = "health" // 健康
|
||||
RecordGroupCare = "care" // 洗护
|
||||
RecordGroupClean = "clean" // 清洁
|
||||
)
|
||||
|
||||
// 弹层形态
|
||||
const (
|
||||
RecordFormFull = "full" // 有专用表单(原来那 9 种,各自一个 wx:elif 分支)
|
||||
RecordFormSimple = "simple" // 通用简易表单:发生时间 + 备注 + 可选照片
|
||||
)
|
||||
|
||||
// RecordType 一种可记录的事项
|
||||
type RecordType struct {
|
||||
Base
|
||||
// Code 就是小程序 bottom-sheet 的 type,也是 health_records.type 落库的值。
|
||||
// 建过记录之后改 Code 会让历史数据对不上,后台不允许改(只能删了重建)。
|
||||
Code string `gorm:"size:32;uniqueIndex" json:"code"`
|
||||
Label string `gorm:"size:32" json:"label"`
|
||||
Icon string `gorm:"size:32" json:"icon"` // pt-icon 名,见 styles/iconfont.wxss
|
||||
|
||||
// GroupKey 存成 group_key 而不是 group:group 是 MySQL 保留字,
|
||||
// GORM 虽然会加反引号,但手写 SQL 排查时很容易踩
|
||||
GroupKey string `gorm:"column:group_key;size:16;index" json:"group"`
|
||||
Sort int `json:"sort"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Form string `gorm:"size:16" json:"form"`
|
||||
|
||||
// Locked 有代码分支或模板引用依赖它,后台不许删。
|
||||
// 硬拦在 service 层,不是靠前端隐藏按钮。
|
||||
Locked bool `json:"locked"`
|
||||
|
||||
// Fields 表单字段配置,一行一个。后台用紧凑写法编辑:
|
||||
//
|
||||
// number:体重:kg 数值,落 num_value
|
||||
// options:状态:正常|软便|拉稀 单选,落 category
|
||||
// options:症状:呕吐|拉稀|精神差:- 单选,末尾 - 表示不落 category(只进标题)
|
||||
// text:吃了什么 文本,只进标题
|
||||
//
|
||||
// 空 = 这个事项只要「时间 + 描述 + 照片」(洗澡、换猫砂这类)。
|
||||
//
|
||||
// 为什么要有「不落 category」这个开关:异常观察有两个单选(症状 + 严重程度),
|
||||
// 而 category 只有一个坑,周报的高风险数按 category='高' 统计,
|
||||
// 所以必须能指定是哪一个占这个坑。
|
||||
Fields string `gorm:"type:text" json:"fields_raw"`
|
||||
|
||||
// ParsedFields 由 Fields 解析出来,前端直接照它渲染表单,不用自己写解析器
|
||||
ParsedFields []RecordField `gorm:"-" json:"fields"`
|
||||
}
|
||||
|
||||
// 字段类型
|
||||
const (
|
||||
FieldNumber = "number"
|
||||
FieldOptions = "options"
|
||||
FieldText = "text"
|
||||
)
|
||||
|
||||
// RecordField 一个表单字段
|
||||
type RecordField struct {
|
||||
Kind string `json:"kind"`
|
||||
Label string `json:"label"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
// ToCategory 这个字段的值是否落到 health_records.category。
|
||||
// 同一个事项里最多一个字段能占它
|
||||
ToCategory bool `json:"to_category"`
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// RefreshToken 刷新令牌。
|
||||
// access token(JWT) 只有 2 小时,过期后用它换新的,用户不必重新登录;
|
||||
// 库里只存 sha256,即使数据库泄露也换不出可用令牌。
|
||||
// 每次刷新都会轮换(旧的立即作废),旧令牌被再次使用视为泄露。
|
||||
type RefreshToken struct {
|
||||
Base
|
||||
TokenHash string `gorm:"size:64;uniqueIndex" json:"-"`
|
||||
Kind string `gorm:"size:16;index" json:"kind"` // user | admin
|
||||
SubjectID string `gorm:"size:24;index" json:"subject_id"` // 用户 ID 或管理员 ID
|
||||
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
|
||||
RevokedAt *time.Time `json:"revoked_at"`
|
||||
// 作废原因:rotate=正常轮换(有宽限期)/ logout=主动退出 / reuse=检测到泄露被连坐吊销。
|
||||
// 只有 rotate 允许宽限期内再用一次,否则「吊销全部会话」会被宽限期架空。
|
||||
RevokedReason string `gorm:"size:16" json:"revoked_reason"`
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// 提醒类型
|
||||
const (
|
||||
ReminderVaccine = "vaccine"
|
||||
ReminderDeworm = "deworm"
|
||||
ReminderWeight = "weight"
|
||||
ReminderMonthlyReport = "monthlyReport"
|
||||
)
|
||||
|
||||
// Reminder 提醒
|
||||
type Reminder struct {
|
||||
Base
|
||||
PetID string `gorm:"size:24;index" json:"pet_id"`
|
||||
UserID string `gorm:"size:24;index" json:"user_id"`
|
||||
Type string `gorm:"size:24;index" json:"type"`
|
||||
Title string `gorm:"size:128" json:"title"`
|
||||
NextDueDate *time.Time `json:"next_due_date"`
|
||||
Frequency string `gorm:"size:64" json:"frequency"`
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package model
|
||||
|
||||
// User 小程序用户(铲屎官)
|
||||
type User struct {
|
||||
Base
|
||||
OpenID string `gorm:"size:64;index" json:"openid"`
|
||||
Nickname string `gorm:"size:64" json:"nickname"`
|
||||
// 昵称是系统随机生成的(还没被用户或微信授权覆盖过)。
|
||||
// 有这个标记才能安全地在用户授权时覆盖,而不会把他改过的昵称冲掉。
|
||||
NicknameAuto bool `json:"nickname_auto"`
|
||||
PetCount int `gorm:"-" json:"pet_count"` // 计算字段:后台列表用
|
||||
Avatar string `gorm:"size:512" json:"avatar"`
|
||||
AvatarFileID string `gorm:"size:24" json:"avatar_file_id"` // 关联 files 表;优先于 Avatar
|
||||
AvatarURL string `gorm:"-" json:"avatar_url"` // 计算字段:由 AvatarFileID 解析
|
||||
Phone string `gorm:"size:32" json:"phone"`
|
||||
Onboarded bool `json:"onboarded"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IsBot bool `gorm:"size:24;index" json:"is_bot"` // 虚拟宠友账号(AI 社区运营用)
|
||||
|
||||
// —— 个人主页装扮 ——
|
||||
Bio string `gorm:"size:120" json:"bio"` // 个性签名
|
||||
BgFileID string `gorm:"size:24" json:"bg_file_id"` // 主页头图,关联 files 表
|
||||
BgURL string `gorm:"-" json:"bg_url"` // 计算字段:由 BgFileID 解析
|
||||
Theme string `gorm:"size:16" json:"theme"` // 主题色 key,见 service.UserThemes
|
||||
// 存「隐藏」而不是「展示」,为的是零值即默认:老数据 AutoMigrate 出来是 0,
|
||||
// 正好等于「展示宠物墙」,不需要给存量用户跑一次刷值
|
||||
HidePets bool `json:"hide_pets"`
|
||||
}
|
||||
Reference in New Issue
Block a user