860 lines
23 KiB
Go
860 lines
23 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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:"distinct id" json:"id"`
|
|
Account string `bql:"account" json:"account"`
|
|
Date string `bql:"date" json:"date"`
|
|
Payee string `bql:"payee" json:"payee"`
|
|
Narration string `bql:"narration" json:"desc"`
|
|
Number string `bql:"number" json:"number"`
|
|
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"`
|
|
CostCurrency string `bql:"cost_currency" json:"costCurrency"`
|
|
Price string `bql:"price" json:"price"`
|
|
Tags []string `bql:"tags" json:"tags"`
|
|
CurrencySymbol string `json:"currencySymbol,omitempty"`
|
|
CostCurrencySymbol string `json:"costCurrencySymbol,omitempty"`
|
|
IsAnotherCurrency bool `json:"isAnotherCurrency,omitempty"`
|
|
}
|
|
|
|
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"`
|
|
RawText string `json:"rawText,omitempty"`
|
|
}
|
|
|
|
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"`
|
|
Price decimal.Decimal `form:"price" json:"price,omitempty"`
|
|
PriceCurrency string `form:"priceCurrency" json:"priceCurrency,omitempty"`
|
|
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 {
|
|
if entry.Account == openingBalances {
|
|
return decimal.NewFromInt(0)
|
|
}
|
|
pVal, _ := entry.Price.Float64()
|
|
if pVal == 0 {
|
|
sumVal = entry.Number.Add(sumVal)
|
|
} else {
|
|
sumVal = entry.Number.Mul(entry.Price).Add(sumVal)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
var err error
|
|
divideCount := len(addTransactionForm.DivideDateList)
|
|
|
|
if divideCount <= 0 {
|
|
err = saveTransaction(c, addTransactionForm, ledgerConfig)
|
|
} else {
|
|
// 分期处理
|
|
for idx, entry := range addTransactionForm.Entries {
|
|
addTransactionForm.Entries[idx].Number = entry.Number.Div(decimal.NewFromInt(int64(divideCount))).Round(3)
|
|
}
|
|
for _, date := range addTransactionForm.DivideDateList {
|
|
addTransactionForm.Date = date
|
|
err = saveTransaction(c, addTransactionForm, ledgerConfig)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
OK(c, nil)
|
|
}
|
|
|
|
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("交易不平衡")
|
|
}
|
|
|
|
// 构建交易文本
|
|
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)
|
|
|
|
for _, entry := range form.Entries {
|
|
if entry.Account == ledgerConfig.OpeningBalances {
|
|
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)
|
|
}
|
|
|
|
// 多币种处理
|
|
if entry.Currency != ledgerConfig.OperatingCurrency && entry.Account != ledgerConfig.OpeningBalances {
|
|
if entry.Price.LessThanOrEqual(zero) {
|
|
continue
|
|
}
|
|
|
|
currency, isCurrency := currencyMap[entry.Currency]
|
|
currencyPrice := entry.Price
|
|
if currencyPrice.Equal(zero) {
|
|
currencyPrice, _ = decimal.NewFromString(currency.Price)
|
|
}
|
|
|
|
if !isCurrency {
|
|
if entry.Number.GreaterThan(zero) {
|
|
line += fmt.Sprintf(" {%s %s, %s}", entry.Price, ledgerConfig.OperatingCurrency, form.Date)
|
|
} else {
|
|
line += fmt.Sprintf(" {} @ %s %s", entry.Price, ledgerConfig.OperatingCurrency)
|
|
}
|
|
} else {
|
|
line += fmt.Sprintf(" {%s %s}", currencyPrice, ledgerConfig.OperatingCurrency)
|
|
}
|
|
|
|
// 更新价格文件
|
|
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 {
|
|
if err := script.LoadLedgerCurrencyMap(ledgerConfig); err != nil {
|
|
return errors.New("internal error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 确定文件路径
|
|
month, err := time.Parse("2006-01-02", form.Date)
|
|
if err != nil {
|
|
return errors.New("internal error")
|
|
}
|
|
|
|
monthStr := month.Format("2006-01")
|
|
if err := CreateMonthBeanFileIfNotExist(ledgerConfig.DataPath, monthStr); err != nil {
|
|
return err
|
|
}
|
|
|
|
beanFilePath := script.GetLedgerMonthFilePath(ledgerConfig.DataPath, monthStr)
|
|
|
|
if form.ID != "" {
|
|
// 更新交易
|
|
return updateTransaction(ledgerConfig, form, beanFilePath, line)
|
|
} else {
|
|
// 新增交易
|
|
return script.AppendFileInNewLine(beanFilePath, line)
|
|
}
|
|
}
|
|
|
|
func updateTransaction(ledgerConfig *script.Config, form TransactionForm, beanFilePath, newContent string) error {
|
|
result, err := script.BQLPrint(ledgerConfig, form.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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 form UpdateRawTextTransactionForm
|
|
if err := c.ShouldBindJSON(&form); err != nil {
|
|
BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
beanFilePath, err := getBeanFilePathByTransactionId(form.ID, ledgerConfig)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := script.BQLPrint(ledgerConfig, form.ID)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
oldLines := filterEmptyStrings(strings.Split(result, "\n"))
|
|
startLine, endLine, err := script.FindConsecutiveMultilineTextInFile(beanFilePath, oldLines)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
beanFilePath, err := getBeanFilePathByTransactionId(queryParams.ID, ledgerConfig)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := script.BQLPrint(ledgerConfig, queryParams.ID)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
oldLines := filterEmptyStrings(strings.Split(result, "\n"))
|
|
startLine, endLine, err := script.FindConsecutiveMultilineTextInFile(beanFilePath, oldLines)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func getBeanFilePathByTransactionId(transactionId string, ledgerConfig *script.Config) (string, error) {
|
|
queryParams := script.QueryParams{ID: transactionId, Where: true}
|
|
transactions := make([]Transaction, 0)
|
|
err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(transactions) == 0 {
|
|
return "", errors.New("no transaction found")
|
|
}
|
|
|
|
month, err := script.GetMonth(transactions[0].Date)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return script.GetLedgerMonthFilePath(ledgerConfig.DataPath, month), nil
|
|
}
|
|
|
|
// ==================== 模板相关 ====================
|
|
|
|
func QueryTransactionTemplates(c *gin.Context) {
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
|
|
templates, err := getLedgerTransactionTemplates(filePath)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
OK(c, templates)
|
|
}
|
|
|
|
func AddTransactionTemplate(c *gin.Context) {
|
|
var template TransactionTemplate
|
|
if err := c.ShouldBindJSON(&template); err != nil {
|
|
BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
|
|
templates, err := getLedgerTransactionTemplates(filePath)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// 生成唯一ID
|
|
t := sha1.New()
|
|
io.WriteString(t, time.Now().String())
|
|
template.Id = hex.EncodeToString(t.Sum(nil))
|
|
templates = append(templates, template)
|
|
|
|
if err := writeLedgerTransactionTemplates(filePath, templates); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
OK(c, template)
|
|
}
|
|
|
|
func DeleteTransactionTemplate(c *gin.Context) {
|
|
templateId := c.Query("id")
|
|
if templateId == "" {
|
|
BadRequest(c, "templateId is not blank")
|
|
return
|
|
}
|
|
|
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
|
filePath := script.GetLedgerTransactionsTemplateFilePath(ledgerConfig.DataPath)
|
|
|
|
oldTemplates, err := getLedgerTransactionTemplates(filePath)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
newTemplates := make([]TransactionTemplate, 0)
|
|
for _, template := range oldTemplates {
|
|
if template.Id != templateId {
|
|
newTemplates = append(newTemplates, template)
|
|
}
|
|
}
|
|
|
|
if err := writeLedgerTransactionTemplates(filePath, newTemplates); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
OK(c, templateId)
|
|
}
|
|
|
|
func getLedgerTransactionTemplates(filePath string) ([]TransactionTemplate, error) {
|
|
result := make([]TransactionTemplate, 0)
|
|
if !script.FileIfExist(filePath) {
|
|
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) {
|
|
if err := script.CreateFile(filePath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
bytes, err := json.Marshal(templates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|