Merge branch 'main' of github.com:BaoXuebin/beancount-gs

This commit is contained in:
BaoXuebin 2024-10-29 13:41:00 +08:00
commit 9b55de7e22
5 changed files with 359 additions and 23 deletions

View File

@ -61,6 +61,10 @@ func GetQueryParams(c *gin.Context) QueryParams {
queryParams.Limit = 100
hasWhere = true
}
if c.Query("id") != "" {
queryParams.ID = c.Query("id")
hasWhere = true
}
queryParams.Where = hasWhere
if c.Query("path") != "" {
queryParams.Path = c.Query("path")
@ -81,6 +85,19 @@ func GetQueryParams(c *gin.Context) QueryParams {
// return nil
//}
func BQLPrint(ledgerConfig *Config, transactionId string) (string, error) {
// PRINT FROM id = 'xxx'
output, err := queryByBQL(ledgerConfig, "PRINT FROM id = '"+transactionId+"'")
if err != nil {
return "", err
}
utf8, err := ConvertGBKToUTF8(output)
if err != nil {
return "", err
}
return utf8, nil
}
func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
assertQueryResultIsPointer(queryResultPtr)
output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr)

View File

@ -2,6 +2,7 @@ package script
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@ -207,3 +208,123 @@ func MkDir(dirPath string) error {
LogSystemInfo("Success mkdir " + dirPath)
return nil
}
// FindConsecutiveMultilineTextInFile 查找文件中连续多行文本片段的开始和结束行号
func FindConsecutiveMultilineTextInFile(filePath string, multilineLines []string) (startLine, endLine int, err error) {
for i := range multilineLines {
multilineLines[i] = cleanString(multilineLines[i])
}
file, err := os.Open(filePath)
if err != nil {
return -1, -1, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
startLine = -1
endLine = -1
lineNumber := 0
matchIndex := 0
for scanner.Scan() {
lineNumber++
// 清理文件中的当前行
lineText := cleanString(scanner.Text())
// 检查当前行是否匹配多行文本片段的当前行
if lineText == multilineLines[matchIndex] {
if startLine == -1 {
startLine = lineNumber // 记录起始行号
}
matchIndex++
// 如果所有行都匹配完成,记录结束行号并退出循环
if matchIndex == len(multilineLines) {
endLine = lineNumber
break
}
} else {
// 如果匹配失败,重置匹配索引和起始行号
matchIndex = 0
startLine = -1
}
}
if err := scanner.Err(); err != nil {
return -1, -1, err
}
// 如果未找到完整的多行文本片段,则返回 -1
if startLine == -1 || endLine == -1 {
return -1, -1, fmt.Errorf("未找到连续的多行文本片段")
}
LogSystemInfo("Success find content in file " + filePath + " line range: " + string(rune(startLine)) + "," + string(rune(endLine)))
return startLine, endLine, nil
}
// cleanString 去除字符串中的首尾空白和中间的所有空格字符
func cleanString(str string) string {
return strings.ReplaceAll(strings.TrimSpace(str), " ", "")
}
// 删除指定行范围的内容
func RemoveLines(filePath string, startLineNo, endLineNo int) ([]string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// 读取文件的每一行
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
// 检查行号的有效性
if startLineNo < 1 || endLineNo > len(lines) || startLineNo > endLineNo {
return nil, fmt.Errorf("行号范围无效")
}
// 删除从 startLineNo 到 endLineNo 的行(下标从 0 开始)
modifiedLines := append(lines[:startLineNo-1], lines[endLineNo:]...)
return modifiedLines, nil
}
// 在指定行号插入多行文本
func InsertLines(lines []string, startLineNo int, newLines []string) ([]string, error) {
// 检查插入位置的有效性
if startLineNo < 1 || startLineNo > len(lines)+1 {
return nil, fmt.Errorf("插入行号无效")
}
// 在指定位置插入新的内容
modifiedLines := append(lines[:startLineNo-1], append(newLines, lines[startLineNo-1:]...)...)
return modifiedLines, nil
}
// 写回文件
func WriteToFile(filePath string, lines []string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
// 将修改后的内容写回文件
writer := bufio.NewWriter(file)
for _, line := range lines {
_, err := writer.WriteString(line + "\n")
if err != nil {
return err
}
}
LogSystemInfo("Success write content in file " + filePath)
return writer.Flush()
}

View File

@ -2,6 +2,9 @@ package script
import (
"bytes"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io/ioutil"
"math/rand"
"net"
"os/exec"
@ -84,3 +87,31 @@ func getMaxDate(str_date1 string, str_date2 string) string {
}
return max_date
}
// ConvertGBKToUTF8 将 GBK 编码的字符串转换为 UTF-8 编码
func ConvertGBKToUTF8(gbkStr string) (string, error) {
if !isWindows() {
return gbkStr, nil
}
// 创建一个 GBK 到 UTF-8 的转换器
reader := transform.NewReader(bytes.NewReader([]byte(gbkStr)), simplifiedchinese.GBK.NewDecoder())
// 将转换后的内容读出为 UTF-8 字符串
utf8Bytes, err := ioutil.ReadAll(reader)
if err != nil {
return "", err
}
return string(utf8Bytes), nil
}
func GetMonth(date string) (string, error) {
// 解析日期字符串
parsedDate, err := time.Parse("2006-01-02", date)
if err != nil {
return "", err
}
// 格式化日期为 "YYYY-MM" 格式
formattedDate := parsedDate.Format("2006-01")
return formattedDate, nil
}

View File

@ -79,8 +79,11 @@ func RegisterRouter(router *gin.Engine) {
authorized.GET("/stats/month/total", service.StatsMonthTotal)
authorized.GET("/stats/month/calendar", service.StatsMonthCalendar)
authorized.GET("/stats/commodity/price", service.StatsCommodityPrice)
authorized.GET("/transaction/detail", service.QueryTransactionDetailById)
authorized.GET("/transaction/raw", service.QueryTransactionRawTextById)
authorized.GET("/transaction", service.QueryTransactions)
authorized.POST("/transaction", service.AddTransactions)
authorized.DELETE("/transaction", service.DeleteTransactionById)
authorized.POST("/transaction/batch", service.AddBatchTransactions)
authorized.GET("/transaction/payee", service.QueryTransactionPayees)
authorized.GET("/transaction/template", service.QueryTransactionTemplates)

View File

@ -6,14 +6,13 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/beancount-gs/script"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"io"
"strconv"
"strings"
"time"
"github.com/beancount-gs/script"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
)
type Transaction struct {
@ -35,6 +34,13 @@ 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 {
@ -49,6 +55,64 @@ func (s TransactionSort) Less(i, j int) bool {
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)
@ -83,16 +147,19 @@ func QueryTransactions(c *gin.Context) {
OK(c, transactions)
}
type AddTransactionForm struct {
Date string `form:"date" binding:"required"`
Payee string `form:"payee"`
Desc string `form:"desc" binding:"required"`
Tags []string `form:"tags"`
DivideDateList []string `form:"divideDateList"`
Entries []AddTransactionEntryForm `form:"entries"`
type TransactionForm struct {
ID string `form:"id" json:"id"`
Date string `form:"date" binding:"required" json:"date"`
Payee string `form:"payee" json:"payee,omitempty"`
Desc string `form:"desc" binding:"required" json:"desc"`
Narration string `form:"narration" json:"narration,omitempty"`
Tags []string `form:"tags" json:"tags,omitempty"`
DivideDateList []string `form:"divideDateList" json:"divideDateList,omitempty"`
Entries []TransactionEntryForm `form:"entries" json:"entries"`
Raw RawTransaction `json:"raw,omitempty"`
}
type AddTransactionEntryForm struct {
type TransactionEntryForm struct {
Account string `form:"account" binding:"required" json:"account"`
Number decimal.Decimal `form:"number" json:"number,omitempty"`
Currency string `form:"currency" json:"currency"`
@ -101,7 +168,7 @@ type AddTransactionEntryForm struct {
IsAnotherCurrency bool `form:"isAnotherCurrency" json:"isAnotherCurrency,omitempty"`
}
func sum(entries []AddTransactionEntryForm, openingBalances string) decimal.Decimal {
func sum(entries []TransactionEntryForm, openingBalances string) decimal.Decimal {
sumVal := decimal.NewFromInt(0)
for _, entry := range entries {
if entry.Account == openingBalances {
@ -118,7 +185,7 @@ func sum(entries []AddTransactionEntryForm, openingBalances string) decimal.Deci
}
func AddBatchTransactions(c *gin.Context) {
var addTransactionForms []AddTransactionForm
var addTransactionForms []TransactionForm
if err := c.ShouldBindJSON(&addTransactionForms); err != nil {
BadRequest(c, err.Error())
return
@ -137,7 +204,7 @@ func AddBatchTransactions(c *gin.Context) {
}
func AddTransactions(c *gin.Context) {
var addTransactionForm AddTransactionForm
var addTransactionForm TransactionForm
if err := c.ShouldBindJSON(&addTransactionForm); err != nil {
BadRequest(c, err.Error())
return
@ -169,7 +236,7 @@ func AddTransactions(c *gin.Context) {
OK(c, nil)
}
func saveTransaction(c *gin.Context, addTransactionForm AddTransactionForm, ledgerConfig *script.Config) error {
func saveTransaction(c *gin.Context, addTransactionForm TransactionForm, ledgerConfig *script.Config) error {
// 账户是否平衡
sumVal := sum(addTransactionForm.Entries, ledgerConfig.OpeningBalances)
val, _ := decimal.NewFromString("0.1")
@ -269,7 +336,40 @@ func saveTransaction(c *gin.Context, addTransactionForm AddTransactionForm, ledg
return err
}
err = script.AppendFileInNewLine(script.GetLedgerMonthFilePath(ledgerConfig.DataPath, monthStr), line)
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, "\r\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, "\r\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 err != nil {
if c != nil {
InternalError(c, err.Error())
@ -279,6 +379,70 @@ func saveTransaction(c *gin.Context, addTransactionForm AddTransactionForm, ledg
return nil
}
// 过滤字符串数组中的空字符串
func filterEmptyStrings(arr []string) []string {
// 创建一个新切片来存储非空字符串
var result []string
for _, str := range arr {
if str != "" { // 检查字符串是否为空
result = append(result, str)
}
}
return result
}
func DeleteTransactionById(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 {
InternalError(c, "No transaction found.")
return
}
month, err := script.GetMonth(transactions[0].Date)
if err != nil {
InternalError(c, err.Error())
return
}
// 交易记录所在文件位置
beanFilePath := script.GetLedgerMonthFilePath(ledgerConfig.DataPath, month)
result, e := script.BQLPrint(ledgerConfig, queryParams.ID)
if e != nil {
InternalError(c, e.Error())
return
}
oldLines := filterEmptyStrings(strings.Split(result, "\r\n"))
startLine, endLine, err := script.FindConsecutiveMultilineTextInFile(beanFilePath, oldLines)
if err != nil {
InternalError(c, err.Error())
return
}
lines, e := script.RemoveLines(beanFilePath, startLine, endLine)
if e != nil {
InternalError(c, err.Error())
return
}
err = script.WriteToFile(beanFilePath, lines)
if err != nil {
InternalError(c, err.Error())
return
}
OK(c, true)
}
type transactionPayee struct {
Value string `bql:"distinct payee" json:"value"`
}
@ -307,7 +471,7 @@ type TransactionTemplate struct {
TemplateName string `form:"templateName" binding:"required" json:"templateName"`
Payee string `form:"payee" json:"payee"`
Desc string `form:"desc" binding:"required" json:"desc"`
Entries []AddTransactionEntryForm `form:"entries" json:"entries"`
Entries []TransactionEntryForm `form:"entries" json:"entries"`
}
func QueryTransactionTemplates(c *gin.Context) {