mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-22 02:34:26 +08:00
support using duplicate checker to prevent duplicate submissions for new transaction record
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||||
@@ -88,6 +89,15 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = duplicatechecker.InitializeDuplicateChecker(config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !isDisableBootLog {
|
||||||
|
log.BootErrorf("[initializer.initializeSystem] initializes duplicate checker failed, because %s", err.Error())
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = mail.InitializeMailer(config)
|
err = mail.InitializeMailer(config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ generator_type = internal
|
|||||||
# For "internal" only, each server must have unique id (0 - 255)
|
# For "internal" only, each server must have unique id (0 - 255)
|
||||||
server_id = 0
|
server_id = 0
|
||||||
|
|
||||||
|
[duplicate_checker]
|
||||||
|
# Duplicate checker type, supports "in_memory" currently
|
||||||
|
checker_type = in_memory
|
||||||
|
|
||||||
|
# For "in_memory" only, cleanup expired data interval seconds (0 - 4294967295), default is 60 (1 minutes)
|
||||||
|
cleanup_interval = 60
|
||||||
|
|
||||||
|
# The minimum interval seconds between duplicate submissions on the same page (exiting and re-entering the page is considered as a new session)
|
||||||
|
# Set to 0 to disable duplicate checker for new data submissions, default is 300 (5 minutes)
|
||||||
|
duplicate_submissions_interval = 300
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
||||||
secret_key =
|
secret_key =
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
|||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -203,6 +206,42 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
|
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
|
||||||
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
|
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
|
||||||
|
|
||||||
|
if settings.Container.Current.EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
|
||||||
|
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
|
accountId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get existed account \"id:%d\" for user \"uid:%d\", because %s", accountId, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
||||||
|
mainAccount, exists := accountMap[accountId]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||||
|
|
||||||
|
for i := 0; i < len(accountAndSubAccounts); i++ {
|
||||||
|
if accountAndSubAccounts[i].ParentAccountId == mainAccount.AccountId {
|
||||||
|
subAccountResp := accountAndSubAccounts[i].ToAccountInfoResponse()
|
||||||
|
accountInfoResp.SubAccounts = append(accountInfoResp.SubAccounts, subAccountResp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountInfoResp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
|
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -212,6 +251,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
||||||
|
|
||||||
|
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
|
||||||
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||||
|
|
||||||
if len(childrenAccounts) > 0 {
|
if len(childrenAccounts) > 0 {
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransactionCategoriesApi represents transaction category api
|
// TransactionCategoriesApi represents transaction category api
|
||||||
@@ -119,6 +122,28 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
|
|||||||
|
|
||||||
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
|
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
|
||||||
|
|
||||||
|
if settings.Container.Current.EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
|
||||||
|
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
|
categoryId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get existed category \"id:%d\" for user \"uid:%d\", because %s", categoryId, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||||
|
|
||||||
|
return categoryResp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = a.categories.CreateCategory(c, category)
|
err = a.categories.CreateCategory(c, category)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,6 +153,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
|
|||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
||||||
|
|
||||||
|
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
|
||||||
categoryResp := category.ToTransactionCategoryInfoResponse()
|
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||||
|
|
||||||
return categoryResp, nil
|
return categoryResp, nil
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import (
|
|||||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -625,6 +627,28 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
|
|||||||
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
|
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.Container.Current.EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" {
|
||||||
|
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] another transaction \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
|
transactionId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
transaction, err = a.transactions.GetTransactionByTransactionId(c, uid, transactionId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to get existed transaction \"id:%d\" for user \"uid:%d\", because %s", transactionId, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
|
||||||
|
|
||||||
|
return transactionResp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = a.transactions.CreateTransaction(c, transaction, tagIds)
|
err = a.transactions.CreateTransaction(c, transaction, tagIds)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -634,6 +658,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
|
|||||||
|
|
||||||
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
|
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
|
||||||
|
|
||||||
|
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId))
|
||||||
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
|
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
|
||||||
|
|
||||||
return transactionResp, nil
|
return transactionResp, nil
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package duplicatechecker
|
||||||
|
|
||||||
|
// DuplicateChecker is common duplicate checker interface
|
||||||
|
type DuplicateChecker interface {
|
||||||
|
Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string)
|
||||||
|
Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package duplicatechecker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DuplicateCheckerContainer contains the current duplicate checker
|
||||||
|
type DuplicateCheckerContainer struct {
|
||||||
|
Current DuplicateChecker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a duplicate checker container singleton instance
|
||||||
|
var (
|
||||||
|
Container = &DuplicateCheckerContainer{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeDuplicateChecker initializes the current duplicate checker according to the config
|
||||||
|
func InitializeDuplicateChecker(config *settings.Config) error {
|
||||||
|
if config.DuplicateCheckerType == settings.InMemoryDuplicateCheckerType {
|
||||||
|
checker, err := NewInMemoryDuplicateChecker(config)
|
||||||
|
Container.Current = checker
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs.ErrInvalidDuplicateCheckerType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns whether the same submission has been processed and related remark by the current duplicate checker
|
||||||
|
func (c *DuplicateCheckerContainer) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
|
||||||
|
return c.Current.Get(checkerType, uid, identification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set saves the identification and remark to in-memory cache by the current duplicate checker
|
||||||
|
func (c *DuplicateCheckerContainer) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
|
||||||
|
c.Current.Set(checkerType, uid, identification, remark)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package duplicatechecker
|
||||||
|
|
||||||
|
// DuplicateCheckerType represents duplicate checker type
|
||||||
|
type DuplicateCheckerType uint8
|
||||||
|
|
||||||
|
// Types of uuid
|
||||||
|
const (
|
||||||
|
DUPLICATE_CHECKER_TYPE_DEFAULT DuplicateCheckerType = 0
|
||||||
|
DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT DuplicateCheckerType = 1
|
||||||
|
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
|
||||||
|
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package duplicatechecker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InMemoryDuplicateChecker represents in-memory duplicate checker
|
||||||
|
type InMemoryDuplicateChecker struct {
|
||||||
|
cache *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInMemoryDuplicateChecker returns a new in-memory duplicate checker
|
||||||
|
func NewInMemoryDuplicateChecker(config *settings.Config) (*InMemoryDuplicateChecker, error) {
|
||||||
|
checker := &InMemoryDuplicateChecker{
|
||||||
|
cache: cache.New(config.DuplicateSubmissionsIntervalDuration, config.InMemoryDuplicateCheckerCleanupIntervalDuration),
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns whether the same submission has been processed and related remark
|
||||||
|
func (c *InMemoryDuplicateChecker) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
|
||||||
|
existedRemark, found := c.cache.Get(c.getCacheKey(checkerType, uid, identification))
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return true, existedRemark.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set saves the identification and remark to in-memory cache
|
||||||
|
func (c *InMemoryDuplicateChecker) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
|
||||||
|
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, cache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string {
|
||||||
|
return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification)
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ var (
|
|||||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
|
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
|
||||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
|
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
|
||||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid amap security verification method")
|
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid amap security verification method")
|
||||||
|
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid duplicate checker type")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ type AccountCreateRequest struct {
|
|||||||
Balance int64 `json:"balance"`
|
Balance int64 `json:"balance"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
|
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
|
||||||
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountModifyRequest represents all parameters of account modification request
|
// AccountModifyRequest represents all parameters of account modification request
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type TransactionCreateRequest struct {
|
|||||||
TagIds []string `json:"tagIds"`
|
TagIds []string `json:"tagIds"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
|
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
|
||||||
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionModifyRequest represents all parameters of transaction modification request
|
// TransactionModifyRequest represents all parameters of transaction modification request
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type TransactionCategoryCreateRequest struct {
|
|||||||
Icon int64 `json:"icon,string" binding:"min=1"`
|
Icon int64 `json:"icon,string" binding:"min=1"`
|
||||||
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionCategoryCreateBatchRequest represents all parameters of transaction category batch creation request
|
// TransactionCategoryCreateBatchRequest represents all parameters of transaction category batch creation request
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ const (
|
|||||||
InternalUuidGeneratorType string = "internal"
|
InternalUuidGeneratorType string = "internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Duplicate checker types
|
||||||
|
const (
|
||||||
|
InMemoryDuplicateCheckerType string = "in_memory"
|
||||||
|
)
|
||||||
|
|
||||||
// User avatar provider types
|
// User avatar provider types
|
||||||
const (
|
const (
|
||||||
GravatarProvider string = "gravatar"
|
GravatarProvider string = "gravatar"
|
||||||
@@ -115,6 +120,9 @@ const (
|
|||||||
defaultLogMode string = "console"
|
defaultLogMode string = "console"
|
||||||
defaultLoglevel Level = LOGLEVEL_INFO
|
defaultLoglevel Level = LOGLEVEL_INFO
|
||||||
|
|
||||||
|
defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes
|
||||||
|
defaultDuplicateSubmissionsInterval uint32 = 300 // 5 minutes
|
||||||
|
|
||||||
defaultSecretKey string = "ezbookkeeping"
|
defaultSecretKey string = "ezbookkeeping"
|
||||||
defaultTokenExpiredTime uint32 = 604800 // 7 days
|
defaultTokenExpiredTime uint32 = 604800 // 7 days
|
||||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||||
@@ -196,6 +204,14 @@ type Config struct {
|
|||||||
UuidGeneratorType string
|
UuidGeneratorType string
|
||||||
UuidServerId uint8
|
UuidServerId uint8
|
||||||
|
|
||||||
|
// Duplicate Checker
|
||||||
|
DuplicateCheckerType string
|
||||||
|
InMemoryDuplicateCheckerCleanupInterval uint32
|
||||||
|
InMemoryDuplicateCheckerCleanupIntervalDuration time.Duration
|
||||||
|
EnableDuplicateSubmissionsCheck bool
|
||||||
|
DuplicateSubmissionsInterval uint32
|
||||||
|
DuplicateSubmissionsIntervalDuration time.Duration
|
||||||
|
|
||||||
// Secret
|
// Secret
|
||||||
SecretKeyNoSet bool
|
SecretKeyNoSet bool
|
||||||
SecretKey string
|
SecretKey string
|
||||||
@@ -297,6 +313,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = loadDuplicateCheckerConfiguration(config, cfgFile, "duplicate_checker")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = loadSecurityConfiguration(config, cfgFile, "security")
|
err = loadSecurityConfiguration(config, cfgFile, "security")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -487,6 +509,30 @@ func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDuplicateCheckerConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||||
|
if getConfigItemStringValue(configFile, sectionName, "checker_type") == InMemoryDuplicateCheckerType {
|
||||||
|
config.DuplicateCheckerType = InMemoryDuplicateCheckerType
|
||||||
|
} else {
|
||||||
|
return errs.ErrInvalidDuplicateCheckerType
|
||||||
|
}
|
||||||
|
|
||||||
|
config.InMemoryDuplicateCheckerCleanupInterval = getConfigItemUint32Value(configFile, sectionName, "cleanup_interval", defaultInMemoryDuplicateCheckerCleanupInterval)
|
||||||
|
config.InMemoryDuplicateCheckerCleanupIntervalDuration = time.Duration(config.InMemoryDuplicateCheckerCleanupInterval) * time.Second
|
||||||
|
|
||||||
|
duplicateSubmissionsInterval := getConfigItemUint32Value(configFile, sectionName, "duplicate_submissions_interval", defaultDuplicateSubmissionsInterval)
|
||||||
|
|
||||||
|
config.EnableDuplicateSubmissionsCheck = duplicateSubmissionsInterval > 0
|
||||||
|
|
||||||
|
if duplicateSubmissionsInterval < 1 {
|
||||||
|
duplicateSubmissionsInterval = defaultDuplicateSubmissionsInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DuplicateSubmissionsInterval = duplicateSubmissionsInterval
|
||||||
|
config.DuplicateSubmissionsIntervalDuration = time.Duration(config.DuplicateSubmissionsInterval) * time.Second
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||||
config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key")
|
config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key")
|
||||||
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
|
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
|
||||||
|
|||||||
+25
-1
@@ -2,6 +2,8 @@ import Clipboard from 'clipboard';
|
|||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import uaParser from 'ua-parser-js';
|
import uaParser from 'ua-parser-js';
|
||||||
|
|
||||||
|
import { base64encode } from './common.js';
|
||||||
|
|
||||||
export function asyncLoadAssets(type, assetUrl) {
|
export function asyncLoadAssets(type, assetUrl) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
let addElement = false;
|
let addElement = false;
|
||||||
@@ -71,10 +73,32 @@ export function asyncLoadAssets(type, assetUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateRandomString() {
|
export function generateRandomString() {
|
||||||
const baseString = 'ebk_' + Math.round(new Date().getTime() / 1000) + '_' + Math.random();
|
let baseString = 'ebk_' + new Date().getTime();
|
||||||
|
|
||||||
|
if (crypto && crypto.getRandomValues) {
|
||||||
|
const randoms = new Uint8Array(256);
|
||||||
|
crypto.getRandomValues(randoms);
|
||||||
|
baseString += '_' + base64encode(randoms);
|
||||||
|
} else {
|
||||||
|
baseString += '_' + Math.random();
|
||||||
|
}
|
||||||
|
|
||||||
return CryptoJS.SHA256(baseString).toString();
|
return CryptoJS.SHA256(baseString).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRandomUUID() {
|
||||||
|
const randomString = generateRandomString();
|
||||||
|
|
||||||
|
// convert hash string to UUID Version 8
|
||||||
|
const uuid = randomString.substring(0, 8) + '-'
|
||||||
|
+ randomString.substring(8, 12) + '-'
|
||||||
|
+ '8' + randomString.substring(13, 16) + '-'
|
||||||
|
+ (0x8 | (parseInt(randomString.charAt(16), 16) & 0x3)).toString(16) + randomString.substring(17, 20) + '-'
|
||||||
|
+ randomString.substring(20, 32);
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseUserAgent(ua) {
|
export function parseUserAgent(ua) {
|
||||||
const uaParseRet = uaParser(ua);
|
const uaParseRet = uaParser(ua);
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -238,7 +238,7 @@ export default {
|
|||||||
getAccount: ({ id }) => {
|
getAccount: ({ id }) => {
|
||||||
return axios.get('v1/accounts/get.json?id=' + id);
|
return axios.get('v1/accounts/get.json?id=' + id);
|
||||||
},
|
},
|
||||||
addAccount: ({ category, type, name, icon, color, currency, balance, comment, subAccounts }) => {
|
addAccount: ({ category, type, name, icon, color, currency, balance, comment, subAccounts, clientSessionId }) => {
|
||||||
return axios.post('v1/accounts/add.json', {
|
return axios.post('v1/accounts/add.json', {
|
||||||
category,
|
category,
|
||||||
type,
|
type,
|
||||||
@@ -248,7 +248,8 @@ export default {
|
|||||||
currency,
|
currency,
|
||||||
balance,
|
balance,
|
||||||
comment,
|
comment,
|
||||||
subAccounts
|
subAccounts,
|
||||||
|
clientSessionId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
modifyAccount: ({ id, category, name, icon, color, comment, hidden, subAccounts }) => {
|
modifyAccount: ({ id, category, name, icon, color, comment, hidden, subAccounts }) => {
|
||||||
@@ -383,7 +384,7 @@ export default {
|
|||||||
getTransaction: ({ id }) => {
|
getTransaction: ({ id }) => {
|
||||||
return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`);
|
return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`);
|
||||||
},
|
},
|
||||||
addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => {
|
addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset, clientSessionId }) => {
|
||||||
return axios.post('v1/transactions/add.json', {
|
return axios.post('v1/transactions/add.json', {
|
||||||
type,
|
type,
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -396,7 +397,8 @@ export default {
|
|||||||
tagIds,
|
tagIds,
|
||||||
comment,
|
comment,
|
||||||
geoLocation,
|
geoLocation,
|
||||||
utcOffset
|
utcOffset,
|
||||||
|
clientSessionId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => {
|
modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => {
|
||||||
@@ -427,14 +429,15 @@ export default {
|
|||||||
getTransactionCategory: ({ id }) => {
|
getTransactionCategory: ({ id }) => {
|
||||||
return axios.get('v1/transaction/categories/get.json?id=' + id);
|
return axios.get('v1/transaction/categories/get.json?id=' + id);
|
||||||
},
|
},
|
||||||
addTransactionCategory: ({ name, type, parentId, icon, color, comment }) => {
|
addTransactionCategory: ({ name, type, parentId, icon, color, comment, clientSessionId }) => {
|
||||||
return axios.post('v1/transaction/categories/add.json', {
|
return axios.post('v1/transaction/categories/add.json', {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
parentId,
|
parentId,
|
||||||
icon,
|
icon,
|
||||||
color,
|
color,
|
||||||
comment
|
comment,
|
||||||
|
clientSessionId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addTransactionCategoryBatch: ({ categories }) => {
|
addTransactionCategoryBatch: ({ categories }) => {
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ export const useAccountsStore = defineStore('accounts', {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveAccount({ account, subAccounts, isEdit }) {
|
saveAccount({ account, subAccounts, isEdit, clientSessionId }) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const submitSubAccounts = [];
|
const submitSubAccounts = [];
|
||||||
@@ -776,6 +776,10 @@ export const useAccountsStore = defineStore('accounts', {
|
|||||||
subAccounts: account.type === accountConstants.allAccountTypes.SingleAccount ? null : submitSubAccounts,
|
subAccounts: account.type === accountConstants.allAccountTypes.SingleAccount ? null : submitSubAccounts,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (clientSessionId) {
|
||||||
|
submitAccount.clientSessionId = clientSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
submitAccount.id = account.id;
|
submitAccount.id = account.id;
|
||||||
submitAccount.hidden = !account.visible;
|
submitAccount.hidden = !account.visible;
|
||||||
|
|||||||
@@ -745,7 +745,7 @@ export const useTransactionsStore = defineStore('transactions', {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveTransaction({ transaction, defaultCurrency, isEdit }) {
|
saveTransaction({ transaction, defaultCurrency, isEdit, clientSessionId }) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const exchangeRatesStore = useExchangeRatesStore();
|
const exchangeRatesStore = useExchangeRatesStore();
|
||||||
@@ -764,6 +764,10 @@ export const useTransactionsStore = defineStore('transactions', {
|
|||||||
utcOffset: transaction.utcOffset
|
utcOffset: transaction.utcOffset
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (clientSessionId) {
|
||||||
|
submitTransaction.clientSessionId = clientSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
if (transaction.type === transactionConstants.allTransactionTypes.Expense) {
|
if (transaction.type === transactionConstants.allTransactionTypes.Expense) {
|
||||||
submitTransaction.categoryId = transaction.expenseCategory;
|
submitTransaction.categoryId = transaction.expenseCategory;
|
||||||
} else if (transaction.type === transactionConstants.allTransactionTypes.Income) {
|
} else if (transaction.type === transactionConstants.allTransactionTypes.Income) {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export const useTransactionCategoriesStore = defineStore('transactionCategories'
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveCategory({ category, isEdit }) {
|
saveCategory({ category, isEdit, clientSessionId }) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const submitCategory = {
|
const submitCategory = {
|
||||||
@@ -269,6 +269,10 @@ export const useTransactionCategoriesStore = defineStore('transactionCategories'
|
|||||||
comment: category.comment
|
comment: category.comment
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (clientSessionId) {
|
||||||
|
submitCategory.clientSessionId = clientSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
submitCategory.id = category.id;
|
submitCategory.id = category.id;
|
||||||
submitCategory.hidden = !category.visible;
|
submitCategory.hidden = !category.visible;
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ import accountConstants from '@/consts/account.js';
|
|||||||
import iconConstants from '@/consts/icon.js';
|
import iconConstants from '@/consts/icon.js';
|
||||||
import colorConstants from '@/consts/color.js';
|
import colorConstants from '@/consts/color.js';
|
||||||
import { isNumber } from '@/lib/common.js';
|
import { isNumber } from '@/lib/common.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
setAccountModelByAnotherAccount,
|
setAccountModelByAnotherAccount,
|
||||||
setAccountSuitableIcon
|
setAccountSuitableIcon
|
||||||
@@ -208,6 +209,7 @@ export default {
|
|||||||
showState: false,
|
showState: false,
|
||||||
activeTab: 'account',
|
activeTab: 'account',
|
||||||
editAccountId: null,
|
editAccountId: null,
|
||||||
|
clientSessionId: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
account: newAccount,
|
account: newAccount,
|
||||||
subAccounts: [],
|
subAccounts: [],
|
||||||
@@ -314,6 +316,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.editAccountId = null;
|
self.editAccountId = null;
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +373,8 @@ export default {
|
|||||||
self.accountsStore.saveAccount({
|
self.accountsStore.saveAccount({
|
||||||
account: self.account,
|
account: self.account,
|
||||||
subAccounts: self.subAccounts,
|
subAccounts: self.subAccounts,
|
||||||
isEdit: !!self.editAccountId
|
isEdit: !!self.editAccountId,
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js';
|
|||||||
import categoryConstants from '@/consts/category.js';
|
import categoryConstants from '@/consts/category.js';
|
||||||
import iconConstants from '@/consts/icon.js';
|
import iconConstants from '@/consts/icon.js';
|
||||||
import colorConstants from '@/consts/color.js';
|
import colorConstants from '@/consts/color.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
setCategoryModelByAnotherCategory,
|
setCategoryModelByAnotherCategory,
|
||||||
allVisiblePrimaryTransactionCategoriesByType
|
allVisiblePrimaryTransactionCategoriesByType
|
||||||
@@ -121,6 +122,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
showState: false,
|
showState: false,
|
||||||
editCategoryId: null,
|
editCategoryId: null,
|
||||||
|
clientSessionId: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
category: newTransactionCategory,
|
category: newTransactionCategory,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
@@ -220,6 +222,7 @@ export default {
|
|||||||
self.category.type = categoryType;
|
self.category.type = categoryType;
|
||||||
self.category.parentId = options.parentId;
|
self.category.parentId = options.parentId;
|
||||||
|
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +245,8 @@ export default {
|
|||||||
|
|
||||||
self.transactionCategoriesStore.saveCategory({
|
self.transactionCategoriesStore.saveCategory({
|
||||||
category: self.category,
|
category: self.category,
|
||||||
isEdit: !!self.editCategoryId
|
isEdit: !!self.editCategoryId,
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ import {
|
|||||||
getTimezoneOffsetMinutes,
|
getTimezoneOffsetMinutes,
|
||||||
getCurrentUnixTime
|
getCurrentUnixTime
|
||||||
} from '@/lib/datetime.js';
|
} from '@/lib/datetime.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
getFirstAvailableCategoryId
|
getFirstAvailableCategoryId
|
||||||
} from '@/lib/category.js';
|
} from '@/lib/category.js';
|
||||||
@@ -370,6 +371,7 @@ export default {
|
|||||||
activeTab: 'basicInfo',
|
activeTab: 'basicInfo',
|
||||||
editTransactionId: null,
|
editTransactionId: null,
|
||||||
originalTransactionEditable: false,
|
originalTransactionEditable: false,
|
||||||
|
clientSessionId: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
transaction: newTransaction,
|
transaction: newTransaction,
|
||||||
geoLocationStatus: null,
|
geoLocationStatus: null,
|
||||||
@@ -642,6 +644,10 @@ export default {
|
|||||||
self.transaction.type = parseInt(options.type);
|
self.transaction.type = parseInt(options.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.mode === 'add') {
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(promises).then(function (responses) {
|
Promise.all(promises).then(function (responses) {
|
||||||
if (self.editTransactionId && !responses[3]) {
|
if (self.editTransactionId && !responses[3]) {
|
||||||
if (self.reject) {
|
if (self.reject) {
|
||||||
@@ -691,7 +697,8 @@ export default {
|
|||||||
self.transactionsStore.saveTransaction({
|
self.transactionsStore.saveTransaction({
|
||||||
transaction: self.transaction,
|
transaction: self.transaction,
|
||||||
defaultCurrency: self.defaultCurrency,
|
defaultCurrency: self.defaultCurrency,
|
||||||
isEdit: self.mode === 'edit'
|
isEdit: self.mode === 'edit',
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
|
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ import iconConstants from '@/consts/icon.js';
|
|||||||
import colorConstants from '@/consts/color.js';
|
import colorConstants from '@/consts/color.js';
|
||||||
import transactionConstants from '@/consts/transaction.js';
|
import transactionConstants from '@/consts/transaction.js';
|
||||||
import { getNameByKeyValue } from '@/lib/common.js';
|
import { getNameByKeyValue } from '@/lib/common.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
setAccountModelByAnotherAccount,
|
setAccountModelByAnotherAccount,
|
||||||
setAccountSuitableIcon
|
setAccountSuitableIcon
|
||||||
@@ -447,6 +448,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
editAccountId: null,
|
editAccountId: null,
|
||||||
|
clientSessionId: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingError: null,
|
loadingError: null,
|
||||||
account: newAccount,
|
account: newAccount,
|
||||||
@@ -547,6 +549,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -614,7 +617,8 @@ export default {
|
|||||||
self.accountsStore.saveAccount({
|
self.accountsStore.saveAccount({
|
||||||
account: self.account,
|
account: self.account,
|
||||||
subAccounts: self.subAccounts,
|
subAccounts: self.subAccounts,
|
||||||
isEdit: !!self.editAccountId
|
isEdit: !!self.editAccountId,
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
self.$hideLoading();
|
self.$hideLoading();
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ import categoryConstants from '@/consts/category.js';
|
|||||||
import iconConstants from '@/consts/icon.js';
|
import iconConstants from '@/consts/icon.js';
|
||||||
import colorConstants from '@/consts/color.js';
|
import colorConstants from '@/consts/color.js';
|
||||||
import { getNameByKeyValue } from '@/lib/common.js';
|
import { getNameByKeyValue } from '@/lib/common.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
setCategoryModelByAnotherCategory,
|
setCategoryModelByAnotherCategory,
|
||||||
allVisiblePrimaryTransactionCategoriesByType
|
allVisiblePrimaryTransactionCategoriesByType
|
||||||
@@ -172,6 +173,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
editCategoryId: null,
|
editCategoryId: null,
|
||||||
|
clientSessionId: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingError: null,
|
loadingError: null,
|
||||||
category: newTransactionCategory,
|
category: newTransactionCategory,
|
||||||
@@ -257,6 +259,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -280,7 +283,8 @@ export default {
|
|||||||
|
|
||||||
self.transactionCategoriesStore.saveCategory({
|
self.transactionCategoriesStore.saveCategory({
|
||||||
category: self.category,
|
category: self.category,
|
||||||
isEdit: !!self.editCategoryId
|
isEdit: !!self.editCategoryId,
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
self.$hideLoading();
|
self.$hideLoading();
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ import {
|
|||||||
getUtcOffsetByUtcOffsetMinutes,
|
getUtcOffsetByUtcOffsetMinutes,
|
||||||
getActualUnixTimeForStore
|
getActualUnixTimeForStore
|
||||||
} from '@/lib/datetime.js';
|
} from '@/lib/datetime.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import {
|
import {
|
||||||
getTransactionPrimaryCategoryName,
|
getTransactionPrimaryCategoryName,
|
||||||
getTransactionSecondaryCategoryName,
|
getTransactionSecondaryCategoryName,
|
||||||
@@ -389,6 +390,7 @@ export default {
|
|||||||
mode: 'add',
|
mode: 'add',
|
||||||
editTransactionId: null,
|
editTransactionId: null,
|
||||||
transaction: newTransaction,
|
transaction: newTransaction,
|
||||||
|
clientSessionId: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
loadingError: null,
|
loadingError: null,
|
||||||
geoLocationStatus: null,
|
geoLocationStatus: null,
|
||||||
@@ -663,6 +665,10 @@ export default {
|
|||||||
self.transaction.type = parseInt(query.type);
|
self.transaction.type = parseInt(query.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.mode === 'add') {
|
||||||
|
self.clientSessionId = generateRandomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(promises).then(function (responses) {
|
Promise.all(promises).then(function (responses) {
|
||||||
if (query.id && !responses[3]) {
|
if (query.id && !responses[3]) {
|
||||||
self.$toast('Unable to retrieve transaction');
|
self.$toast('Unable to retrieve transaction');
|
||||||
@@ -723,7 +729,8 @@ export default {
|
|||||||
self.transactionsStore.saveTransaction({
|
self.transactionsStore.saveTransaction({
|
||||||
transaction: self.transaction,
|
transaction: self.transaction,
|
||||||
defaultCurrency: self.defaultCurrency,
|
defaultCurrency: self.defaultCurrency,
|
||||||
isEdit: self.mode === 'edit'
|
isEdit: self.mode === 'edit',
|
||||||
|
clientSessionId: self.clientSessionId
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self.submitting = false;
|
self.submitting = false;
|
||||||
self.$hideLoading();
|
self.$hideLoading();
|
||||||
|
|||||||
@@ -98,6 +98,12 @@
|
|||||||
"url": "https://github.com/wk8/go-ordered-map",
|
"url": "https://github.com/wk8/go-ordered-map",
|
||||||
"licenseUrl": "https://github.com/wk8/go-ordered-map/blob/v2.1.8/LICENSE"
|
"licenseUrl": "https://github.com/wk8/go-ordered-map/blob/v2.1.8/LICENSE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "go-cache",
|
||||||
|
"copyright": "Copyright (c) 2012-2017 Patrick Mylund Nielsen and the go-cache contributors",
|
||||||
|
"url": "https://github.com/patrickmn/go-cache",
|
||||||
|
"licenseUrl": "https://github.com/patrickmn/go-cache/blob/v2.1.0/LICENSE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Testify",
|
"name": "Testify",
|
||||||
"copyright": "Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.",
|
"copyright": "Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.",
|
||||||
|
|||||||
Reference in New Issue
Block a user