docs: 完整使用文档(部署/配置/API/内存分析)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
305
go/README.md
Normal file
305
go/README.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 宿舍电费监控
|
||||
|
||||
兰州大学宿舍电费实时监控与每日耗电统计 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/<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): **按月查询**——月份选择器导航、月度汇总(总耗电/日均/疑似充值/月初月末电量)、跨月耗电基线衔接
|
||||
Reference in New Issue
Block a user