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>
This commit is contained in:
2026-08-23 21:29:33 +08:00
commit b2ad61e3fe
6 changed files with 1122 additions and 0 deletions

253
go/history.go Normal file
View File

@@ -0,0 +1,253 @@
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
}