add sankey
This commit is contained in:
parent
02d644fe96
commit
7ed00b78e3
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
|
@ -579,6 +579,11 @@ func GetAccountPrefix(account string) string {
|
||||||
return nodes[0]
|
return nodes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAccountName(account string) string {
|
||||||
|
nodes := strings.Split(account, ":")
|
||||||
|
return nodes[len(nodes)-1]
|
||||||
|
}
|
||||||
|
|
||||||
func GetAccountIconName(account string) string {
|
func GetAccountIconName(account string) string {
|
||||||
nodes := strings.Split(account, ":")
|
nodes := strings.Split(account, ":")
|
||||||
return strings.Join(nodes, "_")
|
return strings.Join(nodes, "_")
|
||||||
|
|
|
||||||
|
|
@ -84,30 +84,3 @@ func getMaxDate(str_date1 string, str_date2 string) string {
|
||||||
}
|
}
|
||||||
return max_date
|
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
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ func RegisterRouter(router *gin.Engine) {
|
||||||
authorized.GET("/stats/account/percent", service.StatsAccountPercent)
|
authorized.GET("/stats/account/percent", service.StatsAccountPercent)
|
||||||
authorized.GET("/stats/account/trend", service.StatsAccountTrend)
|
authorized.GET("/stats/account/trend", service.StatsAccountTrend)
|
||||||
authorized.GET("/stats/account/balance", service.StatsAccountBalance)
|
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/total", service.StatsMonthTotal)
|
||||||
authorized.GET("/stats/month/calendar", service.StatsMonthCalendar)
|
authorized.GET("/stats/month/calendar", service.StatsMonthCalendar)
|
||||||
authorized.GET("/stats/commodity/price", service.StatsCommodityPrice)
|
authorized.GET("/stats/commodity/price", service.StatsCommodityPrice)
|
||||||
|
|
|
||||||
174
service/stats.go
174
service/stats.go
|
|
@ -3,7 +3,9 @@ package service
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -241,6 +243,178 @@ func StatsAccountBalance(c *gin.Context) {
|
||||||
OK(c, resultList)
|
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 {
|
type MonthTotalBQLResult struct {
|
||||||
Year int
|
Year int
|
||||||
Month int
|
Month int
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -34,6 +35,20 @@ type Transaction struct {
|
||||||
IsAnotherCurrency bool `json:"isAnotherCurrency,omitempty"`
|
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) {
|
func QueryTransactions(c *gin.Context) {
|
||||||
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
queryParams := script.GetQueryParams(c)
|
queryParams := script.GetQueryParams(c)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue