Files
dorm-power-monitor/go/history.go
fallensigh b2ad61e3fe feat: 核心电费查询 Go 服务
- FasterLZU 登录 + xiaofubao 6 步电费链路

- shiroJID 会话缓存 + 每日记录存储

- 定时任务: 每天 00:00 记录, 失败重试 5 次

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

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

254 lines
6.1 KiB
Go
Raw 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.
package main
import (
"encoding/json"
"os"
"sort"
"strings"
"sync"
"time"
)
// history.go - 每日电费记录存储JSON 文件)与耗电统计
type record struct {
Date string `json:"date"` // YYYY-MM-DD
Surplus float64 `json:"surplus"`
Amount float64 `json:"amount"`
Source string `json:"source"` // auto / manual
CreatedAt int64 `json:"created_at"`
}
type dailyStat struct {
Date string `json:"date"`
Surplus float64 `json:"surplus"`
Amount float64 `json:"amount"`
Usage *float64 `json:"usage"` // nil 表示无前一天基线
}
// monthSummary 月度汇总
type monthSummary struct {
Month string `json:"month"` // YYYY-MM
RecordCount int `json:"record_count"` // 记录天数
StartSurplus *float64 `json:"start_surplus"` // 月初首次电量
EndSurplus *float64 `json:"end_surplus"` // 月末末次电量
TotalUsage *float64 `json:"total_usage"` // 月总耗电
AvgUsage *float64 `json:"avg_usage"` // 日均耗电
RechargeDays int `json:"recharge_days"` // 疑似充值天数(电量回升)
MaxUsage *float64 `json:"max_usage"` // 单日最大耗电
}
var (
historyPath = envOr("HISTORY_FILE", "data/electric_history.json")
historyLock sync.Mutex
historyLoaded bool
historyCache []record
)
// loadHistory 读取历史记录
func loadHistory() []record {
historyLock.Lock()
defer historyLock.Unlock()
if historyLoaded {
return historyCache
}
b, err := os.ReadFile(historyPath)
if err != nil {
historyLoaded = true
return nil
}
_ = json.Unmarshal(b, &historyCache)
historyLoaded = true
return historyCache
}
// saveHistory 持久化历史记录
func saveHistory(records []record) error {
historyLock.Lock()
defer historyLock.Unlock()
if err := os.MkdirAll(dirName(historyPath), 0o755); err != nil {
return err
}
b, _ := json.MarshalIndent(records, "", " ")
if err := os.WriteFile(historyPath, b, 0o644); err != nil {
return err
}
historyCache = records
return nil
}
// hasRecord 判断指定日期是否已有记录
func hasRecord(dateStr string) bool {
for _, r := range loadHistory() {
if r.Date == dateStr {
return true
}
}
return false
}
// addRecord 写入(或覆盖)一条记录
func addRecord(dateStr string, surplus, amount float64, source string) error {
records := loadHistory()
updated := false
for i, r := range records {
if r.Date == dateStr {
records[i] = record{Date: dateStr, Surplus: surplus, Amount: amount, Source: source, CreatedAt: time.Now().Unix()}
updated = true
break
}
}
if !updated {
records = append(records, record{Date: dateStr, Surplus: surplus, Amount: amount, Source: source, CreatedAt: time.Now().Unix()})
}
return saveHistory(records)
}
// getRecords 返回降序(新→旧)记录列表
func getRecords() []record {
records := loadHistory()
sort.Slice(records, func(i, j int) bool {
return records[i].Date > records[j].Date
})
return records
}
// buildDailyStats 按日期升序生成每日统计(含日耗电 = 前一天剩余 - 当天剩余)
// 注意: 耗电计算跨月时使用上月末记录作为基线(若存在)
func buildDailyStats(records []record, month string) []dailyStat {
all := loadHistory()
// 若指定月份,额外包含上一月最后一条记录作为基线(不计入本月输出)
var prevMonthLast *record
if month != "" {
prevPrefix := prevMonth(month) + "-"
var last *record
for i := range all {
if strings.HasPrefix(all[i].Date, prevPrefix) {
if last == nil || all[i].Date > last.Date {
last = &all[i]
}
}
}
prevMonthLast = last
}
// 筛选目标记录
var target []record
if month != "" {
target = filterByMonth(records, month)
} else {
target = records
}
sort.Slice(target, func(i, j int) bool {
return target[i].Date < target[j].Date
})
var daily []dailyStat
var prevSurplus *float64
if prevMonthLast != nil {
prevSurplus = &prevMonthLast.Surplus
}
for _, r := range target {
var usage *float64
if prevSurplus != nil {
u := round2(*prevSurplus - r.Surplus)
if u < 0 {
u = 0 // 充值/异常归零
}
usage = &u
}
daily = append(daily, dailyStat{Date: r.Date, Surplus: r.Surplus, Amount: r.Amount, Usage: usage})
s := r.Surplus
prevSurplus = &s
}
return daily
}
// prevMonth 返回上个月的 YYYY-MM
func prevMonth(month string) string {
t, err := time.Parse("2006-01", month)
if err != nil {
return ""
}
return t.AddDate(0, -1, 0).Format("2006-01")
}
// filterByMonth 按月份(YYYY-MM)筛选记录
func filterByMonth(records []record, month string) []record {
if month == "" {
return records
}
prefix := month + "-"
var out []record
for _, r := range records {
if strings.HasPrefix(r.Date, prefix) {
out = append(out, r)
}
}
return out
}
// buildMonthSummary 生成某月的月度汇总
func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
if len(daily) == 0 {
return &monthSummary{Month: month, RecordCount: 0}
}
// daily 已是升序
m := month
if m == "" {
m = daily[0].Date[:7]
}
start := daily[0].Surplus
end := daily[len(daily)-1].Surplus
var total float64
var max float64
rechargeDays := 0
countWithUsage := 0
for _, d := range daily {
if d.Usage != nil {
total += *d.Usage
if *d.Usage > max {
max = *d.Usage
}
countWithUsage++
}
// 疑似充值:当天剩余比前一天高
}
// 充值与最大耗电基于原始升序记录重新精确计算
if len(daily) >= 2 {
rechargeDays = countRecharges(daily)
}
var totalP, avgP, maxP, startP, endP *float64
startP, endP = &start, &end
totalP = &total
if countWithUsage > 0 {
avg := round2(total / float64(countWithUsage))
avgP = &avg
}
maxP = &max
return &monthSummary{
Month: m,
RecordCount: len(daily),
StartSurplus: startP,
EndSurplus: endP,
TotalUsage: totalP,
AvgUsage: avgP,
RechargeDays: rechargeDays,
MaxUsage: maxP,
}
}
// countRecharges 统计疑似充值天数(当天剩余较前一天回升)
func countRecharges(daily []dailyStat) int {
n := 0
for i := 1; i < len(daily); i++ {
if daily[i].Surplus > daily[i-1].Surplus {
n++
}
}
return n
}