diff --git a/script/bql.go b/script/bql.go index b782533..7f5979d 100644 --- a/script/bql.go +++ b/script/bql.go @@ -1,6 +1,7 @@ package script import ( + "encoding/json" "fmt" "os/exec" "reflect" @@ -13,35 +14,147 @@ type QueryParams struct { AccountType string `bql:"account ~"` } -func BQLQuery(ledgerConfig *Config, queryParams QueryParams, queryResult interface{}) error { - bql := "SELECT '\\', id, '\\', date, '\\', payee, '\\', narration, '\\', account, '\\', position, '\\', tags, '\\' WHERE" - queryParamsType := reflect.TypeOf(queryParams) - queryParamsValue := reflect.ValueOf(queryParams) - for i := 0; i < queryParamsValue.NumField(); i++ { - typeField := queryParamsType.Field(i) - valueField := queryParamsValue.Field(i) - switch valueField.Kind() { - case reflect.String: - val := valueField.String() - if val != "" { - bql = fmt.Sprintf("%s %s '%s' AND", bql, typeField.Tag.Get("bql"), val) - } - case reflect.Int: - val := valueField.Int() - if val != 0 { - bql = fmt.Sprintf("%s %s %d AND", bql, typeField.Tag.Get("bql"), val) - } - } - } - bql = strings.TrimRight(bql, " AND") - - output, err := queryByBQL(ledgerConfig, bql) +func BQLQueryOne(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error { + assertQueryResultIsPointer(queryResultPtr) + output, err := bqlRawQuery(ledgerConfig, queryParams, queryResultPtr) if err != nil { return err } + err = parseResult(output, queryResultPtr, true) + if err != nil { + return err + } + return nil +} - fmt.Println(output) - //panic("Unsupported result type") +func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error { + assertQueryResultIsPointer(queryResultPtr) + output, err := bqlRawQuery(ledgerConfig, queryParams, queryResultPtr) + if err != nil { + return err + } + err = parseResult(output, queryResultPtr, false) + if err != nil { + return err + } + return nil +} + +func bqlRawQuery(ledgerConfig *Config, queryParamsPtr *QueryParams, queryResultPtr interface{}) (string, error) { + bql := "SELECT" + queryResultPtrType := reflect.TypeOf(queryResultPtr) + queryResultType := queryResultPtrType.Elem() + + if queryResultType.Kind() == reflect.Slice { + queryResultType = queryResultType.Elem() + } + + for i := 0; i < queryResultType.NumField(); i++ { + typeField := queryResultType.Field(i) + // 字段的 tag 不带 bql 的不进行拼接 + b := typeField.Tag.Get("bql") + if b != "" { + if strings.Contains(b, "distinct") { + b = strings.ReplaceAll(b, "distinct", "") + bql = fmt.Sprintf("%s distinct '\\', %s, ", bql, b) + } else { + bql = fmt.Sprintf("%s '\\', %s, ", bql, typeField.Tag.Get("bql")) + } + } + } + // 查询条件不为空时,拼接查询条件 + if queryParamsPtr != nil { + bql += " '\\' WHERE" + queryParamsType := reflect.TypeOf(queryParamsPtr).Elem() + queryParamsValue := reflect.ValueOf(queryParamsPtr).Elem() + for i := 0; i < queryParamsType.NumField(); i++ { + typeField := queryParamsType.Field(i) + valueField := queryParamsValue.Field(i) + switch valueField.Kind() { + case reflect.String: + val := valueField.String() + if val != "" { + bql = fmt.Sprintf("%s %s '%s' AND", bql, typeField.Tag.Get("bql"), val) + } + break + case reflect.Int: + val := valueField.Int() + if val != 0 { + bql = fmt.Sprintf("%s %s %d AND", bql, typeField.Tag.Get("bql"), val) + } + break + } + } + bql = strings.TrimRight(bql, " AND") + } else { + bql += " '\\'" + } + return queryByBQL(ledgerConfig, bql) +} + +func parseResult(output string, queryResultPtr interface{}, selectOne bool) error { + queryResultPtrType := reflect.TypeOf(queryResultPtr) + queryResultType := queryResultPtrType.Elem() + + if queryResultType.Kind() == reflect.Slice { + queryResultType = queryResultType.Elem() + } + + lines := strings.Split(output, "\n")[2:] + if selectOne && len(lines) >= 3 { + lines = lines[2:3] + } + + l := make([]map[string]interface{}, 0) + for _, line := range lines { + if line != "" { + values := strings.Split(line, "\\") + // 去除 '\' 分割带来的空字符串 + values = values[1 : len(values)-1] + temp := make(map[string]interface{}) + for i, val := range values { + field := queryResultType.Field(i) + jsonName := field.Tag.Get("json") + if jsonName == "" { + jsonName = field.Name + } + switch field.Type.Kind() { + case reflect.String: + temp[jsonName] = strings.Trim(val, " ") + break + case reflect.Array, reflect.Slice: + // 去除空格 + strArray := strings.Split(val, ",") + notBlanks := make([]string, 0) + for _, s := range strArray { + if strings.Trim(s, " ") != "" { + notBlanks = append(notBlanks, s) + } + } + temp[jsonName] = notBlanks + break + default: + panic("Unsupported field type") + } + } + l = append(l, temp) + } + } + + var jsonBytes []byte + var err error + if selectOne { + jsonBytes, err = json.Marshal(l[0]) + } else { + jsonBytes, err = json.Marshal(l) + } + if err != nil { + return err + } + err = json.Unmarshal(jsonBytes, queryResultPtr) + if err != nil { + return err + } return nil } @@ -55,3 +168,10 @@ func queryByBQL(ledgerConfig *Config, bql string) (string, error) { } return string(output), nil } + +func assertQueryResultIsPointer(queryResult interface{}) { + k := reflect.TypeOf(queryResult).Kind() + if k != reflect.Ptr { + panic("QueryResult type must be pointer, it's " + k.String()) + } +} diff --git a/script/config.go b/script/config.go index 2bfc743..5b2be90 100644 --- a/script/config.go +++ b/script/config.go @@ -118,3 +118,13 @@ func WriteLedgerConfigMap(newLedgerConfigMap ConfigMap) error { LogSystemInfo("Success write ledger_config file (" + path + ")") return err } + +func GetCommoditySymbol(commodity string) string { + switch commodity { + case "CNY": + return "¥" + case "USD": + return "$" + } + return "" +} diff --git a/service/stats.go b/service/stats.go index a010098..dda4950 100644 --- a/service/stats.go +++ b/service/stats.go @@ -3,29 +3,24 @@ package service import ( "github.com/beancount-gs/script" "github.com/gin-gonic/gin" - "os/exec" - "strings" ) -func MonthsList(c *gin.Context) { - months := make([]string, 0) +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) - beanFilePath := ledgerConfig.DataPath + "/index.bean" - bql := "SELECT distinct year(date), month(date)" - cmd := exec.Command("bean-query", beanFilePath, bql) - output, err := cmd.Output() + yearMonthList := make([]YearMonth, 0) + err := script.BQLQueryList(ledgerConfig, nil, &yearMonthList) if err != nil { - InternalError(c, "Failed to exec bql") + InternalError(c, err.Error()) return } - execResult := string(output) - months = make([]string, 0) - for _, line := range strings.Split(execResult, "\n")[2:] { - if line != "" { - yearMonth := strings.Fields(line) - months = append(months, yearMonth[0]+"-"+yearMonth[1]) - } + months := make([]string, 0) + for _, yearMonth := range yearMonthList { + months = append(months, yearMonth.Year+"-"+yearMonth.Month) } OK(c, months) } diff --git a/service/transactions.go b/service/transactions.go index abb0324..08db6d6 100644 --- a/service/transactions.go +++ b/service/transactions.go @@ -4,16 +4,20 @@ import ( "github.com/beancount-gs/script" "github.com/gin-gonic/gin" "strconv" + "strings" ) type Transactions struct { - Id string `bql:"id"` - Date string `bql:"date"` - payee string - narration string - account string - position string - tags string + Id string `bql:"id" json:"id"` + Date string `bql:"date" json:"date"` + Payee string `bql:"payee" json:"payee"` + Narration string `bql:"narration" json:"desc"` + Account string `bql:"account" json:"account"` + Tags []string `bql:"tags" json:"tags"` + Position string `bql:"position" json:"position"` + Amount string `json:"amount"` + Commodity string `json:"commodity"` + CommoditySymbol string `json:"commoditySymbol"` } func getQueryModel(c *gin.Context) script.QueryParams { @@ -40,10 +44,20 @@ func QueryTransactions(c *gin.Context) { ledgerConfig := script.GetLedgerConfigFromContext(c) queryParams := getQueryModel(c) transactions := make([]Transactions, 0) - err := script.BQLQuery(ledgerConfig, queryParams, transactions) + err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) if err != nil { InternalError(c, err.Error()) return } + // 格式化金额 + for i := 0; i < len(transactions); i++ { + pos := strings.Split(transactions[i].Position, " ") + if len(pos) == 2 { + transactions[i].Amount = pos[0] + transactions[i].Commodity = pos[1] + transactions[i].CommoditySymbol = script.GetCommoditySymbol(pos[1]) + } + transactions[i].Position = "" + } OK(c, transactions) }