- 表格新增充值电量列, 汇总卡新增充值统计 - README 更新精确计算说明与 XF_PRICE 配置 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
13 KiB
13 KiB
宿舍电费监控
兰州大学宿舍电费实时监控与每日耗电统计 Web 服务。
基于 FasterLZU 登录接口 + 小富婆(xiaofubao)一码通电费查询接口构建。纯 Go 标准库实现(零第三方依赖),单二进制部署,Docker 镜像仅 6.2 MB。
功能特性
- 实时电费:Web 页面实时显示当前剩余电量、预估金额、房间信息,30 秒自动刷新,可手动刷新
- 每日耗电统计:每天 24:00(00: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(推荐)
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
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/本机直接运行
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 — 实时电费
{
"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)
{
"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 元/度换算、充值当天耗电精确修正、月度汇总增加充值字段