[阶段8] 新增每日数据模块;趋势改为全指标并列 + 周期聚合
每日数据(新页面 /daily): - 按活动/能量/心率/压力/睡眠/血氧呼吸/训练七组,列出全部 40 项指标 - 日期选择器 + 前后一天翻页;"只显示有数据的指标"开关 - 附当天的运动记录明细 - 标题处显示当天记录到多少项,缺数据一目了然 趋势(重写): - 15 组指标全部并列展示,不再一次只能看一个 - 标签可逐个隐藏/显示,选择存入 localStorage(每次刷新都重置的 选择算不上偏好);提供全选/全不选 - 两级筛选:范围(一月/一季/半年/一年/两年)× 周期(每天/每 7 天/ 每月/每季度)。周期选项按范围过滤,避免出现"近一月按季度聚合" - 所有图表共用同一份聚合结果,一行筛选器统摄全部图表 聚合口径(lib/aggregate.ts): - 无论累计型还是速率型指标,一律折算为"周期内日均",这样 30 天的 月份和 31 天的月份不会仅因日历差 3% - 周按最新一天往回切,而不是按自然周一 —— 否则开头会出现一个半空 的桶,看起来像低谷,其实只是窗口起点 - 累计型指标的统计标签写作"日均"而非"平均",读者不必猜口径 fix(viz): 聚合后柱形/面积图掩盖了变化 - 柱形与面积都以延展量编码大小,必须从 0 起;而一年的月均步数都在 9,590~12,701 之间,画出来几乎一样高,恰恰看不见要看的变化 - 聚合视图改用折线:折线编码位置而非延展量,非零轴是正当的。 改后 y 轴自动落在 9350~12750,走势清晰可读 fix(health): 运动记录按日期筛选时 500 - get_activities 复用了按 date 列过滤的子句,但 activities 表只有 start_time,报 "Unknown column 'date'"。此前唯一的调用方不传 日期,所以一直没暴露,每日数据页一传就炸 - 上界改用次日零点的开区间:SQLite 按字符串比较,而存储的分隔符 可能是 'T'(0x54) 也可能是空格(0x20),写成 "end 23:59:59" 会让 当天 18:30 的记录排在上界之后而被排除在自己那天之外 tests (+6, 共 305): 运动记录的单日范围、上下界闭合、仅起点/仅终点、 不传范围返回全部、端点级验证 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,3 +254,52 @@ class TestBadgesAndRecords:
|
||||
assert isinstance(
|
||||
client.get("/api/health/personal-records", headers=auth).get_json(), list
|
||||
)
|
||||
|
||||
|
||||
class TestActivityDateRange:
|
||||
"""Regression: activities were filtered with the daily-metrics range
|
||||
clause, which references a `date` column the activities table does not
|
||||
have. It only failed once a caller actually passed a range."""
|
||||
|
||||
def seed(self, user):
|
||||
for stamp in ("2026-08-20T07:00:00", "2026-08-21T18:30:00",
|
||||
"2026-08-22T09:15:00"):
|
||||
health_svc.insert_activity(user["id"], {
|
||||
"activityType": "running",
|
||||
"startTime": stamp,
|
||||
"endTime": stamp,
|
||||
"duration": 1800,
|
||||
})
|
||||
|
||||
def test_single_day_range_does_not_raise(self, db, user):
|
||||
self.seed(user)
|
||||
rows = health_svc.get_activities(user["id"], "2026-08-21", "2026-08-21")
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["start_time"].startswith("2026-08-21")
|
||||
|
||||
def test_range_bounds_are_inclusive_of_the_whole_day(self, db, user):
|
||||
"""An activity at 18:30 must fall inside its own day."""
|
||||
self.seed(user)
|
||||
rows = health_svc.get_activities(user["id"], "2026-08-20", "2026-08-22")
|
||||
assert len(rows) == 3
|
||||
|
||||
def test_start_only(self, db, user):
|
||||
self.seed(user)
|
||||
assert len(health_svc.get_activities(user["id"], "2026-08-21")) == 2
|
||||
|
||||
def test_end_only(self, db, user):
|
||||
self.seed(user)
|
||||
assert len(health_svc.get_activities(user["id"], None, "2026-08-21")) == 2
|
||||
|
||||
def test_no_range_returns_everything(self, db, user):
|
||||
self.seed(user)
|
||||
assert len(health_svc.get_activities(user["id"])) == 3
|
||||
|
||||
def test_endpoint_with_a_date_range(self, client, auth, user, db):
|
||||
self.seed(user)
|
||||
r = client.get(
|
||||
"/api/health/activities?startDate=2026-08-21&endDate=2026-08-21",
|
||||
headers=auth,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert len(r.get_json()) == 1
|
||||
|
||||
Reference in New Issue
Block a user