Skip to content

Commit

Permalink
Merge pull request #2 from yann0917/develop
Browse files Browse the repository at this point in the history
🎨 charts
  • Loading branch information
yann0917 authored Mar 15, 2022
2 parents 2ac0ebb + 3197dbe commit e174739
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 21 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ flowchart LR

### Step 3 分析账单数据

1. 执行 `awm chart`
2. 选择合并后的账单 `output_xxxxxxxxxx.csv`
3. 按 enter 键执行
4. 输出 `charts.html` 为分析好的账单,使用浏览器打开即可查看

## LICENSE

[MIT](./LICENSE)
Expand Down
128 changes: 125 additions & 3 deletions cmd/echart.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package cmd

import (
"os"
"sort"
"strings"

"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/go-echarts/go-echarts/v2/types"
"github.com/spf13/cobra"
)

var _ = &cobra.Command{
var chartCmd = &cobra.Command{
Use: "chart",
Short: "图表分析账单",
Long: `使用 awm chart 分析合并后的账单`,
Expand All @@ -15,15 +20,132 @@ var _ = &cobra.Command{
oPath = PromptSelectAnalysis()
var b Bill
if strings.Contains(oPath, ".csv") {
_, err := b.ReadMergeFile(oPath)
list, err := b.ReadMergeFile(oPath)
if err != nil {
return err
}
Bar(list)
}

return nil
},
}

func init() {
// rootCmd.AddCommand(chartCmd)
rootCmd.AddCommand(chartCmd)
}

// generate random data for bar chart
func generateBarItems(list []*Account, pType int) []opts.BarData {
items := make([]opts.BarData, 0)
iTotal, oTotal := 0.0, 0.0
for _, account := range list {
if account.IO == "收入" {
iTotal += account.Money * 100
}
if account.IO == "支出" {
oTotal += account.Money * 100
}
}
if pType == 1 {
items = append(items, opts.BarData{Name: "累计收入", Value: iTotal / 100})
}
if pType == 2 {
items = append(items, opts.BarData{Name: "累计支出", Value: oTotal / 100})
}
return items
}

func generateLineItems(list []*Account, pType int) []opts.LineData {
sort.Sort(Accounts(list))
items := make([]opts.LineData, 0)
goodsName := ""
for _, account := range list {
if account.TransFrom != "" {
goodsName = account.TransFrom + ":" + account.GoodsName
} else {
goodsName = account.TransType
}
switch pType {
case 1:
if account.IO == "收入" {
items = append(items, opts.LineData{Name: goodsName, Value: account.Money})
} else {
items = append(items, opts.LineData{Name: "", Value: 0})
}
case 2:
if account.IO == "支出" {
items = append(items, opts.LineData{Name: goodsName, Value: account.Money})
} else {
items = append(items, opts.LineData{Name: "", Value: 0})
}
}
}

return items
}

func Bar(list []*Account) {
// create a new bar instance
bar := charts.NewBar()
// set some global options like Title/Legend/ToolTip or anything else
bar.SetGlobalOptions(
charts.WithInitializationOpts(opts.Initialization{
PageTitle: "账单分析",
Theme: types.ThemeMacarons}),
charts.WithLegendOpts(opts.Legend{
Show: true,
}),
charts.WithTooltipOpts(opts.Tooltip{
Show: true,
}),
charts.WithTitleOpts(opts.Title{
Title: "收支分析",
Subtitle: "账单中收支项总和",
}),
)

var xAxis []string
sort.Sort(Accounts(list))
for _, account := range list {
xAxis = append(xAxis, account.TransAt.Time.String())
}

bar.SetXAxis([]string{"累计"}).
AddSeries("收入", generateBarItems(list, 1)).
AddSeries("支出", generateBarItems(list, 2))

line := charts.NewLine()
var lineX []string
for _, account := range list {
lineX = append(lineX, account.TransAt.Format(layout))
}

line.SetGlobalOptions(charts.WithInitializationOpts(opts.Initialization{
PageTitle: "账单分析",
Theme: types.ThemeMacarons}),
charts.WithLegendOpts(opts.Legend{
Show: true,
}),
charts.WithDataZoomOpts(opts.DataZoom{
Type: "slider",
}),
charts.WithTooltipOpts(opts.Tooltip{
Show: true,
}),
charts.WithTitleOpts(opts.Title{
Title: "收支详情分析",
Subtitle: "账单中收支项详情 by 交易时间",
}),
)

line.SetXAxis(lineX).
AddSeries("收入", generateLineItems(list, 1)).
AddSeries("支出", generateLineItems(list, 2)).
SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true}))

// Where the magic happens
f, _ := os.Create("charts.html")
bar.Render(f)
line.Render(f)
}
84 changes: 66 additions & 18 deletions cmd/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package cmd
import (
"encoding/csv"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"time"

"github.com/gocarina/gocsv"
"github.com/manifoldco/promptui"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
Expand All @@ -20,6 +23,34 @@ type Bill struct {
wechat [][]string // 微信账单
}

type Account struct {
TransAt DateTime `csv:"交易时间"`
TransType string `csv:"交易类型"`
TransFrom string `csv:"交易对方"`
GoodsName string `csv:"商品"`
IO string `csv:"收/支"`
Money float64 `csv:"金额(元)"`
PayType string `csv:"支付方式"`
CurrentStatus string `csv:"当前状态"`
TransNo string `csv:"交易单号"`
BusinessNo string `csv:"商户单号"`
Remark string `csv:"备注"`
}

type Accounts []*Account

func (I Accounts) Len() int {
return len(I)
}

func (I Accounts) Less(i, j int) bool {
return I[i].TransAt.Before(I[j].TransAt.Time)
}

func (I Accounts) Swap(i, j int) {
I[i], I[j] = I[j], I[i]
}

func (b *Bill) ReadAliPay(path string) error {
f, err := os.Open(path)
if err != nil {
Expand Down Expand Up @@ -69,7 +100,7 @@ func (b *Bill) ReadAliPay(path string) error {

// 微信账单
// []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "当前状态", "交易单号", "商户单号", "备注"}
tempV := []string{temp[2], temp[6], temp[7], temp[8], temp[10], "¥" + temp[9], "支付宝", temp[15], temp[0], temp[1], temp[14]}
tempV := []string{temp[2], temp[6], temp[7], temp[8], temp[10], temp[9], "支付宝", temp[15], temp[0], temp[1], temp[14]}
// fmt.Printf("%#v\n", temp)
b.aliPay = append(b.aliPay, tempV)
}
Expand Down Expand Up @@ -108,6 +139,7 @@ func (b *Bill) ReadWechatPay(path string) error {
for _, v1 := range v {
temp = append(temp, strings.Trim(strings.Trim(v1, "/"), " "))
}
temp[5] = strings.Trim(temp[5], "¥")
// TODO: 从备注中提取出有服务费的项目,填写进入收支明细中。
b.wechat = append(b.wechat, temp)
}
Expand All @@ -122,10 +154,6 @@ func (b *Bill) WriteMergeFile(path string) error {
fmt.Println(err)
}
defer destFile.Close()
_, err = destFile.WriteString("\xEF\xBB\xBF")
if err != nil {
return err
} // 写入一个UTF-8 BOM

df := csv.NewWriter(destFile)
b.GetTitle()
Expand Down Expand Up @@ -154,33 +182,30 @@ func (b *Bill) WriteMergeFile(path string) error {
}

// ReadMergeFile 读取合并后的账单
func (b *Bill) ReadMergeFile(path string) (data [][]string, err error) {
func (b *Bill) ReadMergeFile(path string) (accounts []*Account, err error) {
f, err := os.Open(path)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
reader := csv.NewReader(f)

reader.FieldsPerRecord = -1
data, err = reader.ReadAll()
if err != nil {
fmt.Println(err)
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.LazyQuotes = true
r.FieldsPerRecord = -1
return r
})

if err = gocsv.UnmarshalFile(f, &accounts); err != nil {
return
}
return
}

// 支付宝账单
// []string{"交易号", "商家订单号", "交易创建时间", "付款时间 ", "最近修改时间", "交易来源地", "类型", "交易对方", "商品名称", "金额(元)", "收/支", "交易状态 ", "服务费(元)", "成功退款(元)", "备注", "资金状态"}

// 微信账单
// []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "当前状态", "交易单号", "商户单号", "备注"}

// GetTitle 转换 title
func (b *Bill) GetTitle() {
titles := []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "交易状态", "交易单号", "商户单号", "备注", "服务费(元)"}
titles := []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "交易状态", "交易单号", "商户单号", "备注"}
b.title = append(b.title, titles...)
}

Expand Down Expand Up @@ -245,3 +270,26 @@ func PromptSelectAnalysis() string {

return result
}

type DateTime struct {
time.Time
}

var layout = "2006-01-02 15:04:05"

// MarshalCSV Convert the internal date as CSV string
func (date *DateTime) MarshalCSV() (string, error) {
return date.Format(layout), nil
}

// String You could also use the standard Stringer interface
func (date *DateTime) String() string {
return date.String() // Redundant, just for example
}

// UnmarshalCSV Convert the CSV string as internal date
func (date *DateTime) UnmarshalCSV(csv string) (err error) {
loc, _ := time.LoadLocation("Asia/Shanghai")
date.Time, err = time.ParseInLocation(layout, csv, loc)
return err
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module github.com/yann0917/alipay-wechat-merge
go 1.16

require (
github.com/go-echarts/go-echarts/v2 v2.2.4 // indirect
github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055 // indirect
github.com/manifoldco/promptui v0.9.0
github.com/spf13/cobra v1.4.0
golang.org/x/text v0.3.7
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,34 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0=
github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A=
github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055 h1:UfcDMw41lSx3XM7UvD1i7Fsu3rMgD55OU5LYwLoR/Yk=
github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
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/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=

0 comments on commit e174739

Please sign in to comment.