// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Get a free hosted Postgres database in seconds: `npx create-db` generator client { provider = "prisma-client" output = "../generated/prisma" } datasource db { provider = "mysql" } model User { id Int @id @default(autoincrement()) name String email String @unique password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // V0.2 打卡 / 清单: 用户删除时级联清理 habits Habit[] checkIns CheckIn[] checkLists CheckList[] templates CheckListTemplate[] // V0.2 任务模块 ownedProjects Project[] @relation("ProjectOwner") projectMembers ProjectMember[] assignedTasks Task[] @relation("TaskAssignee") createdTasks Task[] @relation("TaskCreator") taskMembers TaskMember[] } model Article { id Int @id @default(autoincrement()) title String slug String @unique content String date String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Strategy { id Int @id @default(autoincrement()) name String description String data Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Indicator { id Int @id @default(autoincrement()) name String @unique groupName String title String @default("") unit String @default("") source String @default("") frequency String @default("month") // year | quarter | month | day hidden Boolean @default(false) // true = 数据/脚本保留, 但不在前端展示 points IndicatorPoint[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model IndicatorPoint { id Int @id @default(autoincrement()) indicatorId Int indicator Indicator @relation(fields: [indicatorId], references: [id], onDelete: Cascade) period String // "2024" | "2024-Q1" | "2024-01" | "2024-01-15" value Float createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([indicatorId, period]) } // 新闻: 每天 3 点由 mx_finance_search_news 拉取, 按影响大小排序, 累加多日供首页无限滚动 model News { id Int @id @default(autoincrement()) title String summary String @default("") content String? @db.Text source String @default("") url String @default("") publishedAt DateTime impactRank Int createdAt DateTime @default(now()) @@unique([title, publishedAt]) @@index([publishedAt]) } // 报警: refresh 写库时比对同 (indicatorName, period) 的跨源数据, 差异超阈值记录 model Alert { id Int @id @default(autoincrement()) indicatorName String period String valueA Float valueB Float sourceA String sourceB String diff Float diffRatio Float createdAt DateTime @default(now()) @@index([createdAt]) } // 服务器监控: 对接 komari 公开 API model ServerNode { id Int @id @default(autoincrement()) uuid String @unique name String cpuName String @default("") os String @default("") region String @default("") memTotal BigInt @default(0) diskTotal BigInt @default(0) cpuCores Int @default(0) // 最新实时快照 cpuUsage Float @default(0) ramUsed BigInt @default(0) swapUsed BigInt @default(0) load1 Float @default(0) load5 Float @default(0) load15 Float @default(0) diskUsed BigInt @default(0) netIn BigInt @default(0) netOut BigInt @default(0) uptime BigInt @default(0) process Int @default(0) connections Int @default(0) status String @default("unknown") // online | offline | unknown recordedAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // 时序历史: 每节点每分钟一条 (komari records 1 分钟粒度) model ServerNodeHistory { id Int @id @default(autoincrement()) uuid String period DateTime cpu Float @default(0) ram BigInt @default(0) load Float @default(0) disk BigInt @default(0) netIn BigInt @default(0) netOut BigInt @default(0) process Int @default(0) connections Int @default(0) createdAt DateTime @default(now()) @@unique([uuid, period]) @@index([uuid, period]) } // 服务健康检查: 本应用自检 (健康端点 / DB / 第三方) model ServiceCheck { id Int @id @default(autoincrement()) name String @unique url String status String @default("unknown") // up | down | unknown latencyMs Int @default(0) message String @default("") lastCheckAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Econ_SowInventory { id Int @id @default(autoincrement()) month String @unique inventory Int // 能繁母猪数量(万头) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ScraperCache { id Int @id @default(autoincrement()) scraperName String @unique // 爬虫名称,如:sow-inventory lastDataHash String? // 上次数据指纹 lastUpdateAt DateTime // 上次更新时间 metadata Json? // 其他元数据 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // 浙江省油价历史 (apizero price 接口每日抓取) // 每天 04:00 cron 触发: 检查 schedule 里待定且 <= 昨天的调价日, 抓 price?province=浙江 存本表 // unique (date, province, fuel) 防重复 model OilPriceHistory { id Int @id @default(autoincrement()) date DateTime // 调价日次日 (即数据实际抓取日) province String // '浙江' fuel String // '92' | '95' | '0' price Float // 油价 (元/升, 4 位小数足够) source String @default("apizero") createdAt DateTime @default(now()) @@unique([date, province, fuel]) @@index([province, fuel, date]) } // ==================== 环境监控:米家 ==================== // 米家授权状态 (DiskCache 存凭证, 本表只存状态/错误供前端展示) model MihomeAccount { id Int @id @default(autoincrement()) key String @unique @default("default") userId String @default("") status String @default("unknown") // ok | pending | expired lastError String @default("") updatedAt DateTime @updatedAt } model MihomeHome { id Int @id @default(autoincrement()) homeId String @unique name String createdAt DateTime @default(now()) devices MihomeDevice[] } model MihomeDevice { id Int @id @default(autoincrement()) did String @unique homeId String home MihomeHome @relation(fields: [homeId], references: [homeId]) model String name String alias String? // 用户别名, 仅本应用展示 tempSiid Int tempPiid Int humSiid Int humPiid Int battSiid Int // 电池电量属性 (炎米 3.1003 / 青萍 2.3) battPiid Int battery Int? // 最近一次电量 (每天读一次, 标题展示用) batteryAt DateTime? // 电量读取时间 (当天缓存, 不频繁更新) enabled Boolean @default(true) lastSyncAt DateTime? createdAt DateTime @default(now()) samples MihomeEnvSample[] hours MihomeEnvHour[] } // 日档: 月视图(逐日)数据源; unique (deviceId, date) 防重 model MihomeEnvSample { id Int @id @default(autoincrement()) deviceId Int device MihomeDevice @relation(fields: [deviceId], references: [id], onDelete: Cascade) date DateTime // 采样日 (Asia/Shanghai 当日 00:00 存 UTC) temperature Float? // 当日最后一次实时值 (或 vendor hour 均值) humidity Float? temperatureAvg Float? // 当日均值 (由小时档聚合, 或 vendor_hour) humidityAvg Float? temperatureMin Float? // 当日最低 temperatureMax Float? // 当日最高 humidityMin Float? humidityMax Float? origin String @default("live") // live | vendor_history | backfill sampledAt DateTime? raw Json? createdAt DateTime @default(now()) @@unique([deviceId, date]) @@index([deviceId, date]) } // 小时档: 当天视图(逐小时)数据源; unique (deviceId, hour) 防重 model MihomeEnvHour { id Int @id @default(autoincrement()) deviceId Int device MihomeDevice @relation(fields: [deviceId], references: [id], onDelete: Cascade) hour DateTime // Asia/Shanghai 整点 (存 UTC) temperatureAvg Float? // 该小时均值 (live 采样点, 或 vendor_hour) humidityAvg Float? raw Json? createdAt DateTime @default(now()) @@unique([deviceId, hour]) @@index([deviceId, hour]) } // ==================== 打卡 / 日历 / 清单 (V0.2) ==================== // 打卡项: 周期性目标 (每天 / 每周 / 每月 / 每年 n 次) model Habit { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) name String period String @default("day") // day | week | month | year target Int @default(1) // 周期内目标次数 n (>=1) color String @default("#1677ff") icon String @default("") // emoji 或图标名, 预留 note String @default("") archived Boolean @default(false) // 归档: 不出现在今日待办, 历史保留 sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt records CheckIn[] @@index([userId, archived, sortOrder]) } // 打卡记录: 一次打卡 = 一条; 同日多次 => seq 递增 (1..n, 超额继续累加) model CheckIn { id Int @id @default(autoincrement()) habitId Int habit Habit @relation(fields: [habitId], references: [id], onDelete: Cascade) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) date String @db.VarChar(10) // 本地日历日 "YYYY-MM-DD" (Asia/Shanghai) seq Int @default(1) // 当日第几次 note String @default("") source String @default("user") // user | backfill (补打) createdAt DateTime @default(now()) @@unique([habitId, date, seq]) @@index([userId, date]) @@index([habitId, date]) } // 检查清单: 出差 / 回老家 等场景的一次性清单 model CheckList { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) title String scene String @default("") // 出差 | 回老家 | 自定义 note String @default("") archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt items CheckListItem[] @@index([userId, archived]) } // 检查项: 勾选即完成 model CheckListItem { id Int @id @default(autoincrement()) listId Int list CheckList @relation(fields: [listId], references: [id], onDelete: Cascade) title String note String @default("") done Boolean @default(false) doneAt DateTime? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([listId, sortOrder]) } // 清单模板: builtin = 系统预置 (userId 为空); 否则为用户自定义 model CheckListTemplate { id Int @id @default(autoincrement()) userId Int? user User? @relation(fields: [userId], references: [id], onDelete: Cascade) name String scene String @default("") items Json // [{ title, note }] builtin Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, builtin]) } // ================== 任务模块 (V0.2 任务模块需求文档 T-*) ================== // 口径见 docs/V0.2/任务模块/任务模块数据库设计文档.md // 项目 = 隔离 + 权限单位; 看板列是实体(可增删改排序); // 任务状态(todo/doing/done) 与 看板列 是两个正交维度, 由列的 boundStatus 决定是否联动. model Project { id Int @id @default(autoincrement()) name String @db.VarChar(50) ownerId Int owner User @relation("ProjectOwner", fields: [ownerId], references: [id], onDelete: Cascade) isPersonal Boolean @default(false) archived Boolean @default(false) notes Json @default("[]") // 项目信息块 [{ title, content }] (T-PJ-13) taskKinds Json? // 任务类型标签列表 (未配置时服务端回落默认四类) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members ProjectMember[] columns BoardColumn[] tasks Task[] @@index([ownerId]) @@index([archived]) } model ProjectMember { id Int @id @default(autoincrement()) projectId Int project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) permission String @default("member") createdAt DateTime @default(now()) @@unique([projectId, userId]) @@index([userId]) } model BoardColumn { id Int @id @default(autoincrement()) projectId Int project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) name String @db.VarChar(20) sortOrder Int @default(0) boundStatus String? @db.VarChar(8) retention String @default("forever") @db.VarChar(8) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tasks Task[] @@unique([projectId, name]) @@index([projectId, sortOrder]) } model Task { id Int @id @default(autoincrement()) projectId Int project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) columnId Int column BoardColumn @relation(fields: [columnId], references: [id], onDelete: Restrict) parentId Int? parent Task? @relation("Subtask", fields: [parentId], references: [id], onDelete: Cascade) children Task[] @relation("Subtask") title String @db.VarChar(200) description String @default("") @db.Text kind String? @db.VarChar(20) // 任务类型标签 (项目 taskKinds 之一) status String @default("todo") @db.VarChar(8) doneAt DateTime? assigneeId Int? assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) creatorId Int creator User @relation("TaskCreator", fields: [creatorId], references: [id], onDelete: Cascade) extras Json @default("[]") sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members TaskMember[] tags TaskTag[] @@index([projectId, columnId]) @@index([projectId, status]) @@index([parentId]) @@index([projectId, doneAt]) } model TaskMember { id Int @id @default(autoincrement()) taskId Int task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([taskId, userId]) } model TaskTag { id Int @id @default(autoincrement()) taskId Int task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) name String @db.VarChar(20) @@index([taskId]) }