# 宿舍电费监控 兰州大学宿舍电费实时监控与每日耗电统计 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(推荐) ```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 签名密钥(由环境注入)**必填** | | `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` | 全部记录数 | **跨月基线**:指定月份时,月内第一天的耗电以上月末记录为基线计算(跨月衔接正确)。 ## 定时任务说明 - **触发时机**:每天 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//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): **按月查询**——月份选择器导航、月度汇总(总耗电/日均/疑似充值/月初月末电量)、跨月耗电基线衔接