宠物档案的鸿蒙建表:时间序列记录与疫苗到期提醒


实例:宠物档案(Pet Profile)|技术:宠物/体重/喂食三表、时间序列记录、最新值查询、疫苗到期计算
一、业务需求分析
1.1 养宠人的记录需求
养宠物和养孩子类似:要建档(名字/品种/生日)、要记录(体重变化、喂食)、要提醒(疫苗)。宠物档案应用要解决:
- 宠物基本档案——叫什么、什么品种、生日、性别;
- 健康数据——体重随时间的变化趋势;
- 日常记录——每天喂食了几次、吃了什么;
- 疫苗提醒——下次疫苗什么时候、是否临期。
建模上,"体重记录"和"喂食记录"都是时间序列(每行带时间戳、只追加)——这与 14 健康数据记录同族,但宠物场景新增了疫苗到期提醒(类似 28 的到期计算)。
1.2 功能清单
| 编号 | 功能 | 技术要点 |
|---|---|---|
| 1 | 宠物建档(名字/种类/品种/性别/生日) | INSERT |
| 2 | 记录体重 | 时间序列 INSERT |
| 3 | 记录喂食(食物/分量) | 时间序列 INSERT |
| 4 | 查看体重/喂食记录 | 按 pet_id 查,倒序 |
| 5 | 疫苗日期设置与到期提醒 | 日期差值计算 |
| 6 | 今日喂食统计 | 时间范围 COUNT |
| 7 | 删除宠物(连同全部记录) | 先子后父 |
1.3 与已有实例的关系
| 实例 | 相似点 | 本实例新增 |
|---|---|---|
| 14 健康数据 | 时间序列记录 | 多子表(体重+喂食) |
| 28 车辆保养 | 到期提醒 | 疫苗到期 |
| 29 药箱 | 有效期 | 疫苗是"一次性到期日" |
32 的独特价值:一个主表挂两个时间序列从表(体重 + 喂食),且主表字段(next_vaccine)驱动到期提醒。这演示了"1 主 N 从"的表结构组织——比前几实例的单从表更进一步。
二、字段设计表
2.1 主表 pet(宠物)
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| name | TEXT | NOT NULL | 宠物名 |
| species | TEXT | DEFAULT ‘’ | 种类(猫/狗/仓鼠…) |
| breed | TEXT | DEFAULT ‘’ | 品种 |
| gender | INTEGER | NOT NULL DEFAULT 0 | 0 男 / 1 女 |
| birthday | INTEGER | NOT NULL DEFAULT 0 | 生日时间戳 |
| avatar | TEXT | DEFAULT ‘’ | 头像路径(预留) |
| next_vaccine | INTEGER | NOT NULL DEFAULT 0 | 下次疫苗日期(0=无) |
| note | TEXT | DEFAULT ‘’ | 备注 |
| created_at | INTEGER | NOT NULL | 建档时间戳 |
2.2 从表 weight_record(体重记录)
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| pet_id | INTEGER | NOT NULL | 逻辑外键 → pet.id |
| weight | REAL | NOT NULL | 体重(千克) |
| created_at | INTEGER | NOT NULL | 记录时间戳 |
2.3 从表 feed_record(喂食记录)
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| pet_id | INTEGER | NOT NULL | 逻辑外键 → pet.id |
| food | TEXT | DEFAULT ‘’ | 食物 |
| amount | TEXT | DEFAULT ‘’ | 分量 |
| feed_time | INTEGER | NOT NULL | 喂食时间戳 |
2.4 设计要点详解
要点一:两个从表为何分开而非合并?
体重和喂食都是"带时间的记录",能否合成一张 pet_log 表(type + value)?可以,但分开更清晰:
| 对比 | 合并表(type+value) | 分表 |
|---|---|---|
| 字段 | type 区分、value 泛化 | 各自专属字段(weight / food+amount) |
| 查询 | type 过滤 | 天然隔离 |
| 类型安全 | value 是通用型 | weight 强类型 REAL |
| 演进 | 加类型要改枚举 | 加表独立演进 |
判断标准:两个时间序列的字段结构不同(体重一个数 vs 喂食两个文本),分开建表类型安全、查询清晰。同构的多类型日志才考虑合并(type + value 模式),异构的分开。
要点二:weight 用 REAL(千克,可带小数)。
宠物体重 4.2kg、18.5kg——连续量,REAL。与 28 里程同理,连续计量用浮点。
要点三:next_vaccine 冗余在主表。
疫苗日期是"下一次的时间点"(现状属性),放主表(同 28 的 next_date 语义)。它是单值状态而非序列——历史疫苗记录(何时打过什么针)才需要子表,本实例简化为主表存"下次日期"。
要点四:feed_time 与 created_at 命名区分。
体重表用 created_at、喂食表用 feed_time——语义一致(记录时间),命名随业务习惯。命名一致性内部统一即可,两表不同名不影响功能(各自查询时用各自的列)。
三、建表 SQL
CREATE TABLE IF NOT EXISTS pet (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
species TEXT DEFAULT '',
breed TEXT DEFAULT '',
gender INTEGER NOT NULL DEFAULT 0,
birthday INTEGER NOT NULL DEFAULT 0,
avatar TEXT DEFAULT '',
next_vaccine INTEGER NOT NULL DEFAULT 0,
note TEXT DEFAULT '',
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS weight_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pet_id INTEGER NOT NULL,
weight REAL NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS feed_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pet_id INTEGER NOT NULL,
food TEXT DEFAULT '',
amount TEXT DEFAULT '',
feed_time INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pet_name ON pet (name);
CREATE INDEX IF NOT EXISTS idx_weight_pet ON weight_record (pet_id);
CREATE INDEX IF NOT EXISTS idx_feed_pet ON feed_record (pet_id);
索引分析:三个索引都是"按宠物查记录"的关联入口(pet_id)。时间序列的常见查询"某宠物最新体重"走 idx_weight_pet + ORDER BY created_at DESC;"今日喂食"走 idx_feed_pet + feed_time >= 今天0点。
四、PetDao 数据访问层
4.1 最新体重查询 ★核心查询
static async latestWeight(context: common.Context, petId: number): Promise<number> {
const store = await PetDao.getStore(context);
const predicates = new relationalStore.RdbPredicates(PetDao.WEIGHT_TABLE);
predicates.equalTo('pet_id', petId).orderByDesc('created_at').limitAs(1);
const result = await store.query(predicates);
let w = 0;
if (result.goToNextRow()) {
w = result.getDouble(result.getColumnIndex('weight'));
}
result.close();
return w;
}
时间序列"最新值"的标准查询:equalTo + orderByDesc + limitAs(1)——按时间倒序取第一条。这是 27 latestBorrow、28 lastMaintain 的第三次复用,已成为本系列的"最新记录"固定范式。
4.2 今日喂食统计:时间范围 COUNT ★核心查询
static async todayFeedCount(context: common.Context, petId: number): Promise<number> {
const store = await PetDao.getStore(context);
const today0 = new Date();
today0.setHours(0, 0, 0, 0);
const result = await store.querySql(
`SELECT COUNT(*) AS c FROM ${PetDao.FEED_TABLE}
WHERE pet_id = ${petId} AND feed_time >= ${today0.getTime()}`
);
// 读 c
}
"今日"的边界:today0 对齐今日 0 点,feed_time >= today0 即今天全部喂食。统计口径与 31 的月度统计同款(日期对象构造时间边界)。每日统计是时间序列的高频需求——"今天喂了几次"比"总共喂了几次"更有行动指导意义。
4.3 疫苗到期计算
static async queryPetViews(context: common.Context): Promise<PetView[]> {
const pets = await PetDao.queryAll(context);
const views: PetView[] = [];
const now = Date.now();
for (const p of pets) {
const v: PetView = {
pet: p,
latestWeight: await PetDao.latestWeight(context, p.id),
weightCount: await PetDao.weightCount(context, p.id),
vaccineDueDays: p.nextVaccine > 0 ? Math.floor((p.nextVaccine - now) / 86400000) : 0,
feedCount: await PetDao.todayFeedCount(context, p.id),
};
views.push(v);
}
return views;
}
疫苗到期天数:(nextVaccine - now) / 一天的毫秒 向下取整。nextVaccine > 0 才计算(0=未设置);负数=已到期。与 28 的 dueDays 算法完全一致——到期提醒是跨实例的统一模式。
4.4 视图组装:一次查 4 个派生数据
queryPetViews 对每只宠物组装:最新体重 + 体重次数 + 疫苗到期 + 今日喂食。4 个派生数据 = 4 次查询 × N 只宠物 = 4N+1 次查询。数据量小(几只宠物)无压力;派生数据在 DAO 组装而非页面计算,页面零逻辑。
4.5 删除:三表先子后父
static async deletePet(context: common.Context, id: number): Promise<void> {
const store = await PetDao.getStore(context);
for (const table of [PetDao.WEIGHT_TABLE, PetDao.FEED_TABLE]) {
const predicates = new relationalStore.RdbPredicates(table);
predicates.equalTo('pet_id', id);
await store.delete(predicates);
}
const petPredicates = new relationalStore.RdbPredicates(PetDao.TABLE);
petPredicates.equalTo('id', id);
await store.delete(petPredicates);
}
两个从表循环删除(数组遍历表名)——避免重复代码。先清全部从表(体重+喂食)再删主表,无孤儿数据。
4.6 疫苗临期筛选
static async vaccineDue(context: common.Context, days: number): Promise<PetView[]> {
const views = await PetDao.queryPetViews(context);
return views.filter((v) => v.pet.nextVaccine > 0 && v.vaccineDueDays <= days);
}
应用层过滤:先组装视图再按疫苗天数筛选(<= days 含已到期负值)。threshold 传 VACCINE_WARN_DAYS(30)得"30 天内需打疫苗"。
4.7 统计
static async summary(context: common.Context): Promise<{ pets: number; todayFeeds: number; vaccineDue: number }> {
const views = await PetDao.queryPetViews(context);
const todayFeeds = views.reduce((acc, v) => acc + v.feedCount, 0); // 全宠物今日喂食总和
const vaccineDue = views.filter((v) => v.pet.nextVaccine > 0 && v.vaccineDueDays <= VACCINE_WARN_DAYS).length;
return { pets: views.length, todayFeeds: todayFeeds, vaccineDue: vaccineDue };
}
reduce 聚合:views.reduce((acc, v) => acc + v.feedCount, 0) 把所有宠物的今日喂食数相加。基于已组装的视图做应用层聚合,避免额外 SQL。
4.8 种子数据
// 宠物 1:咪咪(猫),4 条体重记录(趋势上升),3 条喂食(今日 2),疫苗 25 天后(临期)
// 宠物 2:旺财(狗),2 条体重记录,1 条喂食(今日 1),疫苗未设
种子刻意设计:咪咪的疫苗 25 天后(VACCINE_WARN_DAYS=30 内 → 橙色"疫苗临期"),体重 4 条形成上升趋势;旺财无疫苗(灰"疫苗未设")。两种疫苗状态 + 两条体重趋势都有演示。
五、技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 1 主 2 从 | pet + weight + feed | 异构时间序列分表 |
| 最新值查询 | orderByDesc + limitAs(1) | 时间序列最新记录范式 |
| 今日统计 | 0 点边界 + COUNT | 每日喂食统计 |
| 到期计算 | (next - now)/day floor | 疫苗提醒 |
| 视图组装 | 4 派生数据一次组装 | 页面零计算 |
| 循环删除 | 表名数组遍历 | 多从表清理 |
| reduce 聚合 | 视图数组归约 | 应用层统计 |
| 阈值集中 | VACCINE_WARN_DAYS | 徽标与筛选同步 |
六、文章小结
本篇完成了宠物档案的数据层:pet 存宠物档案(含疫苗日期),weight_record 与 feed_record 两个异构时间序列从表,最新体重用"倒序+limit"范式查询,今日喂食用 0 点边界统计,疫苗到期用日期差值即时计算。这套模型的精髓是:"1 主 N 从"的表结构 + 时间序列的三种查询形态(最新值、按日统计、到期计算)——覆盖了宠物健康管理的数据需求。
下一篇《页面 UI 与操作实现》将搭建:宠物卡片列表(疫苗徽标)、详情抽屉(体重/喂食双记录流)、记体重/记喂食/疫苗设置三个弹窗。
七、数据层深度扩展
1. 体重趋势图数据源
体重记录的展示升级为折线图:queryWeights 已是时间升序/倒序数组,页面可传给 Chart 组件(23 数据图表的 ChartCanvas 有折线绘制)。数据源无需改动——时间序列表天然支持趋势图。
2. 合并表方案(type + value)的适用场景
若日志类型多(体重/喂食/洗澡/遛弯)且字段同构(都是"时间 + 备注"),合并为一张 pet_log:
CREATE TABLE pet_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pet_id INTEGER NOT NULL,
type TEXT NOT NULL, -- weight/feed/bath/walk
value REAL DEFAULT 0, -- 数值型(体重)
text_value TEXT DEFAULT '', -- 文本型(食物)
created_at INTEGER NOT NULL
);
何时选合并:类型多、字段同构、查询常按类型过滤。本实例只有两种异构类型,分表更清晰。
3. 多宠物统计
summary 的 reduce 已支持多宠物汇总。按宠物分组统计:GROUP BY pet_id(SQL)或 views 分组(应用层)。宠物数量少(1~5 只),应用层分组足够。
4. 疫苗历史(完整免疫记录)
next_vaccine 只存"下次"。完整免疫史需 vaccine_record 表(疫苗名/接种日/下次)。升级路径:pet 主表 + vaccine_record 从表,列表显示最近一次,提醒用最新记录的 next_date。本实例简化存主表单值,扩展时拆表。
5. FAQ
Q1:为什么体重和喂食不合成一张表?
A:字段异构(weight REAL vs food+amount TEXT),分开类型安全、查询互不干扰。若字段同构才考虑合并。表设计跟着字段结构走。
Q2:今日喂食的"今日"是自然日吗?
A:是,today0.setHours(0,0,0,0) 对齐本地时区的自然日零点。跨时区(带宠物旅行)按设备时区计,符合使用直觉。
Q3:疫苗到期负值怎么处理?
A:vaccineDueDays < 0 表示已到期,页面显示"疫苗已到期 X 天"红色。筛选 <= days 会包含负值(已到期当然算临期)。负值在数据层是信号,UI 层翻译为警示。
Q4:体重记录能删除/修改吗?
A:本实例只增。误记录可加 deleteWeight(按 id 删)。时间序列的修改需谨慎(趋势图会跳变),一般只删不改。历史记录删除比修改更安全。
Q5:avatar 头像字段怎么用?
A:预留字段。可用 photoAccessHelper 选图,存沙箱路径(参考 21 签名板的保存流程),列表加载 image 组件显示。文件路径存库、文件本体存沙箱。
八、下篇预告
下一篇《页面 UI 与操作实现》将完成:宠物卡片列表(疫苗三态徽标 + 今日喂食)、详情抽屉(体重记录 + 喂食记录双列表)、记体重/记喂食/疫苗设置三个弹窗、删除确认。敬请期待。
更多推荐




所有评论(0)