From 57b63316f309707eb3c3e9103ba3b26f5b12f124 Mon Sep 17 00:00:00 2001 From: fallensigh Date: Sun, 23 Aug 2026 21:43:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B2=BE=E7=A1=AE=E8=80=97=E7=94=B5?= =?UTF-8?q?=E8=AE=A1=E7=AE=97(=E6=8E=A5=E5=85=A5=E5=85=85=E5=80=BC?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 queryISIMSRoomBuyRecord 充值记录获取 + 10分钟缓存 - 日耗电 = 剩余变化 + 当天充值电量(电价0.535元/度) - 月度汇总增加充值次数/电量/金额 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- go/buy_records.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++ go/history.go | 86 ++++++++++++++++++++------------- go/main.go | 32 +++++++++---- 3 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 go/buy_records.go diff --git a/go/buy_records.go b/go/buy_records.go new file mode 100644 index 0000000..64b2c5e --- /dev/null +++ b/go/buy_records.go @@ -0,0 +1,118 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// buy_records.go - 充值/缴费记录获取与缓存 +// 接口: POST /app/electric/queryISIMSRoomBuyRecord +// 返回每笔充值记录(金额/类型/时间),用于精确计算日耗电 + +// buyRecord 单笔充值记录 +type buyRecord struct { + Date string `json:"date"` // 充值日期 YYYY-MM-DD + Money float64 `json:"money"` // 充值金额(元) + Kwh float64 `json:"kwh"` // 换算电量(度) = money / price + Type string `json:"type"` // 现金充值 / 下发补助 + Detail string `json:"detail"` // 完整时间 YYYY-M-D H:M:S +} + +// 充值记录缓存 +var ( + buyRecordsCache []buyRecord + buyRecordsTime int64 +) + +// queryBuyRecords 获取某房间的充值记录(需 application 域 shiroJID) +func queryBuyRecords(shiroJID string, room *roomInfo) ([]buyRecord, error) { + price := unitPrice() + s := newXfSession(shiroJID) + body := fmt.Sprintf("areaId=%s&buildingCode=%s&floorCode=%s&roomCode=%s&mdtype=room&platform=%s", + room.AreaID, room.BuildingCode, room.FloorCode, room.RoomCode, xfPlatform) + b, _, err := s.postForm("https://application.xiaofubao.com/app/electric/queryISIMSRoomBuyRecord", + body, "https://application.xiaofubao.com/") + if err != nil { + return nil, err + } + var res struct { + StatusCode flexInt `json:"statusCode"` + Rows []struct { + DateTime string `json:"datetime"` // "2026-8-23 0:45:49" + BuyType string `json:"buytype"` // 现金充值 / 下发补助 + BuyUsing string `json:"buyusingtpe"` // 照明用电 + Money string `json:"money"` // "40.00" + } `json:"rows"` + } + if err := json.Unmarshal(b, &res); err != nil { + return nil, err + } + if res.StatusCode != 0 { + return nil, fmt.Errorf("queryISIMSRoomBuyRecord 失败: %s", string(b)) + } + + var records []buyRecord + for _, r := range res.Rows { + var money float64 + fmt.Sscanf(r.Money, "%f", &money) + rec := buyRecord{ + Date: datePart(r.DateTime), + Money: round2(money), + Kwh: round2(money / price), + Type: r.BuyType, + Detail: r.DateTime, + } + records = append(records, rec) + } + return records, nil +} + +// getBuyRecordsCached 获取充值记录(带缓存,10 分钟有效) +func getBuyRecordsCached(shiroJID string, room *roomInfo) ([]buyRecord, error) { + if buyRecordsCache != nil && time.Now().Unix()-buyRecordsTime < 600 { + return buyRecordsCache, nil + } + recs, err := queryBuyRecords(shiroJID, room) + if err != nil { + return nil, err + } + buyRecordsCache = recs + buyRecordsTime = time.Now().Unix() + return recs, nil +} + +// rechargeKwhByDate 生成 map[date] -> 当天充值电量(度) +func rechargeKwhByDate(records []buyRecord) map[string]float64 { + m := map[string]float64{} + for _, r := range records { + m[r.Date] = round2(m[r.Date] + r.Kwh) + } + return m +} + +// unitPrice 电价(元/度),默认 0.535,可用 XF_PRICE 环境变量覆盖 +func unitPrice() float64 { + if v := envOr("XF_PRICE", ""); v != "" { + var p float64 + if _, err := fmt.Sscanf(v, "%f", &p); err == nil && p > 0 { + return p + } + } + return 0.535 +} + +// datePart 从 "2026-8-23 0:45:49" 提取 "2026-08-23" +func datePart(dt string) string { + parts := strings.SplitN(dt, " ", 2) + if len(parts) == 0 { + return dt + } + // 规范化月份/日补零 + ymd := strings.Split(parts[0], "-") + if len(ymd) != 3 { + return parts[0] + } + return fmt.Sprintf("%s-%02s-%02s", ymd[0], ymd[1], ymd[2]) +} diff --git a/go/history.go b/go/history.go index e735195..594d144 100644 --- a/go/history.go +++ b/go/history.go @@ -20,10 +20,12 @@ type record struct { } type dailyStat struct { - Date string `json:"date"` - Surplus float64 `json:"surplus"` - Amount float64 `json:"amount"` - Usage *float64 `json:"usage"` // nil 表示无前一天基线 + 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 月度汇总 @@ -34,7 +36,9 @@ type monthSummary struct { EndSurplus *float64 `json:"end_surplus"` // 月末末次电量 TotalUsage *float64 `json:"total_usage"` // 月总耗电 AvgUsage *float64 `json:"avg_usage"` // 日均耗电 - RechargeDays int `json:"recharge_days"` // 疑似充值天数(电量回升) + RechargeDays int `json:"recharge_days"` // 充值天数 + RechargeKwh *float64 `json:"recharge_kwh"` // 月充值电量(度) + RechargeMoney *float64 `json:"recharge_money"` // 月充值金额(元) MaxUsage *float64 `json:"max_usage"` // 单日最大耗电 } @@ -113,9 +117,10 @@ func getRecords() []record { return records } -// buildDailyStats 按日期升序生成每日统计(含日耗电 = 前一天剩余 - 当天剩余) +// buildDailyStats 按日期升序生成每日统计(含日耗电 = 前一天剩余 - 当天剩余 + 当天充值电量) // 注意: 耗电计算跨月时使用上月末记录作为基线(若存在) -func buildDailyStats(records []record, month string) []dailyStat { +// rechargeMap: date -> 当天充值电量(度);精确修正充值当天的耗电 +func buildDailyStats(records []record, month string, rechargeMap map[string]float64) []dailyStat { all := loadHistory() // 若指定月份,额外包含上一月最后一条记录作为基线(不计入本月输出) var prevMonthLast *record @@ -151,13 +156,23 @@ func buildDailyStats(records []record, month string) []dailyStat { for _, r := range target { var usage *float64 if prevSurplus != nil { - u := round2(*prevSurplus - r.Surplus) + // 精确耗电 = 剩余变化 + 当天充值电量 + u := round2(*prevSurplus - r.Surplus + rechargeMap[r.Date]) if u < 0 { - u = 0 // 充值/异常归零 + u = 0 // 极端异常(数据缺口)归零 } usage = &u } - daily = append(daily, dailyStat{Date: r.Date, Surplus: r.Surplus, Amount: r.Amount, Usage: usage}) + 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 } @@ -203,6 +218,8 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary { var total float64 var max float64 + var totalRechargeKwh float64 + var totalRechargeMoney float64 rechargeDays := 0 countWithUsage := 0 for _, d := range daily { @@ -213,14 +230,16 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary { } countWithUsage++ } - // 疑似充值:当天剩余比前一天高 - } - // 充值与最大耗电基于原始升序记录重新精确计算 - if len(daily) >= 2 { - rechargeDays = countRecharges(daily) + if d.Recharge != nil { + totalRechargeKwh += *d.Recharge + rechargeDays++ + } + if d.Money != nil { + totalRechargeMoney += *d.Money + } } - var totalP, avgP, maxP, startP, endP *float64 + var totalP, avgP, maxP, startP, endP, rkwhP, rmoneyP *float64 startP, endP = &start, &end totalP = &total if countWithUsage > 0 { @@ -228,26 +247,25 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary { 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, - MaxUsage: maxP, + Month: m, + RecordCount: len(daily), + StartSurplus: startP, + EndSurplus: endP, + TotalUsage: totalP, + AvgUsage: avgP, + RechargeDays: rechargeDays, + RechargeKwh: rkwhP, + RechargeMoney: rmoneyP, + 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 -} + diff --git a/go/main.go b/go/main.go index 3304249..bac4643 100644 --- a/go/main.go +++ b/go/main.go @@ -100,21 +100,35 @@ func apiHistory(w http.ResponseWriter, r *http.Request) { month := q.Get("month") // YYYY-MM,空=全部 records := getRecords() - allDaily := buildDailyStats(records, "") // 全量(用于可用月份列表) - monthDaily := buildDailyStats(records, month) + + // 获取充值记录(用于精确耗电计算) + rechargeMap := map[string]float64{} + var buyRecords []buyRecord + if c, err := loadCache(); err == nil { + if recs, err := getBuyRecordsCached(c.AppShiro, &c.Room); err == nil { + rechargeMap = rechargeKwhByDate(recs) + buyRecords = recs + } else { + fmt.Println("[充值] 获取充值记录失败:", err) + } + } + + allDaily := buildDailyStats(records, "", rechargeMap) // 全量(用于可用月份列表) + monthDaily := buildDailyStats(records, month, rechargeMap) // 可用月份列表(降序) months := availableMonths(records) summary := buildMonthSummary(monthDaily, month) writeJSON(w, map[string]any{ - "ok": true, - "month": month, - "months": months, - "records": monthDaily, // 升序每日统计(含 usage) - "daily": monthDaily, - "summary": summary, - "all_count": len(allDaily), + "ok": true, + "month": month, + "months": months, + "records": monthDaily, // 升序每日统计(含 usage) + "daily": monthDaily, + "summary": summary, + "recharges": buyRecords, // 充值记录明细 + "all_count": len(allDaily), }) }