add account flow sankey api
This commit is contained in:
parent
7ed00b78e3
commit
c7f238982f
|
|
@ -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 ="`
|
||||||
|
|
|
||||||
180
service/stats.go
180
service/stats.go
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue