采用分级聚合策略,使用异步IO优化性能,仅记录日期→余额映射,不计算统计指标。
aiofiles 和 asyncio 并发读取所有文件database/
├── [校区]/[楼栋]/[房间]-[id]/[日期].json # 原始每日数据
│
└── summaries/ # 聚合数据目录
├── overview.json # 总览(所有校区汇总)
│
└── campuses/ # 校区级数据
├── 仙林校区/
│ ├── summary.json # 校区汇总
│ │
│ └── buildings/ # 楼栋级数据
│ ├── 19幢/
│ │ ├── summary.json # 楼栋汇总
│ │ │
│ │ └── rooms/ # 房间级数据
│ │ ├── 53463.json # 房间详情(日期→余额)
│ │ └── 53464.json
│ │
│ └── 20幢/
│ └── ...
│
└── 鼓楼校区/
└── ...
路径: database/summaries/overview.json
用途: 快速了解所有校区的整体情况
数据结构:
{
"generated_at": "2026-05-15T02:05:00Z",
"total_rooms": 500,
"query_success_rate": 0.98,
"campuses": {
"仙林校区": {
"total_rooms": 350,
"avg_balance": 125.50,
"min_balance": 20.00,
"max_balance": 300.00,
"avg_trend": -0.15
},
"鼓楼校区": {
"total_rooms": 150,
"avg_balance": 130.20,
"min_balance": 15.00,
"max_balance": 280.00,
"avg_trend": -0.12
}
}
}
文件大小: < 10KB
加载时间: < 100ms
路径: database/summaries/campuses/仙林校区/summary.json
用途: 查看特定校区的所有楼栋情况
数据结构:
{
"campus": "仙林校区",
"generated_at": "2026-05-15T02:05:00Z",
"aggregate": {
"total_rooms": 350,
"avg_balance": 125.50,
"min_balance": 20.00,
"max_balance": 300.00,
"avg_trend": -0.15
},
"buildings": {
"19幢": {
"total_rooms": 50,
"avg_balance": 128.30,
"min_balance": 25.00,
"max_balance": 290.00,
"avg_trend": -0.13
},
"20幢": {
"total_rooms": 48,
"avg_balance": 122.40,
"min_balance": 22.00,
"max_balance": 285.00,
"avg_trend": -0.16
}
}
}
文件大小: < 50KB
加载时间: < 200ms
路径: database/summaries/campuses/仙林校区/buildings/19幢/summary.json
用途: 查看特定楼栋的所有房间概览
数据结构:
{
"building": "19幢",
"campus": "仙林校区",
"generated_at": "2026-05-15T02:05:00Z",
"aggregate": {
"total_rooms": 50,
"avg_balance": 128.30,
"min_balance": 25.00,
"max_balance": 290.00,
"avg_trend": -0.13
},
"rooms": {
"53463": {
"room_name": "19栋第16层1613",
"current_balance": 125.50,
"avg_7d": 128.30,
"trend_30d": -0.15,
"last_updated": "2026-05-15T02:00:00Z"
},
"53464": {
"room_name": "19栋第16层1614",
"current_balance": 130.20,
"avg_7d": 132.10,
"trend_30d": -0.12,
"last_updated": "2026-05-15T02:00:00Z"
}
}
}
文件大小: < 100KB (50个房间)
加载时间: < 300ms
路径: database/summaries/campuses/仙林校区/buildings/19幢/rooms/53463.json
用途: 查看特定房间的历史余额数据
数据结构:
{
"room_id": "53463",
"room_name": "19栋第16层1613",
"campus": "仙林校区",
"building": "19幢",
"current_balance": 125.50,
"balance_history": {
"20260501": 135.20,
"20260502": 133.80,
"20260503": 132.40,
"...": "...",
"20260515": 125.50
},
"last_updated": "20260515"
}
特点:
balance_history: 日期 → 余额的映射,前端可自行计算统计指标前端计算统计指标示例:
// 从balance_history计算统计指标
const balances = Object.values(room.balance_history);
const stats = {
current: room.current_balance,
avg7d: balances.slice(-7).reduce((a,b) => a+b, 0) / 7,
avg30d: balances.reduce((a,b) => a+b, 0) / balances.length,
min: Math.min(...balances),
max: Math.max(...balances)
};
// 加载总览数据
async function loadOverview() {
const response = await fetch('database/summaries/overview.json');
const data = await response.json();
// 显示校区列表
const campuses = Object.keys(data.campuses);
// 显示统计卡片
document.getElementById('totalRooms').textContent = data.total_rooms;
document.getElementById('avgBalance').textContent =
calculateOverallAvg(data.campuses).toFixed(2);
}
// 计算所有校区平均余额
function calculateOverallAvg(campuses) {
const total = Object.values(campuses)
.reduce((sum, c) => sum + c.avg_balance * c.total_rooms, 0);
const count = Object.values(campuses)
.reduce((sum, c) => sum + c.total_rooms, 0);
return total / count;
}
// 加载校区数据
async function loadCampus(campusName) {
const response = await fetch(
`database/summaries/campuses/${campusName}/summary.json`
);
const data = await response.json();
// 显示楼栋列表
const buildings = Object.keys(data.buildings);
// 显示校区统计
document.getElementById('campusAvg').textContent =
data.aggregate.avg_balance;
document.getElementById('campusTotal').textContent =
data.aggregate.total_rooms;
}
// 加载楼栋数据
async function loadBuilding(campusName, buildingName) {
const response = await fetch(
`database/summaries/campuses/${campusName}/buildings/${buildingName}/summary.json`
);
const data = await response.json();
// 显示房间列表(简略信息)
const rooms = Object.entries(data.rooms).map(([id, info]) => ({
id,
name: info.room_name,
balance: info.current_balance,
trend: info.trend_30d
}));
renderRoomList(rooms);
}
// 加载房间详细数据
async function loadRoom(campusName, buildingName, roomId) {
const response = await fetch(
`database/summaries/campuses/${campusName}/buildings/${buildingName}/rooms/${roomId}.json`
);
const data = await response.json();
// 显示完整统计信息
renderRoomChart(data);
renderRoomStats(data);
}
// 首次只加载总览
loadOverview();
// 用户点击校区时再加载校区数据
document.getElementById('campusList').addEventListener('click', (e) => {
const campusName = e.target.dataset.campus;
loadCampus(campusName);
});
// 用户点击楼栋时再加载楼栋数据
document.getElementById('buildingList').addEventListener('click', (e) => {
const buildingName = e.target.dataset.building;
const campusName = e.target.dataset.campus;
loadBuilding(campusName, buildingName);
});
| 房间数量 | 同步处理时间 | 异步处理时间 | 提速 |
|---|---|---|---|
| 100个房间 | ~15秒 | ~2秒 | 7.5x |
| 500个房间 | ~75秒 | ~8秒 | 9.4x |
| 1000个房间 | ~150秒 | ~15秒 | 10x |
| 文件类型 | 单文件聚合 | 分级聚合 | 减少 |
|---|---|---|---|
| 总览 | 500KB (全部房间) | 10KB (仅统计) | 98% |
| 校区 | 500KB (全部房间) | 50KB (该校区) | 90% |
| 楼栋 | 500KB (全部房间) | 100KB (该楼栋) | 80% |
| 房间 | 500KB (全部房间) | 2KB (仅该房间) | 99.6% |
旧版(包含统计指标):
{
"current_balance": 125.50,
"avg_7d": 128.30,
"avg_30d": 130.45,
"trend_30d": -0.15,
"min_30d": 120.00,
"max_30d": 135.20
}
新版(仅日期→余额):
{
"current_balance": 125.50,
"balance_history": {
"20260501": 135.20,
"20260502": 133.80,
...
}
}
优势:
// 加载总览数据
async function loadOverview() {
const response = await fetch('database/summaries/overview.json');
const data = await response.json();
// 显示校区列表
const campuses = Object.keys(data.campuses);
// 显示统计卡片
document.getElementById('totalRooms').textContent = data.total_rooms;
}
loadOverview();
// 加载校区数据
async function loadCampus(campusName) {
const response = await fetch(
`database/summaries/campuses/${campusName}/summary.json`
);
const data = await response.json();
// 显示楼栋列表
const buildings = Object.keys(data.buildings);
// 显示校区统计
document.getElementById('campusTotal').textContent = data.total_rooms;
}
// 加载楼栋数据
async function loadBuilding(campusName, buildingName) {
const response = await fetch(
`database/summaries/campuses/${campusName}/buildings/${buildingName}/summary.json`
);
const data = await response.json();
// 显示房间列表
const rooms = Object.entries(data.rooms).map(([id, info]) => ({
id,
name: info.room_name,
balance: info.current_balance
}));
renderRoomList(rooms);
}
// 加载房间数据并计算统计指标
async function loadRoom(campusName, buildingName, roomId) {
const response = await fetch(
`database/summaries/campuses/${campusName}/buildings/${buildingName}/rooms/${roomId}.json`
);
const data = await response.json();
// 计算统计指标
const balances = Object.values(data.balance_history);
const dates = Object.keys(data.balance_history).sort();
const stats = {
current: data.current_balance,
avg7d: calculateAverage(balances.slice(-7)),
avg30d: calculateAverage(balances),
min: Math.min(...balances),
max: Math.max(...balances),
trend: calculateTrend(dates, balances)
};
// 绘制趋势图
renderChart(dates, balances);
renderStats(stats);
}
function calculateAverage(values) {
return values.reduce((a, b) => a + b, 0) / values.length;
}
function calculateTrend(dates, balances) {
// 简单线性趋势计算
const n = balances.length;
const avgX = (n - 1) / 2;
const avgY = balances.reduce((a, b) => a + b, 0) / n;
let numerator = 0;
let denominator = 0;
for (let i = 0; i < n; i++) {
numerator += (i - avgX) * (balances[i] - avgY);
denominator += Math.pow(i - avgX, 2);
}
return numerator / denominator; // 斜率
}
首页加载: 500KB 总传输量: 500KB
**分级聚合**:
首页加载: 10KB (overview) 点击校区: +50KB = 60KB 点击楼栋: +100KB = 160KB 点击房间: +1KB = 161KB 总传输量: 161KB (减少68%)
---
## 扩展性优势
### 1. 支持大规模房间
- **单文件聚合**: 1000个房间 → 1MB+ 文件,加载缓慢
- **分级聚合**: 1000个房间 → 总览仍 < 10KB,按需加载
### 2. 易于缓存
```javascript
// 前端可轻松实现缓存
const cache = {
overview: null,
campuses: {},
buildings: {},
rooms: {}
};
async function getCachedData(type, ...keys) {
const cacheKey = keys.join('/');
if (!cache[type][cacheKey]) {
const path = buildPath(type, ...keys);
cache[type][cacheKey] = await fetch(path).then(r => r.json());
}
return cache[type][cacheKey];
}
// 同时加载多个楼栋数据
const buildings = ['19幢', '20幢', '21幢'];
const buildingData = await Promise.all(
buildings.map(b =>
fetch(`database/summaries/campuses/仙林校区/buildings/${b}/summary.json`)
.then(r => r.json())
)
);
# 重新生成所有分级聚合文件
python scripts/aggregate_data.py \
--database ./database \
--output ./database/summaries
# 输出:
# ✓ Hierarchical summaries generated:
# Total rooms: 500
# Campuses: 2
# Output: ./database/summaries
# 检查总览文件
cat database/summaries/overview.json | jq
# 检查特定校区
cat database/summaries/campuses/仙林校区/summary.json | jq
# 检查特定楼栋
cat database/summaries/campuses/仙林校区/buildings/19幢/summary.json | jq
# 检查特定房间
cat database/summaries/campuses/仙林校区/buildings/19幢/rooms/53463.json | jq
原始数据: database/仙林校区/19幢/19栋第16层1613-53463/20260515.json
↓ (aggregate_data.py 处理)
聚合数据: database/summaries/campuses/仙林校区/buildings/19幢/rooms/53463.json
原始数据: 所有房间的每日JSON文件
↓ (aggregate_data.py 处理)
聚合数据: database/summaries/overview.json + 各级summary.json
注意:
summaries/ 目录