feat: rebuild pet app and backend around core care flows

This commit is contained in:
2026-10-01 13:11:30 +08:00
parent 51ead8f411
commit bfcf8788a4
321 changed files with 4918 additions and 25199 deletions
-10
View File
@@ -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"`
}
-11
View File
@@ -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"`
}
-20
View File
@@ -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"`
}
-13
View File
@@ -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"`
}
-57
View File
@@ -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{},
}
}
-26
View File
@@ -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"`
}
-77
View File
@@ -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"` // 频率描述,如「每周二、周五」
}
-23
View File
@@ -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"` // 回复总数(可能多于返回的条数)
}
-10
View File
@@ -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"` // 白天投放窗口止(不含)
}
-9
View File
@@ -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"`
}
-17
View File
@@ -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"`
}
-11
View File
@@ -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"`
}
-12
View File
@@ -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"`
}
-8
View File
@@ -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"`
}
-38
View File
@@ -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"`
}
-17
View File
@@ -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
}
+146
View File
@@ -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{},
}
}
-35
View File
@@ -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"` // 备注信息(照护提醒)
}
-44
View File
@@ -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
}
-34
View File
@@ -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"` // 是不是我自己发的
}
-8
View File
@@ -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"`
}
-21
View File
@@ -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"`
}
-82
View File
@@ -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"`
}
-19
View File
@@ -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"`
}
-22
View File
@@ -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"`
}
-28
View File
@@ -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"`
}