diff --git a/service/transactions.go b/service/transactions.go index ddaddcf..358c1dd 100644 --- a/service/transactions.go +++ b/service/transactions.go @@ -6,17 +6,32 @@ import ( "encoding/json" "errors" "fmt" - "github.com/beancount-gs/script" - "github.com/gin-gonic/gin" - "github.com/shopspring/decimal" "io" + "net/url" "strconv" "strings" "time" + + "github.com/beancount-gs/script" + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" ) +// DateRange 查询账本中交易记录的时间范围 +type DateRange struct { + MinDate string `bql:"min(date)" json:"minDate"` + MaxDate string `bql:"max(date)" json:"maxDate"` +} + +// YearMonthOption 年月选项 +type YearMonthOption struct { + Year int `json:"year"` + Month int `json:"month"` // 0 表示全年 + Count int `json:"count"` // 该月份的交易数量 +} + type Transaction struct { - Id string `bql:"id" json:"id"` + Id string `bql:"distinct id" json:"id"` Account string `bql:"account" json:"account"` Date string `bql:"date" json:"date"` Payee string `bql:"payee" json:"payee"` @@ -25,7 +40,7 @@ type Transaction struct { Balance string `bql:"balance" json:"balance"` Currency string `bql:"currency" json:"currency"` CostDate string `bql:"cost_date" json:"costDate"` - CostPrice string `bql:"cost_number" json:"costPrice"` // 交易净值 + CostPrice string `bql:"cost_number" json:"costPrice"` CostCurrency string `bql:"cost_currency" json:"costCurrency"` Price string `bql:"price" json:"price"` Tags []string `bql:"tags" json:"tags"` @@ -34,119 +49,6 @@ type Transaction struct { IsAnotherCurrency bool `json:"isAnotherCurrency,omitempty"` } -type RawTransaction struct { - RawText string `json:"text"` - StartLineNo int `json:"startLineNo"` - EndLineNo int `json:"endLineNo"` - FilePath string `json:"filePath,omitempty"` -} - -type TransactionSort []Transaction - -func (s TransactionSort) Len() int { - return len(s) -} -func (s TransactionSort) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} -func (s TransactionSort) Less(i, j int) bool { - a, _ := strconv.Atoi(s[i].Number) - b, _ := strconv.Atoi(s[j].Number) - return a <= b -} - -func QueryTransactionDetailById(c *gin.Context) { - queryParams := script.GetQueryParams(c) - if queryParams.ID == "" { - BadRequest(c, "Param 'id' must not be blank.") - return - } - ledgerConfig := script.GetLedgerConfigFromContext(c) - transactions := make([]Transaction, 0) - err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) - if err != nil { - BadRequest(c, err.Error()) - return - } - if len(transactions) == 0 { - BadRequest(c, "No transaction found.") - } - - transactionForm := TransactionForm{} - transactionForm.Entries = make([]TransactionEntryForm, 0) - for _, transaction := range transactions { - if transactionForm.ID == "" { - transactionForm.ID = transaction.Id - transactionForm.Date = transaction.Date - transactionForm.Payee = transaction.Payee - transactionForm.Desc = transaction.Narration - transactionForm.Narration = transaction.Narration - } - transactionEntryForm := TransactionEntryForm{ - Account: transaction.Account, - } - if transaction.Number != "" && transaction.Number != "0" { - transactionEntryForm.Number = decimal.RequireFromString(transaction.Number) - transactionEntryForm.Currency = transaction.Currency - transactionEntryForm.IsAnotherCurrency = transaction.IsAnotherCurrency - } - if transaction.CostPrice != "" && transaction.CostPrice != "0" { - transactionEntryForm.Price = decimal.RequireFromString(transaction.CostPrice) - transactionEntryForm.PriceCurrency = transaction.CostCurrency - } - transactionForm.Entries = append(transactionForm.Entries, transactionEntryForm) - } - OK(c, transactionForm) -} - -func QueryTransactionRawTextById(c *gin.Context) { - queryParams := script.GetQueryParams(c) - if queryParams.ID == "" { - BadRequest(c, "Param 'id' must not be blank.") - } - ledgerConfig := script.GetLedgerConfigFromContext(c) - result, err := script.BQLPrint(ledgerConfig, queryParams.ID) - if err != nil { - InternalError(c, err.Error()) - return - } - OK(c, result) -} - -func QueryTransactions(c *gin.Context) { - ledgerConfig := script.GetLedgerConfigFromContext(c) - queryParams := script.GetQueryParams(c) - // 倒序查询 - queryParams.OrderBy = "date desc" - transactions := make([]Transaction, 0) - err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) - if err != nil { - InternalError(c, err.Error()) - return - } - - currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id) - - // 格式化金额 - for i := 0; i < len(transactions); i++ { - _, ok := currencyMap[transactions[i].Currency] - if ok { - transactions[i].IsAnotherCurrency = transactions[i].Currency != ledgerConfig.OperatingCurrency - } - - symbol := script.GetCommoditySymbol(ledgerConfig.Id, transactions[i].Currency) - transactions[i].CurrencySymbol = symbol - transactions[i].CostCurrencySymbol = symbol - if transactions[i].Price != "" { - transactions[i].Price = strings.Fields(transactions[i].Price)[0] - } - if transactions[i].Balance != "" { - transactions[i].Balance = strings.Fields(transactions[i].Balance)[0] - } - } - OK(c, transactions) -} - type TransactionForm struct { ID string `form:"id" json:"id"` Date string `form:"date" binding:"required" json:"date"` @@ -159,11 +61,6 @@ type TransactionForm struct { RawText string `json:"rawText,omitempty"` } -type UpdateRawTextTransactionForm struct { - ID string `form:"id" binding:"required" json:"id"` - RawText string `form:"rawText" json:"rawText,omitempty" binding:"required"` -} - type TransactionEntryForm struct { Account string `form:"account" binding:"required" json:"account"` Number decimal.Decimal `form:"number" json:"number,omitempty"` @@ -173,6 +70,131 @@ type TransactionEntryForm struct { IsAnotherCurrency bool `form:"isAnotherCurrency" json:"isAnotherCurrency,omitempty"` } +type UpdateRawTextTransactionForm struct { + ID string `form:"id" binding:"required" json:"id"` + RawText string `form:"rawText" json:"rawText,omitempty" binding:"required"` +} + +type TransactionQuery struct { + Year int `form:"year"` + Month int `form:"month"` + Account string `form:"account"` + Tag string `form:"tag"` + Type string `form:"type"` + Limit int `form:"limit"` + Offset int `form:"offset"` +} + +type TransactionTemplate struct { + Id string `json:"id"` + Date string `form:"date" binding:"required" json:"date"` + TemplateName string `form:"templateName" binding:"required" json:"templateName"` + Payee string `form:"payee" json:"payee"` + Desc string `form:"desc" binding:"required" json:"desc"` + Entries []TransactionEntryForm `form:"entries" json:"entries"` +} + +// ==================== 工具函数 ==================== + +// getTransactionDateRange 获取交易记录的时间范围 +func getTransactionDateRange(ledgerConfig *script.Config) (*DateRange, error) { + var dateRange []DateRange + queryParams := script.QueryParams{ + Where: false, + } + + err := script.BQLQueryList(ledgerConfig, &queryParams, &dateRange) + if err != nil { + return nil, err + } + + if len(dateRange) == 0 { + currentDate := time.Now().Format("2006-01-02") + return &DateRange{ + MinDate: currentDate, + MaxDate: currentDate, + }, nil + } + + return &dateRange[0], nil +} + +func buildQuery(params map[string]string) string { + var buf strings.Builder + for k, v := range params { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(url.QueryEscape(k)) + buf.WriteByte('=') + buf.WriteString(url.QueryEscape(v)) + } + return buf.String() +} + +func truncateString(s string, length int) string { + if len(s) <= length { + return s + } + return s[:length] + "..." +} + +func safeString(s string, defaultValue string) string { + if s == "" { + return defaultValue + } + return s +} + +func safeTags(tags []string) string { + if len(tags) == 0 { + return "[]" + } + return fmt.Sprintf("%v", tags) +} + +func logTransactionMultiline(mail string, index int, tx *Transaction) { + script.LogDebugDetailed(mail, "TransactionDetails", ` +交易[%d]详细信息: +├── ID: %s +├── 账户: %s +├── 日期: %s +├── 收款方: %s +├── 描述: %s +├── 金额: %s +├── 货币: %s +├── 余额: %s +├── 成本日期: %s +├── 成本价格: %s +├── 成本货币: %s +├── 价格: %s +└── 标签: %v`, + index, + safeString(tx.Id, "N/A"), + safeString(tx.Account, "N/A"), + safeString(tx.Date, "N/A"), + safeString(tx.Payee, "N/A"), + truncateString(tx.Narration, 40), + safeString(tx.Number, "N/A"), + safeString(tx.Currency, "N/A"), + safeString(tx.Balance, "N/A"), + safeString(tx.CostDate, "N/A"), + safeString(tx.CostPrice, "N/A"), + safeString(tx.CostCurrency, "N/A"), + safeString(tx.Price, "N/A"), + safeTags(tx.Tags)) +} + +func filterEmptyStrings(arr []string) []string { + var result []string + for _, str := range arr { + if script.CleanString(str) != "" { + result = append(result, str) + } + } + return result +} + func sum(entries []TransactionEntryForm, openingBalances string) decimal.Decimal { sumVal := decimal.NewFromInt(0) for _, entry := range entries { @@ -189,40 +211,239 @@ func sum(entries []TransactionEntryForm, openingBalances string) decimal.Decimal return sumVal } +// ==================== 交易查询相关 ==================== + +func QueryTransactionDetailById(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + queryParams := script.GetQueryParams(c) + + if queryParams.ID == "" { + BadRequest(c, "参数 'id' 不能为空") + return + } + + transactions := make([]Transaction, 0) + err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions) + if err != nil { + BadRequest(c, err.Error()) + return + } + + if len(transactions) == 0 { + BadRequest(c, "未找到交易记录") + return + } + + transactionForm := TransactionForm{ + Entries: make([]TransactionEntryForm, 0), + } + + for _, transaction := range transactions { + if transactionForm.ID == "" { + transactionForm.ID = transaction.Id + transactionForm.Date = transaction.Date + transactionForm.Payee = transaction.Payee + transactionForm.Desc = transaction.Narration + transactionForm.Narration = transaction.Narration + } + + entry := TransactionEntryForm{Account: transaction.Account} + + if transaction.Number != "" && transaction.Number != "0" { + entry.Number = decimal.RequireFromString(transaction.Number) + entry.Currency = transaction.Currency + entry.IsAnotherCurrency = transaction.IsAnotherCurrency + } + + if transaction.CostPrice != "" && transaction.CostPrice != "0" { + entry.Price = decimal.RequireFromString(transaction.CostPrice) + entry.PriceCurrency = transaction.CostCurrency + } + + transactionForm.Entries = append(transactionForm.Entries, entry) + } + + OK(c, transactionForm) +} + +func QueryTransactionRawTextById(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + queryParams := script.GetQueryParams(c) + + if queryParams.ID == "" { + BadRequest(c, "参数 'id' 不能为空") + return + } + + result, err := script.BQLPrint(ledgerConfig, queryParams.ID) + if err != nil { + InternalError(c, err.Error()) + return + } + + OK(c, result) +} + +func QueryTransactions(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + + // 参数预处理 + queryValues := c.Request.URL.Query() + params := make(map[string]string) + for k, v := range queryValues { + if len(v) > 0 { + params[k] = v[0] + } + } + + // 处理undefined参数 + now := time.Now() + if params["year"] == "undefined" { + params["year"] = strconv.Itoa(now.Year()) + } + if params["month"] == "undefined" { + params["month"] = strconv.Itoa(int(now.Month())) + } + + c.Request.URL.RawQuery = buildQuery(params) + + // 绑定查询参数 + var transactionQuery TransactionQuery + if err := c.ShouldBindQuery(&transactionQuery); err != nil { + BadRequest(c, "无效的查询参数") + return + } + + // 获取账本时间范围 + dateRange, err := getTransactionDateRange(ledgerConfig) + if err != nil { + InternalError(c, "获取账本时间范围失败") + return + } + + // 解析最小日期作为默认起始点 + minDate, err := time.Parse("2006-01-02", dateRange.MinDate) + if err != nil { + InternalError(c, "解析账本最小日期失败") + return + } + + // 设置默认年月 + if transactionQuery.Year <= 0 { + transactionQuery.Year = minDate.Year() + } + if transactionQuery.Month <= 0 || transactionQuery.Month > 12 { + transactionQuery.Month = int(minDate.Month()) + } + + // 构建查询参数 + queryParams := script.QueryParams{ + FromYear: minDate.Year(), + FromMonth: int(minDate.Month()), + Year: transactionQuery.Year, + Month: transactionQuery.Month, + Account: transactionQuery.Account, + Tag: transactionQuery.Tag, + Where: true, + OrderBy: "date desc", + Limit: transactionQuery.Limit, + From: true, // 启用起始日期查询 + DateRange: true, // 启用日期范围查询 + } + + // 设置日期范围 + if transactionQuery.Year <= 0 { + queryParams.Year = now.Year() + queryParams.Month = int(now.Month()) + } + + // 设置科目匹配模式 + if transactionQuery.Account != "" { + queryParams.StrictAccountMatch = true + } + + // 记录完整的查询参数 + script.LogDebugDetailed(ledgerConfig.Mail, "QueryTransactions-FinalParams", + "完整查询参数: %+v (账本时间范围: %s 至 %s)", + queryParams, dateRange.MinDate, dateRange.MaxDate) + + // 设置合理的limit + if queryParams.Limit <= 0 || queryParams.Limit > 1000 { + queryParams.Limit = 100 + } + + // 执行查询 + transactions := make([]Transaction, 0) + err = script.BQLQueryList(ledgerConfig, &queryParams, &transactions) + if err != nil { + InternalError(c, "查询交易记录失败") + return + } + + // 处理交易记录 + currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id) + for i := range transactions { + if i < 3 { + logTransactionMultiline(ledgerConfig.Mail, i, &transactions[i]) + } + + _, ok := currencyMap[transactions[i].Currency] + if ok { + transactions[i].IsAnotherCurrency = transactions[i].Currency != ledgerConfig.OperatingCurrency + } + + symbol := script.GetCommoditySymbol(ledgerConfig.Id, transactions[i].Currency) + transactions[i].CurrencySymbol = symbol + transactions[i].CostCurrencySymbol = symbol + + if transactions[i].Price != "" { + transactions[i].Price = strings.Fields(transactions[i].Price)[0] + } + if transactions[i].Balance != "" { + transactions[i].Balance = strings.Fields(transactions[i].Balance)[0] + } + } + + OK(c, transactions) +} + +// ==================== 交易操作相关 ==================== + func AddBatchTransactions(c *gin.Context) { var addTransactionForms []TransactionForm if err := c.ShouldBindJSON(&addTransactionForms); err != nil { BadRequest(c, err.Error()) return } + result := make([]string, 0) ledgerConfig := script.GetLedgerConfigFromContext(c) for _, form := range addTransactionForms { err := saveTransaction(nil, form, ledgerConfig) if err == nil { result = append(result, form.Date+form.Payee+form.Desc) - } else { - script.LogError(ledgerConfig.Mail, err.Error()) } } OK(c, result) } func AddTransactions(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) var addTransactionForm TransactionForm + if err := c.ShouldBindJSON(&addTransactionForm); err != nil { BadRequest(c, err.Error()) return } - ledgerConfig := script.GetLedgerConfigFromContext(c) - // 判断是否分期 + var err error - var divideCount = len(addTransactionForm.DivideDateList) + divideCount := len(addTransactionForm.DivideDateList) + if divideCount <= 0 { err = saveTransaction(c, addTransactionForm, ledgerConfig) } else { + // 分期处理 for idx, entry := range addTransactionForm.Entries { - // 保留 3 位小数 addTransactionForm.Entries[idx].Number = entry.Number.Div(decimal.NewFromInt(int64(divideCount))).Round(3) } for _, date := range addTransactionForm.DivideDateList { @@ -235,45 +456,44 @@ func AddTransactions(c *gin.Context) { } if err != nil { - script.LogError(ledgerConfig.Mail, err.Error()) + InternalError(c, err.Error()) return } + OK(c, nil) } -func saveTransaction(c *gin.Context, addTransactionForm TransactionForm, ledgerConfig *script.Config) error { - // 账户是否平衡 - sumVal := sum(addTransactionForm.Entries, ledgerConfig.OpeningBalances) +func saveTransaction(c *gin.Context, form TransactionForm, ledgerConfig *script.Config) error { + // 余额检查 + sumVal := sum(form.Entries, ledgerConfig.OpeningBalances) val, _ := decimal.NewFromString("0.1") if sumVal.Abs().GreaterThan(val) { if c != nil { TransactionNotBalance(c) } - return errors.New("transaction not balance") + return errors.New("交易不平衡") } - // 2021-09-29 * "支付宝" "黄金补仓X元" #Invest - line := fmt.Sprintf("\r\n%s * \"%s\" \"%s\"", addTransactionForm.Date, addTransactionForm.Payee, addTransactionForm.Desc) - if len(addTransactionForm.Tags) > 0 { - for _, tag := range addTransactionForm.Tags { - line += "#" + tag + " " - } + // 构建交易文本 + line := fmt.Sprintf("\r\n%s * \"%s\" \"%s\"", form.Date, form.Payee, form.Desc) + + // 添加标签 + for _, tag := range form.Tags { + line += "#" + tag + " " } currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id) + zero := decimal.NewFromInt(0) - var autoBalance bool - for _, entry := range addTransactionForm.Entries { + for _, entry := range form.Entries { if entry.Account == ledgerConfig.OpeningBalances { - autoBalance = false line += fmt.Sprintf("\r\n %s", entry.Account) } else { line += fmt.Sprintf("\r\n %s %s %s", entry.Account, entry.Number.Round(2).StringFixedBank(2), entry.Currency) } - zero := decimal.NewFromInt(0) - // 判断是否涉及多币种的转换 + + // 多币种处理 if entry.Currency != ledgerConfig.OperatingCurrency && entry.Account != ledgerConfig.OpeningBalances { - // 汇率值小于等于0,则不进行汇率转换 if entry.Price.LessThanOrEqual(zero) { continue } @@ -283,136 +503,99 @@ func saveTransaction(c *gin.Context, addTransactionForm TransactionForm, ledgerC if currencyPrice.Equal(zero) { currencyPrice, _ = decimal.NewFromString(currency.Price) } - // 货币跳过汇率转换 + if !isCurrency { - // 根据 number 的正负来判断是买入还是卖出 if entry.Number.GreaterThan(zero) { - // {351.729 CNY, 2021-09-29} - line += fmt.Sprintf(" {%s %s, %s}", entry.Price, ledgerConfig.OperatingCurrency, addTransactionForm.Date) + line += fmt.Sprintf(" {%s %s, %s}", entry.Price, ledgerConfig.OperatingCurrency, form.Date) } else { - // {} @ 359.019 CNY line += fmt.Sprintf(" {} @ %s %s", entry.Price, ledgerConfig.OperatingCurrency) } } else { - // 外币种格式:Assets:Fixed:三顿半咖啡 -1.00 SATURN_BIRD {5.61 CNY} - // fix issue #66 https://github.com/BaoXuebin/beancount-gs/issues/66 line += fmt.Sprintf(" {%s %s}", currencyPrice, ledgerConfig.OperatingCurrency) } - priceLine := fmt.Sprintf("%s price %s %s %s", addTransactionForm.Date, entry.Currency, entry.Price, ledgerConfig.OperatingCurrency) - err := script.AppendFileInNewLine(script.GetLedgerPriceFilePath(ledgerConfig.DataPath), priceLine) - if err != nil { - if c != nil { - InternalError(c, err.Error()) - } + // 更新价格文件 + priceLine := fmt.Sprintf("%s price %s %s %s", form.Date, entry.Currency, entry.Price, ledgerConfig.OperatingCurrency) + if err := script.AppendFileInNewLine(script.GetLedgerPriceFilePath(ledgerConfig.DataPath), priceLine); err != nil { return errors.New("internal error") } - // 刷新币种汇率 + if isCurrency { - err = script.LoadLedgerCurrencyMap(ledgerConfig) - if err != nil { - InternalError(c, err.Error()) + if err := script.LoadLedgerCurrencyMap(ledgerConfig); err != nil { return errors.New("internal error") } } } } - // 平衡小数点误差 - if autoBalance { - line += "\r\n " + ledgerConfig.OpeningBalances - } - // 记账的日期 - month, err := time.Parse("2006-01-02", addTransactionForm.Date) + // 确定文件路径 + month, err := time.Parse("2006-01-02", form.Date) if err != nil { - if c != nil { - InternalError(c, err.Error()) - } return errors.New("internal error") } - // 交易的月份信息 monthStr := month.Format("2006-01") - err = CreateMonthBeanFileIfNotExist(ledgerConfig.DataPath, monthStr) - if err != nil { - if c != nil { - InternalError(c, err.Error()) - } + if err := CreateMonthBeanFileIfNotExist(ledgerConfig.DataPath, monthStr); err != nil { return err } beanFilePath := script.GetLedgerMonthFilePath(ledgerConfig.DataPath, monthStr) - if addTransactionForm.ID != "" { // 更新交易 - result, e := script.BQLPrint(ledgerConfig, addTransactionForm.ID) - if e != nil { - InternalError(c, e.Error()) - return errors.New(e.Error()) - } - // 使用 \r\t 分割多行文本片段,并清理每一行的空白 - oldLines := filterEmptyStrings(strings.Split(result, "\n")) - startLine, endLine, e := script.FindConsecutiveMultilineTextInFile(beanFilePath, oldLines) - if e != nil { - InternalError(c, e.Error()) - return errors.New(e.Error()) - } - lines, e := script.RemoveLines(beanFilePath, startLine, endLine) - if e != nil { - InternalError(c, e.Error()) - return errors.New(e.Error()) - } - newLines := filterEmptyStrings(strings.Split(line, "\n")) - newLines = append(newLines, "") - lines, e = script.InsertLines(lines, startLine, newLines) - if e != nil { - InternalError(c, e.Error()) - return errors.New(e.Error()) - } - e = script.WriteToFile(beanFilePath, lines) - if e != nil { - InternalError(c, e.Error()) - return errors.New(e.Error()) - } - } else { // 新增交易 - err = script.AppendFileInNewLine(beanFilePath, line) + + if form.ID != "" { + // 更新交易 + return updateTransaction(ledgerConfig, form, beanFilePath, line) + } else { + // 新增交易 + return script.AppendFileInNewLine(beanFilePath, line) } - if err != nil { - if c != nil { - InternalError(c, err.Error()) - } - return errors.New("internal error") - } - return nil } -// 过滤字符串数组中的空字符串 -func filterEmptyStrings(arr []string) []string { - // 创建一个新切片来存储非空字符串 - var result []string - for _, str := range arr { - if script.CleanString(str) != "" { // 检查字符串是否为空 - result = append(result, str) - } +func updateTransaction(ledgerConfig *script.Config, form TransactionForm, beanFilePath, newContent string) error { + result, err := script.BQLPrint(ledgerConfig, form.ID) + if err != nil { + return err } - return result + + oldLines := filterEmptyStrings(strings.Split(result, "\n")) + startLine, endLine, err := script.FindConsecutiveMultilineTextInFile(beanFilePath, oldLines) + if err != nil { + return err + } + + lines, err := script.RemoveLines(beanFilePath, startLine, endLine) + if err != nil { + return err + } + + newLines := filterEmptyStrings(strings.Split(newContent, "\n")) + newLines = append(newLines, "") + lines, err = script.InsertLines(lines, startLine, newLines) + if err != nil { + return err + } + + return script.WriteToFile(beanFilePath, lines) } +// ==================== 其他功能 ==================== + func UpdateTransactionRawTextById(c *gin.Context) { - var rawTextUpdateTransactionForm UpdateRawTextTransactionForm - if err := c.ShouldBindJSON(&rawTextUpdateTransactionForm); err != nil { + var form UpdateRawTextTransactionForm + if err := c.ShouldBindJSON(&form); err != nil { BadRequest(c, err.Error()) return } - ledgerConfig := script.GetLedgerConfigFromContext(c) - beanFilePath, err := getBeanFilePathByTransactionId(rawTextUpdateTransactionForm.ID, ledgerConfig) + ledgerConfig := script.GetLedgerConfigFromContext(c) + beanFilePath, err := getBeanFilePathByTransactionId(form.ID, ledgerConfig) if err != nil { InternalError(c, err.Error()) return } - result, e := script.BQLPrint(ledgerConfig, rawTextUpdateTransactionForm.ID) - if e != nil { - InternalError(c, e.Error()) + result, err := script.BQLPrint(ledgerConfig, form.ID) + if err != nil { + InternalError(c, err.Error()) return } @@ -422,24 +605,27 @@ func UpdateTransactionRawTextById(c *gin.Context) { InternalError(c, err.Error()) return } - lines, e := script.RemoveLines(beanFilePath, startLine, endLine) - if e != nil { - InternalError(c, e.Error()) - return - } - newLines := filterEmptyStrings(strings.Split(rawTextUpdateTransactionForm.RawText, "\n")) - if len(newLines) > 0 { - lines, e = script.InsertLines(lines, startLine, newLines) - if e != nil { - InternalError(c, e.Error()) - return - } - } - err = script.WriteToFile(beanFilePath, lines) + + lines, err := script.RemoveLines(beanFilePath, startLine, endLine) if err != nil { InternalError(c, err.Error()) return } + + newLines := filterEmptyStrings(strings.Split(form.RawText, "\n")) + if len(newLines) > 0 { + lines, err = script.InsertLines(lines, startLine, newLines) + if err != nil { + InternalError(c, err.Error()) + return + } + } + + if err := script.WriteToFile(beanFilePath, lines); err != nil { + InternalError(c, err.Error()) + return + } + OK(c, true) } @@ -449,15 +635,15 @@ func DeleteTransactionById(c *gin.Context) { BadRequest(c, "Param 'id' must not be blank.") return } - ledgerConfig := script.GetLedgerConfigFromContext(c) - result, e := script.BQLPrint(ledgerConfig, queryParams.ID) - if e != nil { - InternalError(c, e.Error()) + ledgerConfig := script.GetLedgerConfigFromContext(c) + beanFilePath, err := getBeanFilePathByTransactionId(queryParams.ID, ledgerConfig) + if err != nil { + InternalError(c, err.Error()) return } - beanFilePath, err := getBeanFilePathByTransactionId(queryParams.ID, ledgerConfig) + result, err := script.BQLPrint(ledgerConfig, queryParams.ID) if err != nil { InternalError(c, err.Error()) return @@ -469,16 +655,18 @@ func DeleteTransactionById(c *gin.Context) { InternalError(c, err.Error()) return } - lines, e := script.RemoveLines(beanFilePath, startLine, endLine) - if e != nil { - InternalError(c, e.Error()) - return - } - err = script.WriteToFile(beanFilePath, lines) + + lines, err := script.RemoveLines(beanFilePath, startLine, endLine) if err != nil { InternalError(c, err.Error()) return } + + if err := script.WriteToFile(beanFilePath, lines); err != nil { + InternalError(c, err.Error()) + return + } + OK(c, true) } @@ -492,45 +680,16 @@ func getBeanFilePathByTransactionId(transactionId string, ledgerConfig *script.C if len(transactions) == 0 { return "", errors.New("no transaction found") } + month, err := script.GetMonth(transactions[0].Date) if err != nil { return "", err } - // 交易记录所在文件位置 - beanFilePath := script.GetLedgerMonthFilePath(ledgerConfig.DataPath, month) - return beanFilePath, nil + + return script.GetLedgerMonthFilePath(ledgerConfig.DataPath, month), nil } -type transactionPayee struct { - Value string `bql:"distinct payee" json:"value"` -} - -func QueryTransactionPayees(c *gin.Context) { - ledgerConfig := script.GetLedgerConfigFromContext(c) - payeeList := make([]transactionPayee, 0) - queryParams := script.QueryParams{Where: false, OrderBy: "date desc", Limit: 100} - err := script.BQLQueryList(ledgerConfig, &queryParams, &payeeList) - if err != nil { - InternalError(c, err.Error()) - return - } - result := make([]string, 0) - for _, payee := range payeeList { - if payee.Value != "" { - result = append(result, payee.Value) - } - } - OK(c, result) -} - -type TransactionTemplate struct { - Id string `json:"id"` - Date string `form:"date" binding:"required" json:"date"` - TemplateName string `form:"templateName" binding:"required" json:"templateName"` - Payee string `form:"payee" json:"payee"` - Desc string `form:"desc" binding:"required" json:"desc"` - Entries []TransactionEntryForm `form:"entries" json:"entries"` -} +// ==================== 模板相关 ==================== func QueryTransactionTemplates(c *gin.Context) { ledgerConfig := script.GetLedgerConfigFromContext(c) @@ -544,8 +703,8 @@ func QueryTransactionTemplates(c *gin.Context) { } func AddTransactionTemplate(c *gin.Context) { - var transactionTemplate TransactionTemplate - if err := c.ShouldBindJSON(&transactionTemplate); err != nil { + var template TransactionTemplate + if err := c.ShouldBindJSON(&template); err != nil { BadRequest(c, err.Error()) return } @@ -558,21 +717,18 @@ func AddTransactionTemplate(c *gin.Context) { return } + // 生成唯一ID t := sha1.New() - _, err = io.WriteString(t, time.Now().String()) - if err != nil { - InternalError(c, err.Error()) - return - } - transactionTemplate.Id = hex.EncodeToString(t.Sum(nil)) - templates = append(templates, transactionTemplate) + io.WriteString(t, time.Now().String()) + template.Id = hex.EncodeToString(t.Sum(nil)) + templates = append(templates, template) - err = writeLedgerTransactionTemplates(filePath, templates) - if err != nil { + if err := writeLedgerTransactionTemplates(filePath, templates); err != nil { InternalError(c, err.Error()) return } - OK(c, transactionTemplate) + + OK(c, template) } func DeleteTransactionTemplate(c *gin.Context) { @@ -598,8 +754,7 @@ func DeleteTransactionTemplate(c *gin.Context) { } } - err = writeLedgerTransactionTemplates(filePath, newTemplates) - if err != nil { + if err := writeLedgerTransactionTemplates(filePath, newTemplates); err != nil { InternalError(c, err.Error()) return } @@ -609,23 +764,22 @@ func DeleteTransactionTemplate(c *gin.Context) { func getLedgerTransactionTemplates(filePath string) ([]TransactionTemplate, error) { result := make([]TransactionTemplate, 0) - if script.FileIfExist(filePath) { - bytes, err := script.ReadFile(filePath) - if err != nil { - return nil, err - } - err = json.Unmarshal(bytes, &result) - if err != nil { - return nil, err - } + if !script.FileIfExist(filePath) { + return result, nil } - return result, nil + + bytes, err := script.ReadFile(filePath) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &result) + return result, err } func writeLedgerTransactionTemplates(filePath string, templates []TransactionTemplate) error { if !script.FileIfExist(filePath) { - err := script.CreateFile(filePath) - if err != nil { + if err := script.CreateFile(filePath); err != nil { return err } } @@ -634,9 +788,72 @@ func writeLedgerTransactionTemplates(filePath string, templates []TransactionTem if err != nil { return err } - err = script.WriteFile(filePath, string(bytes)) - if err != nil { - return err - } - return nil + + return script.WriteFile(filePath, string(bytes)) +} + +// ==================== 辅助查询 ==================== + +type transactionPayee struct { + Value string `bql:"distinct payee" json:"value"` +} + +func QueryTransactionPayees(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + payeeList := make([]transactionPayee, 0) + + queryParams := script.QueryParams{ + Where: false, + OrderBy: "date desc", + Limit: 100, + } + + err := script.BQLQueryList(ledgerConfig, &queryParams, &payeeList) + if err != nil { + InternalError(c, err.Error()) + return + } + + result := make([]string, 0) + for _, payee := range payeeList { + if payee.Value != "" { + result = append(result, payee.Value) + } + } + OK(c, result) +} + +func QueryAvailableYearMonths(c *gin.Context) { + ledgerConfig := script.GetLedgerConfigFromContext(c) + + var yearMonths []struct { + Year int `bql:"year" json:"year"` + Month int `bql:"month" json:"month"` + Count int `bql:"count" json:"count"` + } + + queryParams := script.QueryParams{ + Where: false, + GroupBy: "year, month", + OrderBy: "year desc, month desc", + } + + err := script.BQLQueryList(ledgerConfig, &queryParams, &yearMonths) + if err != nil { + InternalError(c, "查询可用年月失败") + return + } + + result := make([]YearMonthOption, 0) + result = append(result, YearMonthOption{Year: 0, Month: 0, Count: -1}) + + for _, ym := range yearMonths { + result = append(result, YearMonthOption{ + Year: ym.Year, + Month: ym.Month, + Count: ym.Count, + }) + } + + OK(c, result) }