feat: 今天环境条新增下一个倒数日、浙江油价、上证指数、降雨/AQI
ci / gitleaks (push) Successful in 11s
ci / docker (push) Successful in 3m52s

- 后端新增三源:中石化浙江油价(3步会话)、新浪上证行情、Open-Meteo 空气质量(国标AQI换算),按源独立超时与状态
- 天气行改为 降雨概率% · AQI 等级(无雨无AQI时保留观测时间),stale 补缓存标记
- 前端 strip 六格两行(桌面)/四行(移动),倒数日取本地缓存最近一条,超12字截断
- 修复两处 AppSheet 动画竞态存量 e2e 失败(modal contract footer 时序、scrollable 重开残留 dialog)
This commit is contained in:
2026-09-25 09:50:32 +08:00
parent 4c145a2621
commit 273cd7aac7
11 changed files with 1212 additions and 34 deletions
+134 -4
View File
@@ -125,7 +125,11 @@ async def test_fetch_weather_uses_fixed_haishu_location_and_current_conditions()
"temperature_2m": 28.4,
"apparent_temperature": 30.1,
"weather_code": 2,
}
},
"hourly": {
"time": ["2026-09-16T14:00", "2026-09-16T15:00", "2026-09-16T16:00"],
"precipitation_probability": [40, 75, None],
},
}
result = await environment.fetch_weather(request)
@@ -136,6 +140,7 @@ async def test_fetch_weather_uses_fixed_haishu_location_and_current_conditions()
"latitude": 29.88,
"longitude": 121.55,
"current": "temperature_2m,apparent_temperature,weather_code",
"hourly": "precipitation_probability",
"timezone": "Asia/Shanghai",
},
}
@@ -143,6 +148,7 @@ async def test_fetch_weather_uses_fixed_haishu_location_and_current_conditions()
"temperature_c": 28.4,
"apparent_temperature_c": 30.1,
"weather_code": 2,
"precipitation_probability": 75,
"observed_at": "2026-09-16T14:15:00+08:00",
"source": "Open-Meteo",
}
@@ -171,6 +177,102 @@ async def test_fetch_weather_rejects_invalid_contract(current):
await environment.fetch_weather(request)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"hourly",
[
None,
[],
"now",
{"time": ["2026-09-16T14:00"]},
{"time": [], "precipitation_probability": []},
{"time": ["2026-09-16T14:00"], "precipitation_probability": [10, 20]},
{"time": [123], "precipitation_probability": [10]},
{"time": ["2026-09-16T14:00"], "precipitation_probability": ["40"]},
{"time": ["2026-09-16T14:00"], "precipitation_probability": [True]},
{"time": ["2026-09-16T14:00"], "precipitation_probability": [120]},
{"time": ["2026-09-16T14:00"], "precipitation_probability": [-1]},
],
)
async def test_fetch_weather_rejects_invalid_precipitation_hourly(hourly):
payload = {
"timezone": "Asia/Shanghai",
"current": {
"time": "2026-09-16T14:15",
"temperature_2m": 28.4,
"apparent_temperature": 30.1,
"weather_code": 2,
},
}
if hourly is not None:
payload["hourly"] = hourly
async def request(*_args, **_kwargs):
return payload
with pytest.raises((KeyError, TypeError, ValueError)):
await environment.fetch_weather(request)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("probabilities", "expected"),
[
([40, 75, None], 75),
([None, None, None], None),
([0], 0),
([None, 60], 60),
],
)
async def test_fetch_weather_precipitation_probability_window(probabilities, expected):
payload = {
"timezone": "Asia/Shanghai",
"current": {
"time": "2026-09-16T14:15",
"temperature_2m": 28.4,
"apparent_temperature": 30.1,
"weather_code": 2,
},
"hourly": {
"time": [
"2026-09-16T13:00",
"2026-09-16T14:00",
"2026-09-16T15:00",
"2026-09-16T16:00",
][: len(probabilities) + 1],
"precipitation_probability": [99, *probabilities],
},
}
async def request(*_args, **_kwargs):
return payload
result = await environment.fetch_weather(request)
assert result["precipitation_probability"] == expected
@pytest.mark.asyncio
async def test_fetch_weather_precipitation_probability_none_when_hour_slot_missing():
payload = {
"timezone": "Asia/Shanghai",
"current": {
"time": "2026-09-16T14:15",
"temperature_2m": 28.4,
"apparent_temperature": 30.1,
"weather_code": 2,
},
"hourly": {"time": ["2026-09-20T14:00"], "precipitation_probability": [40]},
}
async def request(*_args, **_kwargs):
return payload
result = await environment.fetch_weather(request)
assert result["precipitation_probability"] is None
@pytest.mark.asyncio
async def test_fetch_weather_rejects_wrong_response_timezone():
async def request(*_args, **_kwargs):
@@ -613,6 +715,10 @@ async def test_creator_cancellation_does_not_close_shared_default_fetch_client(m
"apparent_temperature": 26,
"weather_code": 1,
},
"hourly": {
"time": ["2026-09-16T14:00"],
"precipitation_probability": [10],
},
}
@property
@@ -774,7 +880,7 @@ def test_today_environment_endpoint_returns_aggregated_payload(client, monkeypat
"/api/v1/setup/initialize",
json={"username": "owner", "password": "correct horse battery staple"},
)
payload = {
environment_payload = {
"date": {
"solar_date": "2026-09-16",
"weekday": "星期三",
@@ -785,12 +891,36 @@ def test_today_environment_endpoint_returns_aggregated_payload(client, monkeypat
"gold": None,
"errors": {"weather": "unavailable", "gold": "unavailable"},
}
extras_payload = {
"air": None,
"oil": None,
"ashare": None,
"errors": {"air": "unavailable", "oil": "unavailable", "ashare": "unavailable"},
}
async def aggregate():
return payload
return environment_payload
async def aggregate_extras():
return extras_payload
monkeypatch.setattr("backend.main.get_today_environment", aggregate)
monkeypatch.setattr("backend.main.get_today_environment_extras", aggregate_extras)
response = client.get("/api/v1/today/environment")
assert response.status_code == 200
assert response.json() == payload
assert response.json() == {
"date": environment_payload["date"],
"weather": None,
"gold": None,
"air": None,
"oil": None,
"ashare": None,
"errors": {
"weather": "unavailable",
"gold": "unavailable",
"air": "unavailable",
"oil": "unavailable",
"ashare": "unavailable",
},
}