2021-11-19 09:54:02 +00:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2021-11-26 09:12:07 +00:00
|
|
|
|
"encoding/json"
|
2021-11-22 14:50:10 +00:00
|
|
|
|
"fmt"
|
2021-12-22 15:13:37 +00:00
|
|
|
|
"sort"
|
2024-10-04 09:16:53 +00:00
|
|
|
|
"strconv"
|
2021-12-22 15:13:37 +00:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2021-11-19 09:54:02 +00:00
|
|
|
|
"github.com/beancount-gs/script"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2021-11-26 09:12:07 +00:00
|
|
|
|
"github.com/shopspring/decimal"
|
2021-11-19 09:54:02 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2021-11-22 08:47:49 +00:00
|
|
|
|
type YearMonth struct {
|
|
|
|
|
|
Year string `bql:"distinct year(date)" json:"year"`
|
|
|
|
|
|
Month string `bql:"month(date)" json:"month"`
|
|
|
|
|
|
}
|
2021-11-19 09:54:02 +00:00
|
|
|
|
|
2021-11-22 08:47:49 +00:00
|
|
|
|
func MonthsList(c *gin.Context) {
|
2021-11-21 14:37:13 +00:00
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
2022-09-03 15:21:25 +00:00
|
|
|
|
// 添加排序
|
|
|
|
|
|
queryParams := script.GetQueryParams(c)
|
|
|
|
|
|
queryParams.OrderBy = "year, month desc"
|
2021-11-22 08:47:49 +00:00
|
|
|
|
yearMonthList := make([]YearMonth, 0)
|
2022-09-03 15:21:25 +00:00
|
|
|
|
err := script.BQLQueryList(ledgerConfig, &queryParams, &yearMonthList)
|
2021-11-19 09:54:02 +00:00
|
|
|
|
if err != nil {
|
2021-11-22 08:47:49 +00:00
|
|
|
|
InternalError(c, err.Error())
|
2021-11-19 09:54:02 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2021-11-22 08:47:49 +00:00
|
|
|
|
months := make([]string, 0)
|
|
|
|
|
|
for _, yearMonth := range yearMonthList {
|
|
|
|
|
|
months = append(months, yearMonth.Year+"-"+yearMonth.Month)
|
2021-11-21 14:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
OK(c, months)
|
2021-11-19 09:54:02 +00:00
|
|
|
|
}
|
2021-11-22 14:50:10 +00:00
|
|
|
|
|
2021-11-24 15:45:45 +00:00
|
|
|
|
type StatsResult struct {
|
|
|
|
|
|
Key string
|
|
|
|
|
|
Value string
|
2021-11-22 14:50:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func StatsTotal(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
queryParams := script.GetQueryParams(c)
|
|
|
|
|
|
selectBql := fmt.Sprintf("SELECT '\\', root(account, 1), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2021-11-24 15:45:45 +00:00
|
|
|
|
accountTypeTotalList := make([]StatsResult, 0)
|
2021-11-22 14:50:10 +00:00
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, selectBql, &queryParams, &accountTypeTotalList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-07-03 15:26:48 +00:00
|
|
|
|
result := make(map[string]string)
|
2021-11-22 14:50:10 +00:00
|
|
|
|
for _, total := range accountTypeTotalList {
|
2021-11-24 15:45:45 +00:00
|
|
|
|
fields := strings.Fields(total.Value)
|
2021-11-22 14:50:10 +00:00
|
|
|
|
if len(fields) > 1 {
|
2021-11-24 15:45:45 +00:00
|
|
|
|
result[total.Key] = fields[0]
|
2021-11-22 14:50:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
OK(c, result)
|
|
|
|
|
|
}
|
2021-11-24 15:45:45 +00:00
|
|
|
|
|
|
|
|
|
|
type StatsQuery struct {
|
2024-10-03 07:50:33 +00:00
|
|
|
|
Prefix string `form:"prefix"`
|
2021-11-24 15:45:45 +00:00
|
|
|
|
Year int `form:"year"`
|
|
|
|
|
|
Month int `form:"month"`
|
|
|
|
|
|
Level int `form:"level"`
|
|
|
|
|
|
Type string `form:"type"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-26 09:12:07 +00:00
|
|
|
|
type AccountPercentQueryResult struct {
|
|
|
|
|
|
Account string
|
|
|
|
|
|
Position string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-24 15:45:45 +00:00
|
|
|
|
type AccountPercentResult struct {
|
2024-10-04 10:05:15 +00:00
|
|
|
|
Account string `json:"account"`
|
|
|
|
|
|
Amount decimal.Decimal `json:"amount"`
|
|
|
|
|
|
OperatingCurrency string `json:"operatingCurrency"`
|
2021-11-24 15:45:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func StatsAccountPercent(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
var statsQuery StatsQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: statsQuery.Prefix,
|
|
|
|
|
|
Year: statsQuery.Year,
|
|
|
|
|
|
Month: statsQuery.Month,
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
}
|
2024-10-04 10:05:15 +00:00
|
|
|
|
|
|
|
|
|
|
bql := fmt.Sprintf("SELECT '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2021-11-24 15:45:45 +00:00
|
|
|
|
|
2021-11-26 09:12:07 +00:00
|
|
|
|
statsQueryResultList := make([]AccountPercentQueryResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
|
2021-11-24 15:45:45 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-26 09:12:07 +00:00
|
|
|
|
result := make([]AccountPercentResult, 0)
|
|
|
|
|
|
for _, queryRes := range statsQueryResultList {
|
|
|
|
|
|
if queryRes.Position != "" {
|
|
|
|
|
|
fields := strings.Fields(queryRes.Position)
|
2024-10-04 10:05:15 +00:00
|
|
|
|
account := queryRes.Account
|
|
|
|
|
|
if statsQuery.Level == 1 {
|
|
|
|
|
|
accountType := script.GetAccountType(ledgerConfig.Id, queryRes.Account)
|
|
|
|
|
|
account = accountType.Key + ":" + accountType.Name
|
|
|
|
|
|
}
|
|
|
|
|
|
amount, err := decimal.NewFromString(fields[0])
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
result = append(result, AccountPercentResult{Account: account, Amount: amount, OperatingCurrency: fields[1]})
|
|
|
|
|
|
}
|
2021-11-26 09:12:07 +00:00
|
|
|
|
}
|
2021-11-24 15:45:45 +00:00
|
|
|
|
}
|
2024-10-04 10:05:15 +00:00
|
|
|
|
|
|
|
|
|
|
OK(c, aggregateAccountPercentList(result))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func aggregateAccountPercentList(result []AccountPercentResult) []AccountPercentResult {
|
|
|
|
|
|
// 创建一个映射来存储连接
|
|
|
|
|
|
nodeMap := make(map[string]AccountPercentResult)
|
|
|
|
|
|
for _, account := range result {
|
|
|
|
|
|
acc := account.Account
|
|
|
|
|
|
if exist, found := nodeMap[acc]; found {
|
|
|
|
|
|
exist.Amount = exist.Amount.Add(account.Amount)
|
|
|
|
|
|
nodeMap[acc] = exist
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nodeMap[acc] = account
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
aggregateResult := make([]AccountPercentResult, 0)
|
|
|
|
|
|
for _, value := range nodeMap {
|
|
|
|
|
|
aggregateResult = append(aggregateResult, value)
|
|
|
|
|
|
}
|
|
|
|
|
|
return aggregateResult
|
2021-11-24 15:45:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AccountTrendResult struct {
|
2021-11-26 09:12:07 +00:00
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Amount json.Number `json:"amount"`
|
|
|
|
|
|
OperatingCurrency string `json:"operatingCurrency"`
|
2021-11-24 15:45:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func StatsAccountTrend(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
var statsQuery StatsQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: statsQuery.Prefix,
|
|
|
|
|
|
Year: statsQuery.Year,
|
|
|
|
|
|
Month: statsQuery.Month,
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
var bql string
|
2022-07-03 15:26:48 +00:00
|
|
|
|
switch {
|
|
|
|
|
|
case statsQuery.Type == "day":
|
2021-11-24 15:45:45 +00:00
|
|
|
|
bql = fmt.Sprintf("SELECT '\\', date, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2022-07-03 15:26:48 +00:00
|
|
|
|
case statsQuery.Type == "month":
|
2021-12-24 03:55:25 +00:00
|
|
|
|
bql = fmt.Sprintf("SELECT '\\', year, '-', month, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2022-07-03 15:26:48 +00:00
|
|
|
|
case statsQuery.Type == "year":
|
2021-12-24 03:55:25 +00:00
|
|
|
|
bql = fmt.Sprintf("SELECT '\\', year, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2022-07-03 15:26:48 +00:00
|
|
|
|
case statsQuery.Type == "sum":
|
2021-11-24 15:45:45 +00:00
|
|
|
|
bql = fmt.Sprintf("SELECT '\\', date, '\\', convert(balance, '%s'), '\\'", ledgerConfig.OperatingCurrency)
|
2022-07-03 15:26:48 +00:00
|
|
|
|
default:
|
2021-11-24 15:45:45 +00:00
|
|
|
|
OK(c, new([]string))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
statsResultList := make([]StatsResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]AccountTrendResult, 0)
|
|
|
|
|
|
for _, stats := range statsResultList {
|
2021-12-24 03:55:25 +00:00
|
|
|
|
commodities := strings.Split(stats.Value, ",")
|
|
|
|
|
|
// 多币种的处理方式:例如 75799.78 USD, 18500.00 IRAUSD, 176 VACHR
|
|
|
|
|
|
// 选择账本默认(ledgerConfig.OperatingCurrency)币种的值
|
|
|
|
|
|
var selectedCommodity = commodities[0]
|
|
|
|
|
|
for _, commodity := range commodities {
|
|
|
|
|
|
if strings.Contains(commodity, " "+ledgerConfig.OperatingCurrency) {
|
|
|
|
|
|
selectedCommodity = commodity
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fields := strings.Fields(selectedCommodity)
|
2021-11-26 09:12:07 +00:00
|
|
|
|
amount, _ := decimal.NewFromString(fields[0])
|
2021-12-24 03:55:25 +00:00
|
|
|
|
|
|
|
|
|
|
var date = stats.Key
|
|
|
|
|
|
// 月格式化日期
|
|
|
|
|
|
if statsQuery.Type == "month" {
|
|
|
|
|
|
yearMonth := strings.Split(date, "-")
|
|
|
|
|
|
date = fmt.Sprintf("%s-%s", strings.Trim(yearMonth[0], " "), strings.Trim(yearMonth[1], " "))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result = append(result, AccountTrendResult{Date: date, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]})
|
2021-11-24 15:45:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
OK(c, result)
|
|
|
|
|
|
}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
|
2021-12-02 14:48:45 +00:00
|
|
|
|
type AccountBalanceBQLResult struct {
|
|
|
|
|
|
Year string `bql:"year" json:"year"`
|
|
|
|
|
|
Month string `bql:"month" json:"month"`
|
|
|
|
|
|
Day string `bql:"day" json:"day"`
|
|
|
|
|
|
Balance string `bql:"balance" json:"balance"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AccountBalanceResult struct {
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Amount json.Number `json:"amount"`
|
|
|
|
|
|
OperatingCurrency string `json:"operatingCurrency"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func StatsAccountBalance(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
var statsQuery StatsQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: statsQuery.Prefix,
|
2023-07-02 15:31:30 +00:00
|
|
|
|
Year: statsQuery.Year,
|
|
|
|
|
|
Month: statsQuery.Month,
|
2021-12-02 14:48:45 +00:00
|
|
|
|
Where: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
balResultList := make([]AccountBalanceBQLResult, 0)
|
|
|
|
|
|
bql := fmt.Sprintf("select '\\', year, '\\', month, '\\', day, '\\', last(convert(balance, '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &balResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resultList := make([]AccountBalanceResult, 0)
|
|
|
|
|
|
for _, bqlResult := range balResultList {
|
|
|
|
|
|
if bqlResult.Balance != "" {
|
|
|
|
|
|
fields := strings.Fields(bqlResult.Balance)
|
|
|
|
|
|
amount, _ := decimal.NewFromString(fields[0])
|
|
|
|
|
|
resultList = append(resultList, AccountBalanceResult{
|
|
|
|
|
|
Date: bqlResult.Year + "-" + bqlResult.Month + "-" + bqlResult.Day,
|
|
|
|
|
|
Amount: json.Number(amount.Round(2).String()),
|
|
|
|
|
|
OperatingCurrency: fields[1],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
OK(c, resultList)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-01 06:43:07 +00:00
|
|
|
|
type AccountSankeyResult struct {
|
|
|
|
|
|
Nodes []AccountSankeyNode `json:"nodes"`
|
|
|
|
|
|
Links []AccountSankeyLink `json:"links"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AccountSankeyNode struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type AccountSankeyLink struct {
|
2024-10-03 07:50:33 +00:00
|
|
|
|
Source int `json:"source"`
|
|
|
|
|
|
Target int `json:"target"`
|
|
|
|
|
|
Value decimal.Decimal `json:"value"`
|
2024-10-01 06:43:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewAccountSankeyLink() *AccountSankeyLink {
|
|
|
|
|
|
return &AccountSankeyLink{
|
|
|
|
|
|
Source: -1,
|
|
|
|
|
|
Target: -1,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-03 07:50:33 +00:00
|
|
|
|
type TransactionAccountPositionBQLResult struct {
|
|
|
|
|
|
Id string
|
|
|
|
|
|
Account string
|
|
|
|
|
|
Position string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TransactionAccountPosition struct {
|
|
|
|
|
|
Id string
|
|
|
|
|
|
Account string
|
|
|
|
|
|
AccountName string
|
|
|
|
|
|
Value decimal.Decimal
|
|
|
|
|
|
OperatingCurrency string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// StatsAccountSankey 统计账户流向
|
2024-10-01 06:43:07 +00:00
|
|
|
|
func StatsAccountSankey(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
2024-10-03 07:50:33 +00:00
|
|
|
|
var statsQuery StatsQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: statsQuery.Prefix,
|
|
|
|
|
|
Year: statsQuery.Year,
|
|
|
|
|
|
Month: statsQuery.Month,
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
statsQueryResultList := make([]TransactionAccountPositionBQLResult, 0)
|
|
|
|
|
|
var bql string
|
|
|
|
|
|
// 账户不为空,则查询时间范围内所有涉及该账户的交易记录
|
|
|
|
|
|
if statsQuery.Prefix != "" {
|
|
|
|
|
|
bql = "SELECT '\\', id, '\\'"
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 清空 account 查询条件,改为使用 ID 查询包含该账户所有交易记录
|
|
|
|
|
|
queryParams.AccountLike = ""
|
|
|
|
|
|
queryParams.IDList = "|"
|
2024-10-03 07:50:33 +00:00
|
|
|
|
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, "|")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-10-04 10:05:15 +00:00
|
|
|
|
// 查询全部account的交易数据
|
|
|
|
|
|
bql = fmt.Sprintf("SELECT '\\', id, '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
2024-10-03 07:50:33 +00:00
|
|
|
|
|
|
|
|
|
|
statsQueryResultList = make([]TransactionAccountPositionBQLResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-03 07:50:33 +00:00
|
|
|
|
result := make([]Transaction, 0)
|
|
|
|
|
|
for _, queryRes := range statsQueryResultList {
|
|
|
|
|
|
if queryRes.Position != "" {
|
|
|
|
|
|
fields := strings.Fields(queryRes.Position)
|
2024-10-04 10:05:15 +00:00
|
|
|
|
account := queryRes.Account
|
|
|
|
|
|
if statsQuery.Level == 1 {
|
|
|
|
|
|
accountType := script.GetAccountType(ledgerConfig.Id, account)
|
|
|
|
|
|
account = accountType.Key + ":" + accountType.Name
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
result = append(result, Transaction{
|
|
|
|
|
|
Id: queryRes.Id,
|
2024-10-04 10:05:15 +00:00
|
|
|
|
Account: account,
|
2024-10-03 07:50:33 +00:00
|
|
|
|
Number: fields[0],
|
|
|
|
|
|
Currency: fields[1],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-10-04 10:05:15 +00:00
|
|
|
|
|
2024-10-03 07:50:33 +00:00
|
|
|
|
OK(c, buildSankeyResult(result))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildSankeyResult(transactions []Transaction) AccountSankeyResult {
|
2024-10-01 06:43:07 +00:00
|
|
|
|
accountSankeyResult := AccountSankeyResult{}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
accountSankeyResult.Nodes = make([]AccountSankeyNode, 0)
|
|
|
|
|
|
accountSankeyResult.Links = make([]AccountSankeyLink, 0)
|
2024-10-01 06:43:07 +00:00
|
|
|
|
// 构建 nodes 和 links
|
|
|
|
|
|
var nodes []AccountSankeyNode
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历 transactions 中按id进行分组
|
|
|
|
|
|
if len(transactions) > 0 {
|
|
|
|
|
|
for _, transaction := range transactions {
|
|
|
|
|
|
// 如果nodes中不存在该节点,则添加
|
2024-10-03 07:50:33 +00:00
|
|
|
|
account := transaction.Account
|
|
|
|
|
|
if !contains(nodes, account) {
|
|
|
|
|
|
nodes = append(nodes, AccountSankeyNode{Name: account})
|
2024-10-01 06:43:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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:]
|
|
|
|
|
|
|
2024-10-03 07:50:33 +00:00
|
|
|
|
account := transaction.Account
|
|
|
|
|
|
num, err := decimal.NewFromString(transaction.Number)
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
if currentLinkNode.Source == -1 && num.IsNegative() {
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if sourceTransaction.Account == "" {
|
|
|
|
|
|
sourceTransaction = transaction
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
currentLinkNode.Source = indexOf(nodes, account)
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if currentLinkNode.Target == -1 {
|
2024-10-03 07:50:33 +00:00
|
|
|
|
currentLinkNode.Value = num
|
2024-10-01 06:43:07 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// 比较 link node value 和 num 大小
|
2024-10-03 07:50:33 +00:00
|
|
|
|
delta := currentLinkNode.Value.Add(num)
|
|
|
|
|
|
if delta.IsZero() {
|
|
|
|
|
|
currentLinkNode.Value = num.Abs()
|
|
|
|
|
|
} else if delta.IsNegative() { // source > target
|
|
|
|
|
|
targetNumber, _ := decimal.NewFromString(targetTransaction.Number)
|
|
|
|
|
|
currentLinkNode.Value = targetNumber.Abs()
|
|
|
|
|
|
sourceTransaction.Number = delta.String()
|
2024-10-01 06:43:07 +00:00
|
|
|
|
transactions = append(transactions, sourceTransaction)
|
|
|
|
|
|
} else { // source < target
|
2024-10-03 07:50:33 +00:00
|
|
|
|
targetTransaction.Number = delta.String()
|
2024-10-01 06:43:07 +00:00
|
|
|
|
transactions = append(transactions, targetTransaction)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 完成一个 linkNode 的构建,重置判定条件
|
|
|
|
|
|
sourceTransaction.Account = ""
|
|
|
|
|
|
targetTransaction.Account = ""
|
|
|
|
|
|
links = append(links, *currentLinkNode)
|
|
|
|
|
|
currentLinkNode = NewAccountSankeyLink()
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
} else if currentLinkNode.Target == -1 && num.IsPositive() {
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if targetTransaction.Account == "" {
|
|
|
|
|
|
targetTransaction = transaction
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
currentLinkNode.Target = indexOf(nodes, account)
|
2024-10-01 06:43:07 +00:00
|
|
|
|
if currentLinkNode.Source == -1 {
|
2024-10-03 07:50:33 +00:00
|
|
|
|
currentLinkNode.Value = num
|
2024-10-01 06:43:07 +00:00
|
|
|
|
} else {
|
2024-10-03 07:50:33 +00:00
|
|
|
|
delta := currentLinkNode.Value.Add(num)
|
|
|
|
|
|
if delta.IsZero() {
|
|
|
|
|
|
currentLinkNode.Value = num.Abs()
|
|
|
|
|
|
} else if delta.IsNegative() { // source > target
|
|
|
|
|
|
currentLinkNode.Value = num.Abs()
|
|
|
|
|
|
sourceTransaction.Number = delta.String()
|
2024-10-01 06:43:07 +00:00
|
|
|
|
transactions = append(transactions, sourceTransaction)
|
|
|
|
|
|
} else { // source < target
|
2024-10-03 07:50:33 +00:00
|
|
|
|
sourceNumber, _ := decimal.NewFromString(sourceTransaction.Number)
|
|
|
|
|
|
currentLinkNode.Value = sourceNumber.Abs()
|
|
|
|
|
|
targetTransaction.Number = delta.String()
|
2024-10-01 06:43:07 +00:00
|
|
|
|
transactions = append(transactions, targetTransaction)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 完成一个 linkNode 的构建,重置判定条件
|
|
|
|
|
|
sourceTransaction.Account = ""
|
|
|
|
|
|
targetTransaction.Account = ""
|
|
|
|
|
|
links = append(links, *currentLinkNode)
|
|
|
|
|
|
currentLinkNode = NewAccountSankeyLink()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 将当前的 transaction 加入到队列末尾
|
|
|
|
|
|
transactions = append(transactions, transaction)
|
|
|
|
|
|
}
|
|
|
|
|
|
maxCycle -= 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-10-04 09:16:53 +00:00
|
|
|
|
accountSankeyResult.Links = links
|
|
|
|
|
|
// 同样source和target的link进行归并
|
|
|
|
|
|
accountSankeyResult.Links = aggregateLinkNodes(accountSankeyResult.Links)
|
|
|
|
|
|
//// source/target相反的link进行合并
|
|
|
|
|
|
//accountSankeyResult.Nodes = nodes
|
|
|
|
|
|
// 处理桑基图的link循环指向的问题
|
|
|
|
|
|
if hasCycle(accountSankeyResult.Links) {
|
|
|
|
|
|
newNodes, newLinks := breakCycleAndAddNode(accountSankeyResult.Nodes, accountSankeyResult.Links)
|
|
|
|
|
|
accountSankeyResult.Nodes = newNodes
|
|
|
|
|
|
accountSankeyResult.Links = newLinks
|
|
|
|
|
|
}
|
2024-10-01 06:43:07 +00:00
|
|
|
|
}
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 过滤 source 和 target 相同的节点
|
|
|
|
|
|
|
2024-10-03 07:50:33 +00:00
|
|
|
|
return accountSankeyResult
|
2024-10-01 06:43:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 检查是否存在循环引用
|
|
|
|
|
|
func hasCycle(links []AccountSankeyLink) bool {
|
|
|
|
|
|
visited := make(map[int]bool)
|
|
|
|
|
|
recStack := make(map[int]bool)
|
|
|
|
|
|
|
|
|
|
|
|
var dfs func(node int) bool
|
|
|
|
|
|
dfs = func(node int) bool {
|
|
|
|
|
|
if recStack[node] {
|
|
|
|
|
|
return true // 找到循环
|
|
|
|
|
|
}
|
|
|
|
|
|
if visited[node] {
|
|
|
|
|
|
return false // 已访问过,不再检查
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visited[node] = true
|
|
|
|
|
|
recStack[node] = true
|
|
|
|
|
|
|
|
|
|
|
|
// 检查所有 links,看是否有从当前节点指向其他节点
|
|
|
|
|
|
for _, link := range links {
|
|
|
|
|
|
if link.Source == node {
|
|
|
|
|
|
if dfs(link.Target) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
recStack[node] = false // 当前节点的 DFS 结束
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有节点
|
|
|
|
|
|
for _, link := range links {
|
|
|
|
|
|
if dfs(link.Source) {
|
|
|
|
|
|
return true // 发现循环
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false // 没有循环
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打破循环引用,添加新的节点
|
|
|
|
|
|
func breakCycleAndAddNode(nodes []AccountSankeyNode, links []AccountSankeyLink) ([]AccountSankeyNode, []AccountSankeyLink) {
|
|
|
|
|
|
visited := make(map[int]bool)
|
|
|
|
|
|
recStack := make(map[int]bool)
|
|
|
|
|
|
newNodeCount := 0 // 计数新节点
|
|
|
|
|
|
|
|
|
|
|
|
var dfs func(node int) bool
|
|
|
|
|
|
newNodes := make(map[int]int) // 记录新节点的映射
|
|
|
|
|
|
|
|
|
|
|
|
dfs = func(node int) bool {
|
|
|
|
|
|
if recStack[node] {
|
|
|
|
|
|
return true // 找到循环
|
|
|
|
|
|
}
|
|
|
|
|
|
if visited[node] {
|
|
|
|
|
|
return false // 已访问过,不再检查
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visited[node] = true
|
|
|
|
|
|
recStack[node] = true
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有 links,看是否有从当前节点指向其他节点
|
|
|
|
|
|
for _, link := range links {
|
|
|
|
|
|
if link.Source == node {
|
|
|
|
|
|
if dfs(link.Target) {
|
|
|
|
|
|
// 检测到循环,创建新节点
|
|
|
|
|
|
originalNode := nodes[node]
|
|
|
|
|
|
newNode := AccountSankeyNode{
|
|
|
|
|
|
Name: originalNode.Name + "1", // 新节点名称
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将新节点添加到 nodes 列表中
|
|
|
|
|
|
nodes = append(nodes, newNode)
|
|
|
|
|
|
newNodeIndex := len(nodes) - 1
|
|
|
|
|
|
newNodes[node] = newNodeIndex // 记录原节点到新节点的映射
|
|
|
|
|
|
|
|
|
|
|
|
// 更新当前节点的所有链接,将 target 指向新节点
|
|
|
|
|
|
for i := range links {
|
|
|
|
|
|
if links[i].Source == node {
|
|
|
|
|
|
links[i].Target = newNodeIndex
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newNodeCount++ // 增加新节点计数
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
recStack[node] = false // 当前节点的 DFS 结束
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有节点,检测循环
|
|
|
|
|
|
for _, link := range links {
|
|
|
|
|
|
if !visited[link.Source] {
|
|
|
|
|
|
dfs(link.Source) // 如果未访问过,则调用 DFS
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nodes, links
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-01 06:43:07 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 聚合函数,聚合相同 source 和 target(相反方向)的值
|
|
|
|
|
|
func aggregateLinkNodes(links []AccountSankeyLink) []AccountSankeyLink {
|
|
|
|
|
|
// 创建一个映射来存储连接
|
|
|
|
|
|
nodeMap := make(map[string]decimal.Decimal)
|
2024-10-03 07:50:33 +00:00
|
|
|
|
|
2024-10-04 09:16:53 +00:00
|
|
|
|
for _, link := range links {
|
|
|
|
|
|
if link.Source == link.Target {
|
|
|
|
|
|
fmt.Printf("%-%s-%d", link.Source, link.Target, link.Value)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
|
2024-10-04 09:16:53 +00:00
|
|
|
|
key := fmt.Sprintf("%d-%d", link.Source, link.Target)
|
|
|
|
|
|
reverseKey := fmt.Sprintf("%d-%d", link.Target, link.Source)
|
|
|
|
|
|
if existingValue, found := nodeMap[key]; found {
|
|
|
|
|
|
// 如果已存在相同方向,累加 value
|
|
|
|
|
|
nodeMap[key] = existingValue.Add(link.Value)
|
|
|
|
|
|
} else if existingValue, found := nodeMap[reverseKey]; found {
|
|
|
|
|
|
// 如果存在相反方向,确定最终的 source 和 target
|
|
|
|
|
|
totalValue := existingValue.Sub(link.Value)
|
|
|
|
|
|
if totalValue.IsPositive() {
|
|
|
|
|
|
nodeMap[reverseKey] = totalValue
|
|
|
|
|
|
} else if totalValue.IsZero() {
|
|
|
|
|
|
delete(nodeMap, reverseKey)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete(nodeMap, reverseKey)
|
|
|
|
|
|
nodeMap[key] = totalValue.Abs()
|
|
|
|
|
|
}
|
2024-10-03 07:50:33 +00:00
|
|
|
|
} else {
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 否则直接插入新的 value
|
|
|
|
|
|
nodeMap[key] = link.Value
|
2024-10-03 07:50:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-04 09:16:53 +00:00
|
|
|
|
// 将结果转换为 slice
|
|
|
|
|
|
result := make([]AccountSankeyLink, 0)
|
|
|
|
|
|
for key, value := range nodeMap {
|
|
|
|
|
|
var parts = strings.Split(key, "-")
|
|
|
|
|
|
source, _ := strconv.Atoi(parts[0])
|
|
|
|
|
|
target, _ := strconv.Atoi(parts[1])
|
|
|
|
|
|
result = append(result, AccountSankeyLink{Source: source, Target: target, Value: value})
|
2024-10-03 07:50:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-25 07:41:28 +00:00
|
|
|
|
type MonthTotalBQLResult struct {
|
|
|
|
|
|
Year int
|
|
|
|
|
|
Month int
|
|
|
|
|
|
Value string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type MonthTotal struct {
|
2021-11-26 09:12:07 +00:00
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Month string `json:"month"`
|
|
|
|
|
|
Amount json.Number `json:"amount"`
|
|
|
|
|
|
OperatingCurrency string `json:"operatingCurrency"`
|
2021-11-25 07:41:28 +00:00
|
|
|
|
}
|
2021-12-22 15:13:37 +00:00
|
|
|
|
type MonthTotalSort []MonthTotal
|
|
|
|
|
|
|
|
|
|
|
|
func (s MonthTotalSort) Len() int {
|
|
|
|
|
|
return len(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
func (s MonthTotalSort) Swap(i, j int) {
|
|
|
|
|
|
s[i], s[j] = s[j], s[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
func (s MonthTotalSort) Less(i, j int) bool {
|
|
|
|
|
|
iYearMonth, _ := time.Parse("2006-1", s[i].Month)
|
|
|
|
|
|
jYearMonth, _ := time.Parse("2006-1", s[j].Month)
|
|
|
|
|
|
return iYearMonth.Before(jYearMonth)
|
|
|
|
|
|
}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
|
|
|
|
|
|
func StatsMonthTotal(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
|
|
|
|
|
|
monthSet := make(map[string]bool)
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: "Income",
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
OrderBy: "year, month",
|
|
|
|
|
|
}
|
|
|
|
|
|
// 按月查询收入
|
|
|
|
|
|
queryIncomeBql := fmt.Sprintf("select '\\', year, '\\', month, '\\', neg(sum(convert(value(position), '%s'))), '\\'", ledgerConfig.OperatingCurrency)
|
|
|
|
|
|
monthIncomeTotalResultList := make([]MonthTotalBQLResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, queryIncomeBql, &queryParams, &monthIncomeTotalResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
monthIncomeMap := make(map[string]MonthTotalBQLResult)
|
|
|
|
|
|
for _, income := range monthIncomeTotalResultList {
|
|
|
|
|
|
month := fmt.Sprintf("%d-%d", income.Year, income.Month)
|
|
|
|
|
|
monthSet[month] = true
|
|
|
|
|
|
monthIncomeMap[month] = income
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按月查询支出
|
|
|
|
|
|
queryParams.AccountLike = "Expenses"
|
|
|
|
|
|
queryExpensesBql := fmt.Sprintf("select '\\', year, '\\', month, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
|
|
|
|
|
monthExpensesTotalResultList := make([]MonthTotalBQLResult, 0)
|
|
|
|
|
|
err = script.BQLQueryListByCustomSelect(ledgerConfig, queryExpensesBql, &queryParams, &monthExpensesTotalResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
monthExpensesMap := make(map[string]MonthTotalBQLResult)
|
|
|
|
|
|
for _, expenses := range monthExpensesTotalResultList {
|
|
|
|
|
|
month := fmt.Sprintf("%d-%d", expenses.Year, expenses.Month)
|
|
|
|
|
|
monthSet[month] = true
|
|
|
|
|
|
monthExpensesMap[month] = expenses
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
monthTotalResult := make([]MonthTotal, 0)
|
|
|
|
|
|
// 合并结果
|
|
|
|
|
|
var monthIncome, monthExpenses MonthTotal
|
2021-11-26 09:12:07 +00:00
|
|
|
|
var monthIncomeAmount, monthExpensesAmount decimal.Decimal
|
2021-11-25 07:41:28 +00:00
|
|
|
|
for month := range monthSet {
|
|
|
|
|
|
if monthIncomeMap[month].Value != "" {
|
|
|
|
|
|
fields := strings.Fields(monthIncomeMap[month].Value)
|
2021-11-26 09:12:07 +00:00
|
|
|
|
amount, _ := decimal.NewFromString(fields[0])
|
|
|
|
|
|
monthIncomeAmount = amount
|
|
|
|
|
|
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
} else {
|
2021-11-26 09:12:07 +00:00
|
|
|
|
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
monthTotalResult = append(monthTotalResult, monthIncome)
|
|
|
|
|
|
|
|
|
|
|
|
if monthExpensesMap[month].Value != "" {
|
|
|
|
|
|
fields := strings.Fields(monthExpensesMap[month].Value)
|
2021-11-26 09:12:07 +00:00
|
|
|
|
amount, _ := decimal.NewFromString(fields[0])
|
|
|
|
|
|
monthExpensesAmount = amount
|
|
|
|
|
|
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
} else {
|
2021-11-26 09:12:07 +00:00
|
|
|
|
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
|
2021-11-25 07:41:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
monthTotalResult = append(monthTotalResult, monthExpenses)
|
2021-11-26 09:12:07 +00:00
|
|
|
|
monthTotalResult = append(monthTotalResult, MonthTotal{Type: "结余", Month: month, Amount: json.Number(monthIncomeAmount.Sub(monthExpensesAmount).Round(2).String()), OperatingCurrency: ledgerConfig.OperatingCurrency})
|
2021-11-25 07:41:28 +00:00
|
|
|
|
}
|
2021-12-22 15:13:37 +00:00
|
|
|
|
sort.Sort(MonthTotalSort(monthTotalResult))
|
2021-11-25 07:41:28 +00:00
|
|
|
|
OK(c, monthTotalResult)
|
|
|
|
|
|
}
|
2021-11-26 09:12:07 +00:00
|
|
|
|
|
2022-06-05 03:07:03 +00:00
|
|
|
|
type StatsMonthQuery struct {
|
|
|
|
|
|
Year int `form:"year"`
|
|
|
|
|
|
Month int `form:"month"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type StatsCalendarQueryResult struct {
|
|
|
|
|
|
Date string
|
|
|
|
|
|
Account string
|
|
|
|
|
|
Position string
|
|
|
|
|
|
}
|
|
|
|
|
|
type StatsCalendarResult struct {
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Account string `json:"account"`
|
|
|
|
|
|
Amount json.Number `json:"amount"`
|
|
|
|
|
|
Currency string `json:"currency"`
|
|
|
|
|
|
CurrencySymbol string `json:"currencySymbol"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func StatsMonthCalendar(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
var statsMonthQuery StatsMonthQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsMonthQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
Year: statsMonthQuery.Year,
|
|
|
|
|
|
Month: statsMonthQuery.Month,
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bql := fmt.Sprintf("SELECT '\\', date, '\\', root(account, 1), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
|
|
|
|
|
statsCalendarQueryResult := make([]StatsCalendarQueryResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsCalendarQueryResult)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resultList := make([]StatsCalendarResult, 0)
|
|
|
|
|
|
for _, queryRes := range statsCalendarQueryResult {
|
|
|
|
|
|
if queryRes.Position != "" {
|
|
|
|
|
|
fields := strings.Fields(queryRes.Position)
|
|
|
|
|
|
resultList = append(resultList,
|
|
|
|
|
|
StatsCalendarResult{
|
|
|
|
|
|
Date: queryRes.Date,
|
|
|
|
|
|
Account: queryRes.Account,
|
|
|
|
|
|
Amount: json.Number(fields[0]),
|
|
|
|
|
|
Currency: fields[1],
|
2023-12-07 05:57:43 +00:00
|
|
|
|
CurrencySymbol: script.GetCommoditySymbol(ledgerConfig.Id, fields[1]),
|
2022-06-05 03:07:03 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
OK(c, resultList)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-26 09:12:07 +00:00
|
|
|
|
type StatsPayeeQueryResult struct {
|
|
|
|
|
|
Payee string
|
|
|
|
|
|
Count int32
|
|
|
|
|
|
Position string
|
|
|
|
|
|
}
|
|
|
|
|
|
type StatsPayeeResult struct {
|
|
|
|
|
|
Payee string `json:"payee"`
|
|
|
|
|
|
Currency string `json:"operatingCurrency"`
|
|
|
|
|
|
Value json.Number `json:"value"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type StatsPayeeResultSort []StatsPayeeResult
|
|
|
|
|
|
|
|
|
|
|
|
func (s StatsPayeeResultSort) Len() int {
|
|
|
|
|
|
return len(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
func (s StatsPayeeResultSort) Swap(i, j int) {
|
|
|
|
|
|
s[i], s[j] = s[j], s[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
func (s StatsPayeeResultSort) Less(i, j int) bool {
|
|
|
|
|
|
a, _ := s[i].Value.Float64()
|
|
|
|
|
|
b, _ := s[j].Value.Float64()
|
|
|
|
|
|
return a <= b
|
|
|
|
|
|
}
|
|
|
|
|
|
func StatsPayee(c *gin.Context) {
|
|
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
|
|
|
|
var statsQuery StatsQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&statsQuery); err != nil {
|
|
|
|
|
|
BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryParams := script.QueryParams{
|
|
|
|
|
|
AccountLike: statsQuery.Prefix,
|
|
|
|
|
|
Year: statsQuery.Year,
|
|
|
|
|
|
Month: statsQuery.Month,
|
|
|
|
|
|
Where: true,
|
|
|
|
|
|
Currency: ledgerConfig.OperatingCurrency,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bql := fmt.Sprintf("SELECT '\\', payee, '\\', count(payee), '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
|
|
|
|
|
|
statsPayeeQueryResultList := make([]StatsPayeeQueryResult, 0)
|
|
|
|
|
|
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsPayeeQueryResultList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
InternalError(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]StatsPayeeResult, 0)
|
|
|
|
|
|
for _, l := range statsPayeeQueryResultList {
|
2024-03-08 05:00:30 +00:00
|
|
|
|
// 交易账户名称非空
|
2021-11-26 09:12:07 +00:00
|
|
|
|
if l.Payee != "" {
|
|
|
|
|
|
payee := StatsPayeeResult{
|
|
|
|
|
|
Payee: l.Payee,
|
|
|
|
|
|
Currency: ledgerConfig.OperatingCurrency,
|
|
|
|
|
|
}
|
2024-03-08 05:00:30 +00:00
|
|
|
|
//查询交易次数
|
2021-11-26 09:12:07 +00:00
|
|
|
|
if statsQuery.Type == "cot" {
|
|
|
|
|
|
payee.Value = json.Number(decimal.NewFromInt32(l.Count).String())
|
|
|
|
|
|
} else {
|
2024-03-08 05:00:30 +00:00
|
|
|
|
//查询交易金额,要过滤掉空白交易金额的科目,
|
|
|
|
|
|
// 比如 记账购买后又全额退款导致科目交易条目数>0但是累计金额=0
|
|
|
|
|
|
if l.Position != "" {
|
|
|
|
|
|
// 读取交易金额相关信息
|
|
|
|
|
|
fields := strings.Fields(l.Position)
|
|
|
|
|
|
// 交易金额
|
|
|
|
|
|
total, err := decimal.NewFromString(fields[0])
|
|
|
|
|
|
// 错误处理
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if statsQuery.Type == "avg" {
|
|
|
|
|
|
// 如果是查询平均交易金额
|
|
|
|
|
|
payee.Value = json.Number(total.Div(decimal.NewFromInt32(l.Count)).Round(2).String())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果是查询总交易金额
|
|
|
|
|
|
payee.Value = json.Number(fields[0])
|
|
|
|
|
|
}
|
2021-11-26 09:12:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
result = append(result, payee)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Sort(StatsPayeeResultSort(result))
|
|
|
|
|
|
OK(c, result)
|
|
|
|
|
|
}
|
2022-04-12 16:30:38 +00:00
|
|
|
|
|
2022-04-13 16:23:08 +00:00
|
|
|
|
func StatsCommodityPrice(c *gin.Context) {
|
2024-08-18 17:02:42 +00:00
|
|
|
|
OK(c, script.BeanReportAllPrices(script.GetLedgerConfigFromContext(c)))
|
2022-04-12 16:30:38 +00:00
|
|
|
|
}
|