add account flow sankey api

This commit is contained in:
BaoXuebin 2024-10-03 15:50:33 +08:00
parent 7ed00b78e3
commit c7f238982f
2 changed files with 141 additions and 45 deletions

View File

@ -17,6 +17,8 @@ type QueryParams struct {
FromYear int `bql:"year ="` FromYear int `bql:"year ="`
FromMonth int `bql:"month ="` FromMonth int `bql:"month ="`
Where bool `bql:"where"` Where bool `bql:"where"`
ID string `bql:"id ="`
IDList string `bql:"id in"`
Currency string `bql:"currency ="` Currency string `bql:"currency ="`
Year int `bql:"year ="` Year int `bql:"year ="`
Month int `bql:"month ="` Month int `bql:"month ="`

View File

@ -3,9 +3,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -65,7 +63,7 @@ func StatsTotal(c *gin.Context) {
} }
type StatsQuery struct { type StatsQuery struct {
Prefix string `form:"prefix" binding:"required"` Prefix string `form:"prefix"`
Year int `form:"year"` Year int `form:"year"`
Month int `form:"month"` Month int `form:"month"`
Level int `form:"level"` Level int `form:"level"`
@ -254,30 +252,102 @@ type AccountSankeyNode struct {
type AccountSankeyLink struct { type AccountSankeyLink struct {
Source int `json:"source"` Source int `json:"source"`
Target int `json:"target"` Target int `json:"target"`
Value string `json:"value"` Value decimal.Decimal `json:"value"`
} }
func NewAccountSankeyLink() *AccountSankeyLink { func NewAccountSankeyLink() *AccountSankeyLink {
return &AccountSankeyLink{ return &AccountSankeyLink{
Source: -1, Source: -1,
Target: -1, Target: -1,
Value: "",
} }
} }
type TransactionAccountPositionBQLResult struct {
Id string
Account string
Position string
}
type TransactionAccountPosition struct {
Id string
Account string
AccountName string
Value decimal.Decimal
OperatingCurrency string
}
// StatsAccountSankey 统计账户流向
func StatsAccountSankey(c *gin.Context) { func StatsAccountSankey(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c) ledgerConfig := script.GetLedgerConfigFromContext(c)
queryParams := script.GetQueryParams(c) var statsQuery StatsQuery
// 倒序查询 if err := c.ShouldBindQuery(&statsQuery); err != nil {
queryParams.OrderBy = "date desc" BadRequest(c, err.Error())
transactions := make([]Transaction, 0) return
err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) }
queryParams := script.QueryParams{
AccountLike: statsQuery.Prefix,
Year: statsQuery.Year,
Month: statsQuery.Month,
Where: true,
}
statsQueryResultList := make([]TransactionAccountPositionBQLResult, 0)
var bql string
// 账户不为空,则查询时间范围内所有涉及该账户的交易记录
if statsQuery.Prefix != "" {
// 清空 account 查询条件,改为使用 ID 查询包含该账户所有交易记录
queryParams.AccountLike = ""
bql = "SELECT '\\', id, '\\'"
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
if err != nil {
InternalError(c, err.Error())
return
}
if len(statsQueryResultList) != 0 {
idSet := make(map[string]bool)
for _, bqlResult := range statsQueryResultList {
idSet[bqlResult.Id] = true
}
idList := make([]string, 0, len(idSet))
for id := range idSet {
idList = append(idList, id)
}
queryParams.IDList = strings.Join(idList, "|")
}
}
if statsQuery.Level != 0 {
prefixNodeLen := len(strings.Split(strings.Trim(statsQuery.Prefix, ":"), ":"))
bql = fmt.Sprintf("SELECT '\\', id, '\\', root(account, %d) as subAccount, '\\', sum(convert(value(position), '%s')), '\\'", statsQuery.Level+prefixNodeLen, ledgerConfig.OperatingCurrency)
} else {
bql = fmt.Sprintf("SELECT '\\', id, '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
}
statsQueryResultList = make([]TransactionAccountPositionBQLResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
if err != nil { if err != nil {
InternalError(c, err.Error()) InternalError(c, err.Error())
return return
} }
result := make([]Transaction, 0)
for _, queryRes := range statsQueryResultList {
if queryRes.Position != "" {
fields := strings.Fields(queryRes.Position)
result = append(result, Transaction{
Id: queryRes.Id,
Account: queryRes.Account,
Number: fields[0],
Currency: fields[1],
})
}
}
OK(c, buildSankeyResult(result))
}
func buildSankeyResult(transactions []Transaction) AccountSankeyResult {
accountSankeyResult := AccountSankeyResult{} accountSankeyResult := AccountSankeyResult{}
accountSankeyResult.Nodes = make([]AccountSankeyNode, 0)
accountSankeyResult.Links = make([]AccountSankeyLink, 0)
// 构建 nodes 和 links // 构建 nodes 和 links
var nodes []AccountSankeyNode var nodes []AccountSankeyNode
@ -285,9 +355,9 @@ func StatsAccountSankey(c *gin.Context) {
if len(transactions) > 0 { if len(transactions) > 0 {
for _, transaction := range transactions { for _, transaction := range transactions {
// 如果nodes中不存在该节点则添加 // 如果nodes中不存在该节点则添加
accountName := script.GetAccountName(transaction.Account) account := transaction.Account
if !contains(nodes, accountName) { if !contains(nodes, account) {
nodes = append(nodes, AccountSankeyNode{Name: accountName}) nodes = append(nodes, AccountSankeyNode{Name: account})
} }
} }
accountSankeyResult.Nodes = nodes accountSankeyResult.Nodes = nodes
@ -311,31 +381,30 @@ func StatsAccountSankey(c *gin.Context) {
transaction := transactions[0] transaction := transactions[0]
transactions = transactions[1:] transactions = transactions[1:]
accountName := script.GetAccountName(transaction.Account) account := transaction.Account
num, err := strconv.ParseFloat(transaction.Number, 64) num, err := decimal.NewFromString(transaction.Number)
if err != nil { if err != nil {
continue continue
} }
if currentLinkNode.Source == -1 && num < 0 { if currentLinkNode.Source == -1 && num.IsNegative() {
if sourceTransaction.Account == "" { if sourceTransaction.Account == "" {
sourceTransaction = transaction sourceTransaction = transaction
} }
currentLinkNode.Source = indexOf(nodes, accountName) currentLinkNode.Source = indexOf(nodes, account)
if currentLinkNode.Target == -1 { if currentLinkNode.Target == -1 {
currentLinkNode.Value = strconv.FormatFloat(num, 'f', 2, 64) currentLinkNode.Value = num
} else { } else {
// 比较 link node value 和 num 大小 // 比较 link node value 和 num 大小
value, _ := strconv.ParseFloat(currentLinkNode.Value, 64) delta := currentLinkNode.Value.Add(num)
delta := value + num if delta.IsZero() {
if delta == 0 { currentLinkNode.Value = num.Abs()
currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) } else if delta.IsNegative() { // source > target
} else if delta < 0 { // source > target targetNumber, _ := decimal.NewFromString(targetTransaction.Number)
targetNumber, _ := strconv.ParseFloat(targetTransaction.Number, 64) currentLinkNode.Value = targetNumber.Abs()
currentLinkNode.Value = strconv.FormatFloat(math.Abs(targetNumber), 'f', 2, 64) sourceTransaction.Number = delta.String()
sourceTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64)
transactions = append(transactions, sourceTransaction) transactions = append(transactions, sourceTransaction)
} else { // source < target } else { // source < target
targetTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) targetTransaction.Number = delta.String()
transactions = append(transactions, targetTransaction) transactions = append(transactions, targetTransaction)
} }
// 完成一个 linkNode 的构建,重置判定条件 // 完成一个 linkNode 的构建,重置判定条件
@ -344,26 +413,25 @@ func StatsAccountSankey(c *gin.Context) {
links = append(links, *currentLinkNode) links = append(links, *currentLinkNode)
currentLinkNode = NewAccountSankeyLink() currentLinkNode = NewAccountSankeyLink()
} }
} else if currentLinkNode.Target == -1 && num > 0 { } else if currentLinkNode.Target == -1 && num.IsPositive() {
if targetTransaction.Account == "" { if targetTransaction.Account == "" {
targetTransaction = transaction targetTransaction = transaction
} }
currentLinkNode.Target = indexOf(nodes, accountName) currentLinkNode.Target = indexOf(nodes, account)
if currentLinkNode.Source == -1 { if currentLinkNode.Source == -1 {
currentLinkNode.Value = strconv.FormatFloat(num, 'f', 2, 64) currentLinkNode.Value = num
} else { } else {
value, _ := strconv.ParseFloat(currentLinkNode.Value, 64) delta := currentLinkNode.Value.Add(num)
delta := value + num if delta.IsZero() {
if delta == 0 { currentLinkNode.Value = num.Abs()
currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) } else if delta.IsNegative() { // source > target
} else if delta < 0 { // source > target currentLinkNode.Value = num.Abs()
currentLinkNode.Value = strconv.FormatFloat(math.Abs(num), 'f', 2, 64) sourceTransaction.Number = delta.String()
sourceTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64)
transactions = append(transactions, sourceTransaction) transactions = append(transactions, sourceTransaction)
} else { // source < target } else { // source < target
sourceNumber, _ := strconv.ParseFloat(sourceTransaction.Number, 64) sourceNumber, _ := decimal.NewFromString(sourceTransaction.Number)
currentLinkNode.Value = strconv.FormatFloat(math.Abs(sourceNumber), 'f', 2, 64) currentLinkNode.Value = sourceNumber.Abs()
targetTransaction.Number = strconv.FormatFloat(delta, 'f', 2, 64) targetTransaction.Number = delta.String()
transactions = append(transactions, targetTransaction) transactions = append(transactions, targetTransaction)
} }
// 完成一个 linkNode 的构建,重置判定条件 // 完成一个 linkNode 的构建,重置判定条件
@ -379,10 +447,9 @@ func StatsAccountSankey(c *gin.Context) {
maxCycle -= 1 maxCycle -= 1
} }
} }
accountSankeyResult.Links = links accountSankeyResult.Links = aggregateLinkNodes(links)
} }
return accountSankeyResult
OK(c, accountSankeyResult)
} }
func contains(nodes []AccountSankeyNode, str string) bool { func contains(nodes []AccountSankeyNode, str string) bool {
@ -415,6 +482,33 @@ func groupTransactionsByID(transactions []Transaction) map[string][]Transaction
return grouped return grouped
} }
// 聚合函数,聚合相同 source 和 target 的值
func aggregateLinkNodes(nodes []AccountSankeyLink) []AccountSankeyLink {
// 创建一个映射 key 为 "source-target"value 为 LinkNode
nodeMap := make(map[string]AccountSankeyLink)
for _, node := range nodes {
key := fmt.Sprintf("%d-%d", node.Source, node.Target)
if existingNode, found := nodeMap[key]; found {
// 如果已经存在相同的 source 和 target累加 value
existingNode.Value = existingNode.Value.Add(node.Value)
nodeMap[key] = existingNode
} else {
// 否则直接插入新的 LinkNode
nodeMap[key] = node
}
}
// 将 map 转换为 slice
result := make([]AccountSankeyLink, 0, len(nodeMap))
for _, aggregatedNode := range nodeMap {
result = append(result, aggregatedNode)
}
return result
}
type MonthTotalBQLResult struct { type MonthTotalBQLResult struct {
Year int Year int
Month int Month int