Merge branch 'main' of github.com:BaoXuebin/beancount-gs
This commit is contained in:
commit
9b55de7e22
|
|
@ -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)
|
||||
|
|
|
|||
121
script/file.go
121
script/file.go
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -302,12 +466,12 @@ func QueryTransactionPayees(c *gin.Context) {
|
|||
}
|
||||
|
||||
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 []AddTransactionEntryForm `form:"entries" json:"entries"`
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue