- 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>
254 lines
6.1 KiB
Go
254 lines
6.1 KiB
Go
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
|
||
}
|