├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── echart.go ├── merge.go ├── parse.go ├── root.go └── version.go ├── go.mod ├── go.sum └── main.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | .DS_Store 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | .idea 20 | Release/ 21 | *.csv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 yann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | .PHONY: build test clean run build-race build-linux build-osx build-windows test-race enable-race 3 | 4 | all: clean setup build-linux build-osx build-windows 5 | 6 | BUILD_ENV=CGO_ENABLED=0 7 | BUILD=`date +%FT%T%z` 8 | LDFLAGS=-ldflags "-w -s -X main.Version=${VERSION} -X main.Build=${BUILD}" 9 | 10 | GOCMD=go 11 | GOBUILD=$(GOCMD) build 12 | GOCLEAN=$(GOCMD) clean 13 | GOTEST=$(GOCMD) test 14 | GOGET=$(GOCMD) get 15 | TARGET_EXEC=awm 16 | 17 | setup: 18 | mkdir -p Release 19 | 20 | build-linux: setup 21 | $(BUILD_ENV) GOARCH=amd64 GOOS=linux $(GOBUILD) $(LDFLAGS) -o Release/$(TARGET_EXEC)-linux-amd64 22 | 23 | build-osx: setup 24 | $(BUILD_ENV) GOARCH=amd64 GOOS=darwin $(GOBUILD) $(LDFLAGS) -o Release/$(TARGET_EXEC)-darwin-amd64 25 | 26 | build-windows: setup 27 | $(BUILD_ENV) GOARCH=amd64 GOOS=windows $(GOBUILD) $(LDFLAGS) -o Release/$(TARGET_EXEC)-windows-amd64.exe 28 | 29 | default: all 30 | 31 | build: 32 | $(BUILD_ENV) $(GOBUILD) $(RACE) $(LDFLAGS) -o $(TARGET_EXEC) -v . 33 | 34 | test: 35 | $(GOTEST) $(RACE) -v ./test 36 | 37 | enable-race: 38 | $(eval RACE = -race) 39 | 40 | build-race: enable-race build 41 | test-race: enable-race test 42 | 43 | run: 44 | $(GOBUILD) $(RACE) -o $(TARGET_EXEC) -v . 45 | ./$(TARGET_EXEC) 46 | 47 | clean: 48 | $(GOCLEAN) 49 | rm -rf Release 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alipay-wechat-merge 2 | 3 | 合并从支付宝、微信下载的 .csv 格式账单。 4 | 5 | ## 说明 6 | 7 | 本程序适应以下格式的账单文件: 8 | 9 | |账单|数据列|汇总行|编码格式| 10 | |----|----|----|----| 11 | |支付宝|16项| 前4行|gbk| 12 | |微信|11项| 前16行|utf-8| 13 | 14 | ```txt 15 | 1. 支付宝账单包含16项: 16 | // []string{"交易号", "商家订单号", "交易创建时间", "付款时间 ", "最近修改时间", "交易来源地", "类型", "交易对方", "商品名称 ", "金额(元)", "收/支", "交易状态 ", "服务费(元)", "成功退款(元)", "备注", "资金状态"} 17 | 18 | 2. 微信账单包含11项: 19 | // []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "当前状态", "交易单号", "商户单号", "备注"} 20 | 21 | ``` 22 | 23 | ## 使用方式 24 | 25 | ```mermaid 26 | flowchart LR 27 | 下载支付宝账单-->处理 28 | 下载微信账单-->处理 29 | 处理-->合并后的账单 30 | ``` 31 | 32 | ### Step 1 下载账单 33 | 34 | #### 下载微信账单 35 | 36 | 1. 进入手机版微信,选择 “我”,进入用户中心界面,点击 “服务” 选项; 37 | 2. 点击 “钱包”,进入钱包界面后,点击右上角的 “账单” 按钮; 38 | 3. 点击右上角“常见问题” -> 点击“下载账单”->“用于个人对账”; 39 | 4. 自定义账单时间 -> 点击 “下一步”; 40 | 5. 填写邮箱(微信会把账单发送到你填写的邮箱),点击 “下一步”; 41 | 6. 输入支付密码,提示申请已提交,微信支付官方会发送一条『账单文件发送成功通知』,里面有账单的解压码; 42 | 7. 前往第五步填写的邮箱下载得到压缩包,用解压码解压得到 .csv 格式微信账单,导出成功。 43 | 44 | #### 下载支付宝账单 45 | 46 | 1. 电脑浏览器中打开 [支付宝官网](https://www.alipay.com) 扫码登录; 47 | 2. 点击右上角“服务大厅”->“自助服务”; 48 | 3. 在“交易服务”中点击“交易记录”一项; 49 | 4. 选择交易时间,并选择下载 excel 格式,得到 .zip 压缩包; 50 | 5. 解压压缩包得到 .csv 格式的支付宝账单,导出成功。 51 | 52 | ### Step 2 运行合并账单程序 53 | 54 | 1. 将下载好的账单放到程序所在的文件夹 55 | 2. 执行 `awm merge` 56 | 3. 分别选择支付宝账单和微信账单 57 | 4. 按 enter 键执行 58 | 5. 输出合并后的账单为 `output_xxxxxxxxxx.csv` 59 | 60 | 注意:删除了微信账单中的『中性交易』数据:充值/提现/理财通购买/零钱通存取/信用卡还款等交易,将计入中性交易 61 | 62 | ### Step 3 分析账单数据 63 | 64 | 1. 执行 `awm chart` 65 | 2. 选择合并后的账单 `output_xxxxxxxxxx.csv` 66 | 3. 按 enter 键执行 67 | 4. 输出 `charts.html` 为分析好的账单,使用浏览器打开即可查看 68 | 69 | ## LICENSE 70 | 71 | [MIT](./LICENSE) 72 | 73 | --- 74 | -------------------------------------------------------------------------------- /cmd/echart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/go-echarts/go-echarts/v2/charts" 9 | "github.com/go-echarts/go-echarts/v2/opts" 10 | "github.com/go-echarts/go-echarts/v2/types" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var chartCmd = &cobra.Command{ 15 | Use: "chart", 16 | Short: "图表分析账单", 17 | Long: `使用 awm chart 分析合并后的账单`, 18 | Args: cobra.NoArgs, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | oPath = PromptSelectAnalysis() 21 | var b Bill 22 | if strings.Contains(oPath, ".csv") { 23 | list, err := b.ReadMergeFile(oPath) 24 | if err != nil { 25 | return err 26 | } 27 | Bar(list) 28 | } 29 | 30 | return nil 31 | }, 32 | } 33 | 34 | func init() { 35 | rootCmd.AddCommand(chartCmd) 36 | } 37 | 38 | // generate random data for bar chart 39 | func generateBarItems(list []*Account, pType int) []opts.BarData { 40 | items := make([]opts.BarData, 0) 41 | iTotal, oTotal := 0.0, 0.0 42 | for _, account := range list { 43 | if account.IO == "收入" { 44 | iTotal += account.Money * 100 45 | } 46 | if account.IO == "支出" { 47 | oTotal += account.Money * 100 48 | } 49 | } 50 | if pType == 1 { 51 | items = append(items, opts.BarData{Name: "累计收入", Value: iTotal / 100}) 52 | } 53 | if pType == 2 { 54 | items = append(items, opts.BarData{Name: "累计支出", Value: oTotal / 100}) 55 | } 56 | return items 57 | } 58 | 59 | func generateLineItems(list []*Account, pType int) []opts.LineData { 60 | sort.Sort(Accounts(list)) 61 | items := make([]opts.LineData, 0) 62 | goodsName := "" 63 | for _, account := range list { 64 | if account.TransFrom != "" { 65 | goodsName = account.TransFrom + ":" + account.GoodsName 66 | } else { 67 | goodsName = account.TransType 68 | } 69 | switch pType { 70 | case 1: 71 | if account.IO == "收入" { 72 | items = append(items, opts.LineData{Name: goodsName, Value: account.Money}) 73 | } else { 74 | items = append(items, opts.LineData{Name: "", Value: 0}) 75 | } 76 | case 2: 77 | if account.IO == "支出" { 78 | items = append(items, opts.LineData{Name: goodsName, Value: account.Money}) 79 | } else { 80 | items = append(items, opts.LineData{Name: "", Value: 0}) 81 | } 82 | } 83 | } 84 | 85 | return items 86 | } 87 | 88 | func Bar(list []*Account) { 89 | // create a new bar instance 90 | bar := charts.NewBar() 91 | // set some global options like Title/Legend/ToolTip or anything else 92 | bar.SetGlobalOptions( 93 | charts.WithInitializationOpts(opts.Initialization{ 94 | PageTitle: "账单分析", 95 | Theme: types.ThemeMacarons}), 96 | charts.WithLegendOpts(opts.Legend{ 97 | Show: true, 98 | }), 99 | charts.WithTooltipOpts(opts.Tooltip{ 100 | Show: true, 101 | }), 102 | charts.WithTitleOpts(opts.Title{ 103 | Title: "收支分析", 104 | Subtitle: "账单中收支项总和", 105 | }), 106 | ) 107 | 108 | var xAxis []string 109 | sort.Sort(Accounts(list)) 110 | for _, account := range list { 111 | xAxis = append(xAxis, account.TransAt.Time.String()) 112 | } 113 | 114 | bar.SetXAxis([]string{"累计"}). 115 | AddSeries("收入", generateBarItems(list, 1)). 116 | AddSeries("支出", generateBarItems(list, 2)) 117 | 118 | line := charts.NewLine() 119 | var lineX []string 120 | for _, account := range list { 121 | lineX = append(lineX, account.TransAt.Format(layout)) 122 | } 123 | 124 | line.SetGlobalOptions(charts.WithInitializationOpts(opts.Initialization{ 125 | PageTitle: "账单分析", 126 | Theme: types.ThemeMacarons}), 127 | charts.WithLegendOpts(opts.Legend{ 128 | Show: true, 129 | }), 130 | charts.WithDataZoomOpts(opts.DataZoom{ 131 | Type: "slider", 132 | }), 133 | charts.WithTooltipOpts(opts.Tooltip{ 134 | Show: true, 135 | }), 136 | charts.WithTitleOpts(opts.Title{ 137 | Title: "收支详情分析", 138 | Subtitle: "账单中收支项详情 by 交易时间", 139 | }), 140 | ) 141 | 142 | line.SetXAxis(lineX). 143 | AddSeries("收入", generateLineItems(list, 1)). 144 | AddSeries("支出", generateLineItems(list, 2)). 145 | SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true})) 146 | 147 | // Where the magic happens 148 | f, _ := os.Create("charts.html") 149 | bar.Render(f) 150 | line.Render(f) 151 | } 152 | -------------------------------------------------------------------------------- /cmd/merge.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | aPath string 13 | wPath string 14 | oPath string 15 | ) 16 | 17 | var mergeCmd = &cobra.Command{ 18 | Use: "merge", 19 | Short: "合并账单", 20 | Long: `使用 awm merge 合并支付宝 微信账单`, 21 | Args: cobra.OnlyValidArgs, 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | var b Bill 24 | aPath = PromptSelectCSVPath("1") 25 | if strings.Contains(aPath, ".csv") { 26 | if err := b.ReadAliPay(aPath); err != nil { 27 | return err 28 | } 29 | } 30 | wPath = PromptSelectCSVPath("2") 31 | if strings.Contains(wPath, ".csv") { 32 | if err := b.ReadWechatPay(wPath); err != nil { 33 | return err 34 | } 35 | } 36 | unix := strconv.FormatInt(time.Now().Unix(), 10) 37 | if err := b.WriteMergeFile("output_" + unix + ".csv"); err != nil { 38 | return err 39 | } 40 | return nil 41 | }, 42 | } 43 | 44 | func init() { 45 | rootCmd.AddCommand(mergeCmd) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/parse.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gocarina/gocsv" 15 | "github.com/manifoldco/promptui" 16 | "golang.org/x/text/encoding/simplifiedchinese" 17 | "golang.org/x/text/transform" 18 | ) 19 | 20 | type Bill struct { 21 | title []string // 标题 22 | aliPay [][]string // 支付宝账单 23 | wechat [][]string // 微信账单 24 | } 25 | 26 | type Account struct { 27 | TransAt DateTime `csv:"交易时间"` 28 | TransType string `csv:"交易类型"` 29 | TransFrom string `csv:"交易对方"` 30 | GoodsName string `csv:"商品"` 31 | IO string `csv:"收/支"` 32 | Money float64 `csv:"金额(元)"` 33 | PayType string `csv:"支付方式"` 34 | CurrentStatus string `csv:"当前状态"` 35 | TransNo string `csv:"交易单号"` 36 | BusinessNo string `csv:"商户单号"` 37 | Remark string `csv:"备注"` 38 | } 39 | 40 | type Accounts []*Account 41 | 42 | func (I Accounts) Len() int { 43 | return len(I) 44 | } 45 | 46 | func (I Accounts) Less(i, j int) bool { 47 | return I[i].TransAt.Before(I[j].TransAt.Time) 48 | } 49 | 50 | func (I Accounts) Swap(i, j int) { 51 | I[i], I[j] = I[j], I[i] 52 | } 53 | 54 | func (b *Bill) ReadAliPay(path string) error { 55 | f, err := os.Open(path) 56 | if err != nil { 57 | fmt.Println(err) 58 | return err 59 | } 60 | defer f.Close() 61 | 62 | // 支付宝账单是 GBK 格式的,需要转码 63 | gbkFile := transform.NewReader(f, simplifiedchinese.GBK.NewDecoder()) 64 | reader := csv.NewReader(gbkFile) 65 | reader.FieldsPerRecord = -1 66 | 67 | data, err := reader.ReadAll() 68 | if err != nil { 69 | fmt.Println(err) 70 | return err 71 | } 72 | title := data[4] 73 | columnLen := len(title) 74 | for _, v := range data { 75 | if len(v) == columnLen && !reflect.DeepEqual(v, title) { 76 | var temp []string 77 | // 对于资金状态为空的情况,交易关闭,不计入结算。 78 | if v[15] == "" { 79 | continue 80 | } 81 | // 对于资金状态为“资金转移”的,如果服务费为零,不计入结算。 82 | if v[15] == "资金转移" && v[12] == "0.00" { 83 | continue 84 | } 85 | // 移除单元格的空格 86 | for _, v1 := range v { 87 | temp = append(temp, strings.Trim(v1, " ")) 88 | } 89 | // 将服务费算作支出,填写到金额中。 90 | fee, _ := strconv.ParseFloat(v[12], 64) 91 | if fee > 0.00 { 92 | tempV := v 93 | tempV[9] = v[12] 94 | tempV[10] = "支出" 95 | data = append(data, tempV) // FIXME: 循环的 data 是复制出来的副本 96 | } 97 | 98 | // fmt.Println(temp[9]) 99 | // 支付宝账单[]string{"交易号", "商家订单号", "交易创建时间", "付款时间 ", "最近修改时间", "交易来源地", "类型", "交易对方", "商品名称", "金额(元)", "收/支", "交易状态 ", "服务费(元)", "成功退款(元)", "备注", "资金状态"} 100 | 101 | // 微信账单 102 | // []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "当前状态", "交易单号", "商户单号", "备注"} 103 | tempV := []string{temp[2], temp[6], temp[7], temp[8], temp[10], temp[9], "支付宝", temp[15], temp[0], temp[1], temp[14]} 104 | // fmt.Printf("%#v\n", temp) 105 | b.aliPay = append(b.aliPay, tempV) 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func (b *Bill) ReadWechatPay(path string) error { 112 | f, err := os.Open(path) 113 | if err != nil { 114 | fmt.Println(err) 115 | return err 116 | } 117 | defer f.Close() 118 | 119 | reader := csv.NewReader(f) 120 | reader.FieldsPerRecord = -1 121 | 122 | data, err := reader.ReadAll() 123 | if err != nil { 124 | fmt.Println(err) 125 | return err 126 | } 127 | title := data[16] 128 | // fmt.Printf("%#v\n", title) 129 | columnLen := len(title) 130 | for _, v := range data { 131 | if len(v) == columnLen && !reflect.DeepEqual(v, title) { 132 | // []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "当前状态", "交易单号", "商户单号", "备注"} 133 | 134 | // 删除收支为空且备注为空或备注为服务费¥0.00的,中性交易。 135 | if (v[4] == "/" && v[10] == "/") || (v[4] == "/" && v[10] == "服务费¥0.00") { 136 | continue 137 | } 138 | var temp []string 139 | for _, v1 := range v { 140 | temp = append(temp, strings.Trim(strings.Trim(v1, "/"), " ")) 141 | } 142 | temp[5] = strings.Trim(temp[5], "¥") 143 | // TODO: 从备注中提取出有服务费的项目,填写进入收支明细中。 144 | b.wechat = append(b.wechat, temp) 145 | } 146 | } 147 | return nil 148 | } 149 | 150 | // WriteMergeFile 合并账单 151 | func (b *Bill) WriteMergeFile(path string) error { 152 | destFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 153 | if err != nil { 154 | fmt.Println(err) 155 | } 156 | defer destFile.Close() 157 | 158 | df := csv.NewWriter(destFile) 159 | b.GetTitle() 160 | err = df.Write(b.title) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | for _, row := range b.aliPay { 166 | err = df.Write(row) 167 | if err != nil { 168 | fmt.Println(err) 169 | return err 170 | } 171 | } 172 | for _, row := range b.wechat { 173 | err = df.Write(row) 174 | if err != nil { 175 | fmt.Println(err) 176 | return err 177 | } 178 | } 179 | 180 | df.Flush() 181 | return nil 182 | } 183 | 184 | // ReadMergeFile 读取合并后的账单 185 | func (b *Bill) ReadMergeFile(path string) (accounts []*Account, err error) { 186 | f, err := os.Open(path) 187 | if err != nil { 188 | fmt.Println(err) 189 | return 190 | } 191 | defer f.Close() 192 | 193 | gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader { 194 | r := csv.NewReader(in) 195 | r.LazyQuotes = true 196 | r.FieldsPerRecord = -1 197 | return r 198 | }) 199 | 200 | if err = gocsv.UnmarshalFile(f, &accounts); err != nil { 201 | return 202 | } 203 | return 204 | } 205 | 206 | // GetTitle 转换 title 207 | func (b *Bill) GetTitle() { 208 | titles := []string{"交易时间", "交易类型", "交易对方", "商品", "收/支", "金额(元)", "支付方式", "交易状态", "交易单号", "商户单号", "备注"} 209 | b.title = append(b.title, titles...) 210 | } 211 | 212 | // GetCSVPath 查找当前目录下的账单文件 213 | func GetCSVPath() (items []string) { 214 | pwd, _ := os.Getwd() 215 | fileInfoList, _ := ioutil.ReadDir(pwd) 216 | for _, info := range fileInfoList { 217 | if !info.IsDir() && strings.Contains(info.Name(), ".csv") { 218 | items = append(items, info.Name()) 219 | } 220 | } 221 | return 222 | } 223 | 224 | func PromptSelectCSVPath(bType string) string { 225 | bMap := map[string]string{ 226 | "1": "支付宝", 227 | "2": "微信", 228 | } 229 | items := GetCSVPath() 230 | index := -1 231 | var result string 232 | var err error 233 | 234 | for index < 0 { 235 | prompt := promptui.SelectWithAdd{ 236 | Label: "请选择【" + bMap[bType] + "】账单", 237 | Items: items, 238 | } 239 | 240 | index, result, err = prompt.Run() 241 | 242 | if index == -1 { 243 | items = append(items, result) 244 | } 245 | } 246 | 247 | if err != nil { 248 | fmt.Printf("Prompt failed %v\n", err) 249 | os.Exit(1) 250 | } 251 | 252 | fmt.Printf("Input: %s\n", result) 253 | 254 | return result 255 | } 256 | 257 | func PromptSelectAnalysis() string { 258 | items := GetCSVPath() 259 | prompt := promptui.Select{ 260 | Label: "请选择【合并后的】账单", 261 | Items: items, 262 | } 263 | _, result, err := prompt.Run() 264 | if err != nil { 265 | fmt.Printf("Prompt failed %v\n", err) 266 | os.Exit(1) 267 | } 268 | 269 | fmt.Printf("You choose: %s\n", result) 270 | 271 | return result 272 | } 273 | 274 | type DateTime struct { 275 | time.Time 276 | } 277 | 278 | var layout = "2006-01-02 15:04:05" 279 | 280 | // MarshalCSV Convert the internal date as CSV string 281 | func (date *DateTime) MarshalCSV() (string, error) { 282 | return date.Format(layout), nil 283 | } 284 | 285 | // String You could also use the standard Stringer interface 286 | func (date *DateTime) String() string { 287 | return date.String() // Redundant, just for example 288 | } 289 | 290 | // UnmarshalCSV Convert the CSV string as internal date 291 | func (date *DateTime) UnmarshalCSV(csv string) (err error) { 292 | loc, _ := time.LoadLocation("Asia/Shanghai") 293 | date.Time, err = time.ParseInLocation(layout, csv, loc) 294 | return err 295 | } 296 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "awm", 12 | Short: "awm is a very fast tools combine aliPay csv bills and wechat csv bills", 13 | Long: `awm 是一款将支付宝和微信账单合并的小工具`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | // Do Stuff Here 16 | }, 17 | } 18 | 19 | func Execute() { 20 | if err := rootCmd.Execute(); err != nil { 21 | fmt.Fprintln(os.Stderr, err) 22 | os.Exit(1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var versionCmd = &cobra.Command{ 10 | Use: "version", 11 | Short: "打印版本号", 12 | Long: `打印 awm 版本号`, 13 | Run: func(cmd *cobra.Command, args []string) { 14 | fmt.Println("awm v0.0.1") 15 | }, 16 | } 17 | 18 | func init() { 19 | rootCmd.AddCommand(versionCmd) 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yann0917/alipay-wechat-merge 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-echarts/go-echarts/v2 v2.2.4 7 | github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055 8 | github.com/manifoldco/promptui v0.9.0 9 | github.com/spf13/cobra v1.4.0 10 | golang.org/x/text v0.3.8 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 2 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0= 12 | github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A= 13 | github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055 h1:UfcDMw41lSx3XM7UvD1i7Fsu3rMgD55OU5LYwLoR/Yk= 14 | github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= 15 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 16 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 17 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 21 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 26 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 27 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 28 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= 31 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 35 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 36 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 37 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 38 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 39 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 47 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 49 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 50 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 51 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 52 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 53 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 54 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 55 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 56 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 57 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 58 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/yann0917/alipay-wechat-merge/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | --------------------------------------------------------------------------------