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:
253
go/history.go
Normal file
253
go/history.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user