From 7ed00b78e3c8a3579b364817a4c4efcd66bd2c61 Mon Sep 17 00:00:00 2001 From: BaoXuebin Date: Tue, 1 Oct 2024 14:43:07 +0800 Subject: [PATCH] add sankey --- public/logo192.png | Bin 9721 -> 5598 bytes public/logo512.png | Bin 17361 -> 0 bytes script/config.go | 5 ++ script/utils.go | 27 ------- server.go | 1 + service/stats.go | 174 ++++++++++++++++++++++++++++++++++++++++ service/transactions.go | 15 ++++ 7 files changed, 195 insertions(+), 27 deletions(-) delete mode 100644 public/logo512.png diff --git a/public/logo192.png b/public/logo192.png index 1918ff2edeedb1a1cfb429c71deb46b3cb71427e..8aa41eb5a7906f415867d39e828202b2c65b5b60 100644 GIT binary patch literal 5598 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 I6ApM`ywvlPKu<&AtS@JQAdsA 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 TglphG9_i|9n`&TI@kIX#FLjTn diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index 7e7dc74f2a9aa690a5420ac9d4bcf4fbad6c67b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17361 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 E3vX#zv;Y7A diff --git a/script/config.go b/script/config.go index 8d70c92..9d62219 100644 --- a/script/config.go +++ b/script/config.go @@ -579,6 +579,11 @@ func GetAccountPrefix(account string) string { return nodes[0] } +func GetAccountName(account string) string { + nodes := strings.Split(account, ":") + return nodes[len(nodes)-1] +} + func GetAccountIconName(account string) string { nodes := strings.Split(account, ":") return strings.Join(nodes, "_") diff --git a/script/utils.go b/script/utils.go index ff80f54..c1ed18c 100644 --- a/script/utils.go +++ b/script/utils.go @@ -84,30 +84,3 @@ func getMaxDate(str_date1 string, str_date2 string) string { } return max_date } - -// 获取1-2个日期字符串中最小的日期值 -// 如果双参数均为空,则返回账簿开始记账日期 -//func getMinDate(str_date1 string, str_date2 string) string { -// //time_layout := "2006-01-02 15:04:05" -// var min_date string -// if str_date1 != "" && str_date2 == "" { -// // 只定义了第一个账户,取第一个账户的日期为准 -// min_date = str_date1 -// } else if str_date1 == "" && str_date2 != "" { -// // 只定义了第二个账户,取第二个账户的日期为准 -// min_date = str_date2 -// } else if str_date1 != "" && str_date2 != "" { -// // 重复定义的账户,取最早的时间 -// t1 := getTimeStamp(str_date1) -// t2 := getTimeStamp(str_date2) -// if t1 < t2 { -// min_date = str_date1 -// } else { -// min_date = str_date2 -// } -// } else if str_date1 == "" && str_date2 == "" { -// // 没有定义账户,取固定日期"1970-01-01" -// min_date = "1970-01-01" -// } -// return min_date -//} diff --git a/server.go b/server.go index 3a188f0..f68bcc8 100644 --- a/server.go +++ b/server.go @@ -75,6 +75,7 @@ func RegisterRouter(router *gin.Engine) { authorized.GET("/stats/account/percent", service.StatsAccountPercent) authorized.GET("/stats/account/trend", service.StatsAccountTrend) authorized.GET("/stats/account/balance", service.StatsAccountBalance) + authorized.GET("/stats/account/flow", service.StatsAccountSankey) authorized.GET("/stats/month/total", service.StatsMonthTotal) authorized.GET("/stats/month/calendar", service.StatsMonthCalendar) authorized.GET("/stats/commodity/price", service.StatsCommodityPrice) diff --git a/service/stats.go b/service/stats.go index 399ec57..b2e798e 100644 --- a/service/stats.go +++ b/service/stats.go @@ -3,7 +3,9 @@ package service import ( "encoding/json" "fmt" + "math" "sort" + "strconv" "strings" "time" @@ -241,6 +243,178 @@ func StatsAccountBalance(c *gin.Context) { OK(c, resultList) } +type AccountSankeyResult struct { + Nodes []AccountSankeyNode `json:"nodes"` + Links []AccountSankeyLink `json:"links"` +} + +type AccountSankeyNode struct { + Name string `json:"name"` +} +type AccountSankeyLink struct { + Source int `json:"source"` + Target int `json:"target"` + Value string `json:"value"` +} + +func NewAccountSankeyLink() *AccountSankeyLink { + return &AccountSankeyLink{ + Source: -1, + Target: -1, + Value: "", + } +} + +func StatsAccountSankey(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + queryParams := script.GetQueryParams(c) + // 倒序查询 + queryParams.OrderBy = "date desc" + transactions := make([]Transaction, 0) + err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) + if err != nil { + InternalError(c, err.Error()) + return + } + + accountSankeyResult := AccountSankeyResult{} + // 构建 nodes 和 links + var nodes []AccountSankeyNode + + // 遍历 transactions 中按id进行分组 + if len(transactions) > 0 { + for _, transaction := range transactions { + // 如果nodes中不存在该节点,则添加 + accountName := script.GetAccountName(transaction.Account) + if !contains(nodes, accountName) { + nodes = append(nodes, AccountSankeyNode{Name: accountName}) + } + } + accountSankeyResult.Nodes = nodes + + transactionsMap := groupTransactionsByID(transactions) + // 声明 links + links := make([]AccountSankeyLink, 0) + // 遍历 transactionsMap + for _, transactions := range transactionsMap { + // 拼接成 links + sourceTransaction := Transaction{} + targetTransaction := Transaction{} + currentLinkNode := NewAccountSankeyLink() + // transactions 的最大长度 + maxCycle := len(transactions) * 2 + + for { + if len(transactions) == 0 || maxCycle == 0 { + break + } + transaction := transactions[0] + transactions = transactions[1:] + + accountName := script.GetAccountName(transaction.Account) + num, err := strconv.ParseFloat(transaction.Number, 64) + if err != nil { + continue + } + if currentLinkNode.Source == -1 && num < 0 { + if sourceTransaction.Account == "" { + sourceTransaction = transaction + } + currentLinkNode.Source = indexOf(nodes, accountName) + if currentLinkNode.Target == -1 { + currentLinkNode.Value = strconv.FormatFloat(num, 'f', 2, 64) + } else { + // 比较 link node value 和 num 大小 + value, _ := strconv.ParseFloat(currentLinkNode.Value, 64) + delta := value + num + if delta == 0 { + currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) + } else if delta < 0 { // source > target + targetNumber, _ := strconv.ParseFloat(targetTransaction.Number, 64) + currentLinkNode.Value = strconv.FormatFloat(math.Abs(targetNumber), 'f', 2, 64) + sourceTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) + transactions = append(transactions, sourceTransaction) + } else { // source < target + targetTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) + transactions = append(transactions, targetTransaction) + } + // 完成一个 linkNode 的构建,重置判定条件 + sourceTransaction.Account = "" + targetTransaction.Account = "" + links = append(links, *currentLinkNode) + currentLinkNode = NewAccountSankeyLink() + } + } else if currentLinkNode.Target == -1 && num > 0 { + if targetTransaction.Account == "" { + targetTransaction = transaction + } + currentLinkNode.Target = indexOf(nodes, accountName) + if currentLinkNode.Source == -1 { + currentLinkNode.Value = strconv.FormatFloat(num, 'f', 2, 64) + } else { + value, _ := strconv.ParseFloat(currentLinkNode.Value, 64) + delta := value + num + if delta == 0 { + currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) + } else if delta < 0 { // source > target + currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) + sourceTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) + transactions = append(transactions, sourceTransaction) + } else { // source < target + sourceNumber, _ := strconv.ParseFloat(sourceTransaction.Number, 64) + currentLinkNode.Value = strconv.FormatFloat(math.Abs(sourceNumber), 'f', 2, 64) + targetTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) + transactions = append(transactions, targetTransaction) + } + // 完成一个 linkNode 的构建,重置判定条件 + sourceTransaction.Account = "" + targetTransaction.Account = "" + links = append(links, *currentLinkNode) + currentLinkNode = NewAccountSankeyLink() + } + } else { + // 将当前的 transaction 加入到队列末尾 + transactions = append(transactions, transaction) + } + maxCycle -= 1 + } + } + accountSankeyResult.Links = links + } + + OK(c, accountSankeyResult) +} + +func contains(nodes []AccountSankeyNode, str string) bool { + for _, s := range nodes { + if s.Name == str { + return true + } + } + return false +} + +func indexOf(nodes []AccountSankeyNode, str string) int { + idx := 0 + for _, s := range nodes { + if s.Name == str { + return idx + } + idx += 1 + } + return -1 +} + +func groupTransactionsByID(transactions []Transaction) map[string][]Transaction { + grouped := make(map[string][]Transaction) + + for _, transaction := range transactions { + grouped[transaction.Id] = append(grouped[transaction.Id], transaction) + } + + return grouped +} + type MonthTotalBQLResult struct { Year int Month int diff --git a/service/transactions.go b/service/transactions.go index 82d35b8..90f18bd 100644 --- a/service/transactions.go +++ b/service/transactions.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "strconv" "strings" "time" @@ -34,6 +35,20 @@ type Transaction struct { IsAnotherCurrency bool `json:"isAnotherCurrency,omitempty"` } +type TransactionSort []Transaction + +func (s TransactionSort) Len() int { + return len(s) +} +func (s TransactionSort) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s TransactionSort) Less(i, j int) bool { + a, _ := strconv.Atoi(s[i].Number) + b, _ := strconv.Atoi(s[j].Number) + return a <= b +} + func QueryTransactions(c *gin.Context) { ledgerConfig := script.GetLedgerConfigFromContext(c) queryParams := script.GetQueryParams(c)