beancount-gs/service/stats.go

909 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/beancount-gs/script"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
)
type YearMonth struct {
Year string `bql:"distinct year(date)" json:"year"`
Month string `bql:"month(date)" json:"month"`
}
func MonthsList(c *gin.Context) {
ledgerConfig := script.GetLedgerConfigFromContext(c)
// 添加排序
queryParams := script.GetQueryParams(c)
queryParams.OrderBy = "year, month desc"
yearMonthList := make([]YearMonth, 0)
err := script.BQLQueryList(ledgerConfig, &queryParams, &yearMonthList)
if err != nil {
InternalError(c, err.Error())
return
}
months := make([]string, 0)
for _, yearMonth := range yearMonthList {
months = append(months, yearMonth.Year+"-"+yearMonth.Month)
}
OK(c, months)
}
type StatsResult struct {
Key string
Value string
}
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)
accountTypeTotalList := make([]StatsResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, selectBql, &queryParams, &accountTypeTotalList)
if err != nil {
InternalError(c, err.Error())
return
}
result := make(map[string]string)
for _, total := range accountTypeTotalList {
fields := strings.Fields(total.Value)
if len(fields) > 1 {
result[total.Key] = fields[0]
}
}
OK(c, result)
}
type StatsQuery struct {
Prefix string `form:"prefix"`
Year int `form:"year"`
Month int `form:"month"`
Level int `form:"level"`
Type string `form:"type"`
}
type AccountPercentQueryResult struct {
Account string
Position string
}
type AccountPercentResult struct {
Account string `json:"account"`
Amount decimal.Decimal `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
}
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,
}
bql := fmt.Sprintf("SELECT '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
statsQueryResultList := make([]AccountPercentQueryResult, 0)
err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList)
if err != nil {
InternalError(c, err.Error())
return
}
result := make([]AccountPercentResult, 0)
for _, queryRes := range statsQueryResultList {
if queryRes.Position != "" {
fields := strings.Fields(queryRes.Position)
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]})
}
}
}
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
}
type AccountTrendResult struct {
Date string `json:"date"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
}
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
switch {
case statsQuery.Type == "day":
bql = fmt.Sprintf("SELECT '\\', date, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
case statsQuery.Type == "month":
bql = fmt.Sprintf("SELECT '\\', year, '-', month, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
case statsQuery.Type == "year":
bql = fmt.Sprintf("SELECT '\\', year, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency)
case statsQuery.Type == "sum":
bql = fmt.Sprintf("SELECT '\\', date, '\\', convert(balance, '%s'), '\\'", ledgerConfig.OperatingCurrency)
default:
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 {
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)
amount, _ := decimal.NewFromString(fields[0])
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]})
}
OK(c, result)
}
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,
Year: statsQuery.Year,
Month: statsQuery.Month,
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)
}
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 decimal.Decimal `json:"value"`
}
func NewAccountSankeyLink() *AccountSankeyLink {
return &AccountSankeyLink{
Source: -1,
Target: -1,
}
}
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) {
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,
}
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
}
// 清空 account 查询条件,改为使用 ID 查询包含该账户所有交易记录
queryParams.AccountLike = ""
queryParams.IDList = "|"
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, "|")
}
}
// 查询全部account的交易数据
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 {
InternalError(c, err.Error())
return
}
result := make([]Transaction, 0)
for _, queryRes := range statsQueryResultList {
if queryRes.Position != "" {
fields := strings.Fields(queryRes.Position)
account := queryRes.Account
if statsQuery.Level == 1 {
accountType := script.GetAccountType(ledgerConfig.Id, account)
account = accountType.Key + ":" + accountType.Name
}
result = append(result, Transaction{
Id: queryRes.Id,
Account: account,
Number: fields[0],
Currency: fields[1],
})
}
}
OK(c, buildSankeyResult(result))
}
func buildSankeyResult(transactions []Transaction) AccountSankeyResult {
accountSankeyResult := AccountSankeyResult{}
accountSankeyResult.Nodes = make([]AccountSankeyNode, 0)
accountSankeyResult.Links = make([]AccountSankeyLink, 0)
// 构建 nodes 和 links
var nodes []AccountSankeyNode
// 遍历 transactions 中按id进行分组
if len(transactions) > 0 {
for _, transaction := range transactions {
// 如果nodes中不存在该节点则添加
account := transaction.Account
if !contains(nodes, account) {
nodes = append(nodes, AccountSankeyNode{Name: account})
}
}
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:]
account := transaction.Account
num, err := decimal.NewFromString(transaction.Number)
if err != nil {
continue
}
if currentLinkNode.Source == -1 && num.IsNegative() {
if sourceTransaction.Account == "" {
sourceTransaction = transaction
}
currentLinkNode.Source = indexOf(nodes, account)
if currentLinkNode.Target == -1 {
currentLinkNode.Value = num
} else {
// 比较 link node value 和 num 大小
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()
transactions = append(transactions, sourceTransaction)
} else { // source < target
targetTransaction.Number = delta.String()
transactions = append(transactions, targetTransaction)
}
// 完成一个 linkNode 的构建,重置判定条件
sourceTransaction.Account = ""
targetTransaction.Account = ""
links = append(links, *currentLinkNode)
currentLinkNode = NewAccountSankeyLink()
}
} else if currentLinkNode.Target == -1 && num.IsPositive() {
if targetTransaction.Account == "" {
targetTransaction = transaction
}
currentLinkNode.Target = indexOf(nodes, account)
if currentLinkNode.Source == -1 {
currentLinkNode.Value = num
} else {
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()
transactions = append(transactions, sourceTransaction)
} else { // source < target
sourceNumber, _ := decimal.NewFromString(sourceTransaction.Number)
currentLinkNode.Value = sourceNumber.Abs()
targetTransaction.Number = delta.String()
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
// 同样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
}
}
// 过滤 source 和 target 相同的节点
return accountSankeyResult
}
// 检查是否存在循环引用
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
}
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
}
// 聚合函数,聚合相同 source 和 target相反方向的值
func aggregateLinkNodes(links []AccountSankeyLink) []AccountSankeyLink {
// 创建一个映射来存储连接
nodeMap := make(map[string]decimal.Decimal)
for _, link := range links {
if link.Source == link.Target {
fmt.Printf("%-%s-%d", link.Source, link.Target, link.Value)
continue
}
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()
}
} else {
// 否则直接插入新的 value
nodeMap[key] = link.Value
}
}
// 将结果转换为 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})
}
return result
}
type MonthTotalBQLResult struct {
Year int
Month int
Value string
}
type MonthTotal struct {
Type string `json:"type"`
Month string `json:"month"`
Amount json.Number `json:"amount"`
OperatingCurrency string `json:"operatingCurrency"`
}
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)
}
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
var monthIncomeAmount, monthExpensesAmount decimal.Decimal
for month := range monthSet {
if monthIncomeMap[month].Value != "" {
fields := strings.Fields(monthIncomeMap[month].Value)
amount, _ := decimal.NewFromString(fields[0])
monthIncomeAmount = amount
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
} else {
monthIncome = MonthTotal{Type: "收入", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
}
monthTotalResult = append(monthTotalResult, monthIncome)
if monthExpensesMap[month].Value != "" {
fields := strings.Fields(monthExpensesMap[month].Value)
amount, _ := decimal.NewFromString(fields[0])
monthExpensesAmount = amount
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}
} else {
monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency}
}
monthTotalResult = append(monthTotalResult, monthExpenses)
monthTotalResult = append(monthTotalResult, MonthTotal{Type: "结余", Month: month, Amount: json.Number(monthIncomeAmount.Sub(monthExpensesAmount).Round(2).String()), OperatingCurrency: ledgerConfig.OperatingCurrency})
}
sort.Sort(MonthTotalSort(monthTotalResult))
OK(c, monthTotalResult)
}
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],
CurrencySymbol: script.GetCommoditySymbol(ledgerConfig.Id, fields[1]),
})
}
}
OK(c, resultList)
}
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 {
// 交易账户名称非空
if l.Payee != "" {
payee := StatsPayeeResult{
Payee: l.Payee,
Currency: ledgerConfig.OperatingCurrency,
}
//查询交易次数
if statsQuery.Type == "cot" {
payee.Value = json.Number(decimal.NewFromInt32(l.Count).String())
} else {
//查询交易金额,要过滤掉空白交易金额的科目,
// 比如 记账购买后又全额退款导致科目交易条目数>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])
}
}
}
result = append(result, payee)
}
}
sort.Sort(StatsPayeeResultSort(result))
OK(c, result)
}
func StatsCommodityPrice(c *gin.Context) {
OK(c, script.BeanReportAllPrices(script.GetLedgerConfigFromContext(c)))
}