Files
dorm-power-monitor/go/README.md
fallensigh f2cb2e796d feat: 前端充值信息展示 + 文档更新
- 表格新增充值电量列, 汇总卡新增充值统计

- README 更新精确计算说明与 XF_PRICE 配置

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-23 21:43:36 +08:00

317 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 宿舍电费监控
兰州大学宿舍电费实时监控与每日耗电统计 Web 服务。
基于 FasterLZU 登录接口 + 小富婆xiaofubao一码通电费查询接口构建。纯 Go 标准库实现零第三方依赖单二进制部署Docker 镜像仅 **6.2 MB**
---
## 功能特性
- **实时电费**Web 页面实时显示当前剩余电量、预估金额、房间信息30 秒自动刷新,可手动刷新
- **每日耗电统计**:每天 24:0000:00自动查询并记录电费日耗电 = 前一天剩余 - 当天剩余7 天柱状图 + 历史记录表
- **失败重试**:定时记录失败自动重试 5 次(间隔 60 秒),次日 00:00-00:05 窗口内可补记
- **会话缓存**:复用 application 域 shiroJID 会话(约 7 天有效),命中缓存时仅 2 个请求即可查电费;失效自动重新登录
- **数据持久化**:历史记录与会话缓存写入 JSON 文件,挂载 Docker 卷持久保存
## 架构
```
浏览器 (HTML+CSS+JS)
│ http://localhost:8000
Go Web 服务 (单二进制, go:embed 内嵌前端)
├── /api/electric 实时电费查询
├── /api/history 历史记录 + 耗电统计
└── 定时任务 每天 00:00 记录失败重试5次
├── FasterLZU 登录 (appservice.lzu.edu.cn, AES-CBC 加密)
│ └── getSt → ST 票据
└── xiaofubao 链路 (6 步)
getCodeV2 → getUserByCodeV2 → getUserCode
→ getUser4Authorize → queryBind → queryISIMSRoomSurplus
```
### 数据流
```
[查询请求]
├─ 缓存命中? ──是──→ 直接 queryISIMSRoomSurplus (2 个请求)
└─ 否 ──→ FasterLZU 登录 → getSt → xiaofubao 6 步 → 回写缓存
[结果] { room, surplus, amount, status }
├─ 定时任务 → 写入 electric_history.json
└─ API 响应 → 浏览器渲染
```
## 目录结构
```
final/web/go/
├── main.go # HTTP 服务器 + 定时任务 + go:embed 静态文件
├── electric.go # xiaofubao 6 步链路 + shiroJID 缓存
├── lzuapi.go # FasterLZU 登录客户端 (AES 加密)
├── crypto.go # AES-CBC 加密/解密 + MD5 签名
├── history.go # 每日记录 JSON 存储 + 耗电统计
├── static/ # 前端 (go:embed 嵌入二进制)
│ ├── index.html
│ ├── style.css
│ └── app.js
├── Dockerfile # 多阶段构建 → 6.2MB 镜像
├── docker-compose.yml
├── .env.example # 环境配置模板
└── data/ # 运行时数据 (挂载卷, 不入库)
├── electric_cache.json # shiroJID 会话缓存
└── electric_history.json # 每日电费记录
```
## 快速开始
### 方式一Docker Compose推荐
```bash
cd final/web/go
# 1. 准备配置
cp .env.example .env
# 编辑 .env填入真实账号密码
# LZU_USERNAME=你的学号
# LZU_PASSWORD=你的密码
# 2. 构建并启动
docker compose up -d --build
# 3. 访问
# http://localhost:8000
```
### 方式二docker run
```bash
docker build -t electric-monitor .
docker run -d --name electric-monitor \
-p 8000:8000 \
-e LZU_USERNAME=学号 \
-e LZU_PASSWORD=密码 \
-v $(pwd)/data:/app/data \
electric-monitor
```
### 方式三WSL/本机直接运行
```bash
cd final/web/go
# 直接编译运行(需 Go 1.18+
go build -o electric-monitor .
LZU_USERNAME=学号 LZU_PASSWORD=密码 ./electric-monitor
```
## 配置项
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `HOST` | `0.0.0.0` | 监听地址 |
| `PORT` | `8000` | 监听端口 |
| `LZU_USERNAME` | — | FasterLZU 登录账号(学号)**必填** |
| `LZU_PASSWORD` | — | FasterLZU 登录密码 **必填** |
| `LZU_SERVICE_ID` | `29615` | 宿舍电费服务 ID |
| `XF_PLATFORM` | `QHIT_EDU` | xiaofubao 平台标识 |
| `XF_AUTH_APPID` | `2606161276416311297` | xiaofubao 应用 ID |
| `XF_SCHOOL_CODE` | `10730` | 学校代码(兰州大学)|
| `XF_YM_APPID` | `1810181825222034` | 一码通应用 ID |
| `LZU_AES_KEY` | — | AES 加密密钥16字节由环境注入**必填** |
| `LZU_MD5_KEY` | — | MD5 签名密钥(由环境注入)**必填** |
| `XF_PRICE` | `0.535` | 电价(元/度),用于充值金额→电量换算 |
| `CACHE_FILE` | `data/electric_cache.json` | 会话缓存路径 |
| `HISTORY_FILE` | `data/electric_history.json` | 历史记录路径 |
| `XF_CACHE_TTL` | `518400` (6天) | 会话缓存有效期(秒)|
## API 接口
### GET /api/electric — 实时电费
```json
{
"ok": true,
"room": "兰州大学榆中本科生29号公寓3层29-321",
"surplus": 72.57,
"amount": 38.82,
"status": "正常用电",
"source": "cache",
"time": 1787490436
}
```
| 字段 | 说明 |
|---|---|
| `ok` | 查询是否成功 |
| `room` | 房间全名 |
| `surplus` | 剩余电量(度)|
| `amount` | 预估金额(元)|
| `status` | 用电状态 |
| `source` | `cache`=缓存会话 / `login`=重新登录 |
| `time` | 查询时间Unix 秒)|
### GET /api/history — 历史记录与耗电统计(支持按月)
```
GET /api/history # 全部记录
GET /api/history?month=2026-08 # 指定月份 (YYYY-MM)
```
```json
{
"ok": true,
"month": "2026-08",
"months": ["2026-08", "2026-07"],
"records": [
{"date": "2026-08-01", "surplus": 83.5, "amount": 44, "usage": 0.7}
],
"daily": [
{"date": "2026-08-01", "surplus": 83.5, "amount": 44, "usage": 0.7}
],
"summary": {
"month": "2026-08",
"record_count": 5,
"start_surplus": 83.5,
"end_surplus": 72.5,
"total_usage": 11.7,
"avg_usage": 2.34,
"recharge_days": 0,
"max_usage": 8.9
},
"all_count": 7
}
```
| 字段 | 说明 |
|---|---|
| `month` | 当前查询月份(空=全部)|
| `months` | 所有有记录的月份(降序,供前端选择器)|
| `records` / `daily` | 该月每日统计(升序),`usage`=日耗电null=无基线)|
| `summary` | 月度汇总:记录天数 / 月初月末电量 / 月总耗电 / 日均 / 疑似充值天数 / 单日最大耗电 |
| `all_count` | 全部记录数 |
**跨月基线**:指定月份时,月内第一天的耗电以上月末记录为基线计算(跨月衔接正确)。
### 精确耗电计算(含充值修正)
- **电价**:默认 `0.535 元/度`(由余额接口反推,可用 `XF_PRICE` 覆盖)
- **充值记录接口**`POST /app/electric/queryISIMSRoomBuyRecord`areaId/buildingCode/floorCode/roomCode/mdtype=room
- **日耗电公式**`日耗电 = (昨天剩余 - 今天剩余) + 当天充值电量`(充值金额 × 1/电价)
- 充值当天不再归零:如充值 ¥30 → 56.07 度,该日耗电 = 剩余变化 + 56.07
- 月度汇总新增:`recharge_days`(充值次数)、`recharge_kwh`(充值电量)、`recharge_money`(充值金额)
- 充值记录 10 分钟缓存,接口失败时降级为无充值计算(余额回升归零)
## 定时任务说明
- **触发时机**:每天 00:00-00:05 窗口内Asia/Shanghai 时区)
- **执行逻辑**:查询当前电费 → 写入当日记录(`source: auto`
- **失败重试**:最多 5 次,间隔 60 秒;窗口内每分钟补记一次直至成功
- **启动基线**:服务启动时若今日无记录,立即查询建立基线(保证当日耗电可计算)
## 会话与缓存机制
- 认证核心是 **shiroJID** HttpOnly cookie非 localStorage分 webapp 域7 天)与 application 域(可复用)
- 缓存 application 域 shiroJID + 房间编码后,查询仅需 2 个请求queryBind + queryISIMSRoomSurplus
- 缓存约 6 天后自动失效,触发重新登录并回写缓存
- ST 票据getSt与 getUserCode 的 code 均为一次性,每次登录重新生成
## 内存占用
### 实测数据Docker 容器内运行)
| 指标 | 实测值 | 说明 |
|---|---|---|
| 容器 MEM USAGE | **1.4 - 6.6 MiB** | `docker stats` 实测(缓存命中时最低)|
| VmRSS物理内存| **11.4 MB** | 宿主机 `/proc/<pid>/status` |
| RssAnon堆/匿名页)| 6.0 MB | Go 堆 + 运行时数据 |
| RssFile二进制/库)| 5.4 MB | 可执行文件映射(多进程共享)|
| VmSize虚拟地址| 1.23 GB | Go 运行时保留的虚拟地址空间,**非实际占用** |
| 线程数 | 8 | Go runtime 调度器 + 主线程 |
| 镜像大小 | 20.6 MB | alpine 基础层 + 7.7MB 二进制 |
| 二进制大小 | 7.7 MB | `CGO_ENABLED=0` 纯静态 |
### 内存构成分析
```
11.4 MB VmRSS 总物理内存
├── ~5.5 MB 二进制镜像映射(.text/.rodata/embed 静态资源,跨进程共享)
├── ~2 MB Go runtime 基础GC、调度器、栈
├── ~2 MB HTTP 连接缓冲 + 协程栈
├── ~1 MB 内嵌静态资源index.html + style.css + app.js ≈ 9.5KB 实际极小)
└── ~1 MB 零散堆分配JSON 解析、字符串、缓存结构)
```
### 代码级内存分析Go 视角)
**固定成本(常驻):**
- **Go runtime 基础**GC、调度器、内存池mcache/arena~10-20MB RSS与业务无关是本服务内存的主要构成
- **go:embed 静态资源**9.4KB 内嵌二进制,运行时零额外占用(`serveStatic` 每次请求复制 ~4KB 瞬时)
- **配置字符串**~几百字节,静态
**增长项(唯一长期增长):**
- `historyCache` 每日 +1 条记录(~200B/条)→ **~70KB/年**。10 年才 ~700KB可忽略。历史文件同理`/api/history` 响应随记录数线性增长
**瞬态分配GC 回收):**
- 完整登录8 个 `http.Client`(共享 `DefaultTransport` 连接池,非泄漏)+ 8 个小 JSON 响应体,~几十 KB
- 缓存命中1 个 client + 1 个响应体,~几 KB
- JSON 编解码:`json.NewEncoder` 流式写出,无整包缓冲
**goroutine 生命周期:**
- `schedulerLoop`常驻无限循环30s 检查一次),设计如此,非泄漏
- 启动记录协程:成功或 5 次重试后退出(最长 ~5 分钟)
- HTTP 处理协程:连接关闭即回收
### 已实施的内存/稳定性优化
| 优化项 | 说明 |
|---|---|
| HTTP Server 超时 | `ReadHeaderTimeout=10s / ReadTimeout=30s / WriteTimeout=60s / IdleTimeout=90s`,防止空闲/慢速连接堆积 goroutine |
| 出站请求超时 | 所有 API 请求加 `context.WithTimeout(20s)`,上游无响应时 20s 自动放弃,防止 `queryLock` 永久阻塞导致请求排队 |
| 移除 debugMain | 修复编译阻断(原 `-debug` 分支引用未定义函数)|
### 与 Python 版对比
| 指标 | Python (requests + Flask) | Go (本版) |
|---|---|---|
| 运行时内存(常驻)| 30-60 MB解释器+依赖)| **~11 MB** |
| 启动速度 | 1-3 秒 | **<1 秒** |
| 部署体积 | 需 Python + pip 依赖 + venv | **单二进制 7.7MB / Docker 6.2MB** |
| 依赖数量 | requests/Flask 等 ≥5 个 | **0 个** |
| 内存泄漏风险 | 有Python 引用计数/循环引用)| 低Go GC + 自动回收)|
### 优化建议
- 本服务内存占用已极低1.4-6.6MiB**无需进一步优化**
- 若需长期运行多年:可考虑历史记录裁剪(保留最近 N 年),仅影响 ~700KB/10年 级别
- `GOGC`/`GOMEMLIMIT` 环境变量可微调 GC 频率(默认即可)
## 安全说明
- 账号密码、加密密钥通过环境变量注入,不进镜像层、不入代码仓库
- `.env` 已被 `.dockerignore` 排除,切勿提交到公开仓库
- 服务默认监听 `0.0.0.0:8000`,如仅本机使用建议 `HOST=127.0.0.1`
- 会话缓存与历史记录含个人数据,注意目录权限
## 常见问题
**Q: 查询返回 `ok: false`**
A: 查看容器日志定位具体失败步骤(`docker logs electric-monitor`)。常见原因:账号密码错误、网络不通、服务端会话异常(会自动重试/重登)。
**Q: 如何换绑定的房间?**
A: 房间来自 `queryBind` 接口返回的默认绑定,如更换宿舍需在 xiaofubao APP 中重新绑房后重启服务。
**Q: 定时记录失败怎么办?**
A: 自动重试 5 次 + 次日补记。若持续失败,检查账号是否到期、网络是否可达。
## 版本历史
- **v1.0** (2026-08-23): Go 版完整实现Docker 部署,实时监控 + 每日耗电统计
- **v1.0.1** (2026-08-23): 内存/稳定性优化——HTTP Server 超时、出站请求 context 超时、修复 debugMain 编译问题;补充内存分析文档
- **v1.1** (2026-08-23): **按月查询**——月份选择器导航、月度汇总(总耗电/日均/疑似充值/月初月末电量)、跨月耗电基线衔接
- **v1.2** (2026-08-23): **精确耗电计算**——接入充值记录接口 `queryISIMSRoomBuyRecord`、电价 0.535 元/度换算、充值当天耗电精确修正、月度汇总增加充值字段