feat: 精确耗电计算(接入充值记录接口)
- 新增 queryISIMSRoomBuyRecord 充值记录获取 + 10分钟缓存 - 日耗电 = 剩余变化 + 当天充值电量(电价0.535元/度) - 月度汇总增加充值次数/电量/金额 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
118
go/buy_records.go
Normal file
118
go/buy_records.go
Normal file
@@ -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])
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ type dailyStat struct {
|
|||||||
Surplus float64 `json:"surplus"`
|
Surplus float64 `json:"surplus"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
Usage *float64 `json:"usage"` // nil 表示无前一天基线
|
Usage *float64 `json:"usage"` // nil 表示无前一天基线
|
||||||
|
Recharge *float64 `json:"recharge"` // 当天充值电量(度), nil=无充值
|
||||||
|
Money *float64 `json:"money"` // 当天充值金额(元), nil=无充值
|
||||||
}
|
}
|
||||||
|
|
||||||
// monthSummary 月度汇总
|
// monthSummary 月度汇总
|
||||||
@@ -34,7 +36,9 @@ type monthSummary struct {
|
|||||||
EndSurplus *float64 `json:"end_surplus"` // 月末末次电量
|
EndSurplus *float64 `json:"end_surplus"` // 月末末次电量
|
||||||
TotalUsage *float64 `json:"total_usage"` // 月总耗电
|
TotalUsage *float64 `json:"total_usage"` // 月总耗电
|
||||||
AvgUsage *float64 `json:"avg_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"` // 单日最大耗电
|
MaxUsage *float64 `json:"max_usage"` // 单日最大耗电
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +117,10 @@ func getRecords() []record {
|
|||||||
return records
|
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()
|
all := loadHistory()
|
||||||
// 若指定月份,额外包含上一月最后一条记录作为基线(不计入本月输出)
|
// 若指定月份,额外包含上一月最后一条记录作为基线(不计入本月输出)
|
||||||
var prevMonthLast *record
|
var prevMonthLast *record
|
||||||
@@ -151,13 +156,23 @@ func buildDailyStats(records []record, month string) []dailyStat {
|
|||||||
for _, r := range target {
|
for _, r := range target {
|
||||||
var usage *float64
|
var usage *float64
|
||||||
if prevSurplus != nil {
|
if prevSurplus != nil {
|
||||||
u := round2(*prevSurplus - r.Surplus)
|
// 精确耗电 = 剩余变化 + 当天充值电量
|
||||||
|
u := round2(*prevSurplus - r.Surplus + rechargeMap[r.Date])
|
||||||
if u < 0 {
|
if u < 0 {
|
||||||
u = 0 // 充值/异常归零
|
u = 0 // 极端异常(数据缺口)归零
|
||||||
}
|
}
|
||||||
usage = &u
|
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
|
s := r.Surplus
|
||||||
prevSurplus = &s
|
prevSurplus = &s
|
||||||
}
|
}
|
||||||
@@ -203,6 +218,8 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
|
|||||||
|
|
||||||
var total float64
|
var total float64
|
||||||
var max float64
|
var max float64
|
||||||
|
var totalRechargeKwh float64
|
||||||
|
var totalRechargeMoney float64
|
||||||
rechargeDays := 0
|
rechargeDays := 0
|
||||||
countWithUsage := 0
|
countWithUsage := 0
|
||||||
for _, d := range daily {
|
for _, d := range daily {
|
||||||
@@ -213,14 +230,16 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
|
|||||||
}
|
}
|
||||||
countWithUsage++
|
countWithUsage++
|
||||||
}
|
}
|
||||||
// 疑似充值:当天剩余比前一天高
|
if d.Recharge != nil {
|
||||||
|
totalRechargeKwh += *d.Recharge
|
||||||
|
rechargeDays++
|
||||||
|
}
|
||||||
|
if d.Money != nil {
|
||||||
|
totalRechargeMoney += *d.Money
|
||||||
}
|
}
|
||||||
// 充值与最大耗电基于原始升序记录重新精确计算
|
|
||||||
if len(daily) >= 2 {
|
|
||||||
rechargeDays = countRecharges(daily)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalP, avgP, maxP, startP, endP *float64
|
var totalP, avgP, maxP, startP, endP, rkwhP, rmoneyP *float64
|
||||||
startP, endP = &start, &end
|
startP, endP = &start, &end
|
||||||
totalP = &total
|
totalP = &total
|
||||||
if countWithUsage > 0 {
|
if countWithUsage > 0 {
|
||||||
@@ -228,6 +247,12 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
|
|||||||
avgP = &avg
|
avgP = &avg
|
||||||
}
|
}
|
||||||
maxP = &max
|
maxP = &max
|
||||||
|
if totalRechargeKwh > 0 {
|
||||||
|
rkwhP = &totalRechargeKwh
|
||||||
|
}
|
||||||
|
if totalRechargeMoney > 0 {
|
||||||
|
rmoneyP = &totalRechargeMoney
|
||||||
|
}
|
||||||
|
|
||||||
return &monthSummary{
|
return &monthSummary{
|
||||||
Month: m,
|
Month: m,
|
||||||
@@ -237,17 +262,10 @@ func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
|
|||||||
TotalUsage: totalP,
|
TotalUsage: totalP,
|
||||||
AvgUsage: avgP,
|
AvgUsage: avgP,
|
||||||
RechargeDays: rechargeDays,
|
RechargeDays: rechargeDays,
|
||||||
|
RechargeKwh: rkwhP,
|
||||||
|
RechargeMoney: rmoneyP,
|
||||||
MaxUsage: maxP,
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
18
go/main.go
18
go/main.go
@@ -100,8 +100,21 @@ func apiHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
month := q.Get("month") // YYYY-MM,空=全部
|
month := q.Get("month") // YYYY-MM,空=全部
|
||||||
|
|
||||||
records := getRecords()
|
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)
|
months := availableMonths(records)
|
||||||
@@ -114,6 +127,7 @@ func apiHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
"records": monthDaily, // 升序每日统计(含 usage)
|
"records": monthDaily, // 升序每日统计(含 usage)
|
||||||
"daily": monthDaily,
|
"daily": monthDaily,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
"recharges": buyRecords, // 充值记录明细
|
||||||
"all_count": len(allDaily),
|
"all_count": len(allDaily),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user