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 表示无前一天基线 Recharge *float64 `json:"recharge"` // 当天充值电量(度), nil=无充值 Money *float64 `json:"money"` // 当天充值金额(元), 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"` // 充值天数 RechargeKwh *float64 `json:"recharge_kwh"` // 月充值电量(度) RechargeMoney *float64 `json:"recharge_money"` // 月充值金额(元) 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 按日期升序生成每日统计(含日耗电 = 前一天剩余 - 当天剩余 + 当天充值电量) // 注意: 耗电计算跨月时使用上月末记录作为基线(若存在) // rechargeMap: date -> 当天充值电量(度);精确修正充值当天的耗电 func buildDailyStats(records []record, month string, rechargeMap map[string]float64) []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 + rechargeMap[r.Date]) if u < 0 { u = 0 // 极端异常(数据缺口)归零 } usage = &u } var recharge, money *float64 if kwh, ok := rechargeMap[r.Date]; ok && kwh > 0 { recharge = &kwh m := round2(kwh * unitPrice()) money = &m } daily = append(daily, dailyStat{ Date: r.Date, Surplus: r.Surplus, Amount: r.Amount, Usage: usage, Recharge: recharge, Money: money, }) 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 var totalRechargeKwh float64 var totalRechargeMoney 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 d.Recharge != nil { totalRechargeKwh += *d.Recharge rechargeDays++ } if d.Money != nil { totalRechargeMoney += *d.Money } } var totalP, avgP, maxP, startP, endP, rkwhP, rmoneyP *float64 startP, endP = &start, &end totalP = &total if countWithUsage > 0 { avg := round2(total / float64(countWithUsage)) avgP = &avg } maxP = &max if totalRechargeKwh > 0 { rkwhP = &totalRechargeKwh } if totalRechargeMoney > 0 { rmoneyP = &totalRechargeMoney } return &monthSummary{ Month: m, RecordCount: len(daily), StartSurplus: startP, EndSurplus: endP, TotalUsage: totalP, AvgUsage: avgP, RechargeDays: rechargeDays, RechargeKwh: rkwhP, RechargeMoney: rmoneyP, MaxUsage: maxP, } }