diff --git a/go.mod b/go.mod index 5ae37fe..82258c0 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/beancount-gs go 1.17 -require github.com/gin-gonic/gin v1.7.4 +require ( + github.com/gin-gonic/gin v1.7.4 + github.com/shopspring/decimal v1.3.1 +) require ( github.com/gin-contrib/sse v0.1.0 // indirect @@ -15,7 +18,6 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/shopspring/decimal v1.3.1 // indirect github.com/ugorji/go/codec v1.2.6 // indirect golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect diff --git a/go.sum b/go.sum index abc1a89..1551445 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -19,6 +21,7 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -26,9 +29,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -43,8 +48,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -52,6 +59,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= @@ -62,21 +70,19 @@ github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxW golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 h1:5QRxNnVsaJP6NAse0UdkRgL3zHMvCRRkrDVLNdNpdy4= -golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c h1:DHcbWVXeY+0Y8HHKR+rbLwnoh2F4tNCY7rTiHJ30RmA= -golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -87,6 +93,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= @@ -94,6 +101,7 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -101,4 +109,5 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/script/bql.go b/script/bql.go index 10387df..785cca3 100644 --- a/script/bql.go +++ b/script/bql.go @@ -12,6 +12,7 @@ import ( type QueryParams struct { Where bool `bql:"where"` + Currency string `bql:"currency ="` Year int `bql:"year ="` Month int `bql:"month ="` Tag string `bql:"in tags"` @@ -196,7 +197,7 @@ func parseResult(output string, queryResultPtr interface{}, selectOne bool) erro jsonName = field.Name } switch field.Type.Kind() { - case reflect.Int: + case reflect.Int, reflect.Int32: i, err := strconv.Atoi(strings.Trim(val, " ")) if err != nil { panic(err) diff --git a/script/sort.go b/script/sort.go index e19e218..477ab24 100644 --- a/script/sort.go +++ b/script/sort.go @@ -1,7 +1,6 @@ package script type AccountTypeSort []AccountType -type AccountSort []Account func (s AccountTypeSort) Len() int { return len(s) @@ -15,6 +14,8 @@ func (s AccountTypeSort) Less(i, j int) bool { return s[i].Key <= s[j].Key } +type AccountSort []Account + func (s AccountSort) Len() int { return len(s) } diff --git a/server.go b/server.go index 97cf4c0..a505e17 100644 --- a/server.go +++ b/server.go @@ -54,6 +54,7 @@ func RegisterRouter(router *gin.Engine) { authorized.GET("/account/type", service.QueryAccountType) authorized.GET("/stats/months", service.MonthsList) authorized.GET("/stats/total", service.StatsTotal) + authorized.GET("/stats/payee", service.StatsPayee) authorized.GET("/stats/account/percent", service.StatsAccountPercent) authorized.GET("/stats/account/trend", service.StatsAccountTrend) authorized.GET("/stats/month/total", service.StatsMonthTotal) diff --git a/service/stats.go b/service/stats.go index a717833..e4b32b6 100644 --- a/service/stats.go +++ b/service/stats.go @@ -1,10 +1,12 @@ package service import ( + "encoding/json" "fmt" "github.com/beancount-gs/script" "github.com/gin-gonic/gin" - "strconv" + "github.com/shopspring/decimal" + "sort" "strings" ) @@ -63,10 +65,15 @@ type StatsQuery struct { Type string `form:"type"` } +type AccountPercentQueryResult struct { + Account string + Position string +} + type AccountPercentResult struct { - Account string `json:"account"` - Amount string `json:"amount"` - OperatingCurrency string `json:"operatingCurrency"` + Account string `json:"account"` + Amount json.Number `json:"amount"` + OperatingCurrency string `json:"operatingCurrency"` } func StatsAccountPercent(c *gin.Context) { @@ -91,25 +98,27 @@ func StatsAccountPercent(c *gin.Context) { bql = fmt.Sprintf("SELECT '\\', account, '\\', sum(convert(value(position), '%s')), '\\'", ledgerConfig.OperatingCurrency) } - statsResultList := make([]AccountPercentResult, 0) - err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsResultList) + statsQueryResultList := make([]AccountPercentQueryResult, 0) + err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, &queryParams, &statsQueryResultList) if err != nil { InternalError(c, err.Error()) return } - for idx, result := range statsResultList { - fields := strings.Fields(result.Amount) - statsResultList[idx].Amount = fields[0] - statsResultList[idx].OperatingCurrency = fields[1] + result := make([]AccountPercentResult, 0) + for _, queryRes := range statsQueryResultList { + if queryRes.Position != "" { + fields := strings.Fields(queryRes.Position) + result = append(result, AccountPercentResult{Account: queryRes.Account, Amount: json.Number(fields[0]), OperatingCurrency: fields[1]}) + } } - OK(c, statsResultList) + OK(c, result) } type AccountTrendResult struct { - Date string `json:"date"` - Amount float64 `json:"amount"` - OperatingCurrency string `json:"operatingCurrency"` + Date string `json:"date"` + Amount json.Number `json:"amount"` + OperatingCurrency string `json:"operatingCurrency"` } func StatsAccountTrend(c *gin.Context) { @@ -146,8 +155,8 @@ func StatsAccountTrend(c *gin.Context) { result := make([]AccountTrendResult, 0) for _, stats := range statsResultList { fields := strings.Fields(stats.Value) - amount, _ := strconv.ParseFloat(fields[0], 32) - result = append(result, AccountTrendResult{Date: stats.Key, Amount: amount, OperatingCurrency: fields[1]}) + amount, _ := decimal.NewFromString(fields[0]) + result = append(result, AccountTrendResult{Date: stats.Key, Amount: json.Number(amount.Round(2).String()), OperatingCurrency: fields[1]}) } OK(c, result) } @@ -159,10 +168,10 @@ type MonthTotalBQLResult struct { } type MonthTotal struct { - Type string `json:"type"` - Month string `json:"month"` - Amount float64 `json:"amount"` - OperatingCurrency string `json:"operatingCurrency"` + Type string `json:"type"` + Month string `json:"month"` + Amount json.Number `json:"amount"` + OperatingCurrency string `json:"operatingCurrency"` } func StatsMonthTotal(c *gin.Context) { @@ -208,25 +217,104 @@ func StatsMonthTotal(c *gin.Context) { 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, _ := strconv.ParseFloat(fields[0], 64) - monthIncome = MonthTotal{Type: "收入", Month: month, Amount: amount, OperatingCurrency: fields[1]} + 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} + 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, _ := strconv.ParseFloat(fields[0], 64) - monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: amount, OperatingCurrency: fields[1]} + 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} + monthExpenses = MonthTotal{Type: "支出", Month: month, Amount: "0", OperatingCurrency: ledgerConfig.OperatingCurrency} } monthTotalResult = append(monthTotalResult, monthExpenses) - monthTotalResult = append(monthTotalResult, MonthTotal{Type: "结余", Month: month, Amount: monthIncome.Amount - monthExpenses.Amount, OperatingCurrency: ledgerConfig.OperatingCurrency}) + monthTotalResult = append(monthTotalResult, MonthTotal{Type: "结余", Month: month, Amount: json.Number(monthIncomeAmount.Sub(monthExpensesAmount).Round(2).String()), OperatingCurrency: ledgerConfig.OperatingCurrency}) } OK(c, monthTotalResult) } + +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 { + 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) +}