From 45b1cf0176d2647e5eab62bda4408d5ba3b903d9 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 13 Dec 2020 23:58:20 +0800 Subject: [PATCH] add transaction tag index --- cmd/database.go | 8 ++ pkg/api/transaction_tags.go | 2 +- pkg/api/transactions.go | 68 ++++++++++++++-- pkg/errs/transaction_tag.go | 9 ++- pkg/models/transaction_tag_index.go | 10 +++ pkg/services/transaction_tags.go | 36 ++++++++- pkg/services/transactions.go | 118 +++++++++++++++++++++++++++- pkg/utils/slices.go | 37 +++++++++ src/locales/en.js | 1 + src/locales/zh_Hans.js | 1 + 10 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 pkg/models/transaction_tag_index.go diff --git a/cmd/database.go b/cmd/database.go index 505d27cc..791b5cfa 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -107,5 +107,13 @@ func updateAllDatabaseTablesStructure() error { log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag table maintained successfully") } + err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagIndex)) + + if err != nil { + return err + } else { + log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully") + } + return nil } diff --git a/pkg/api/transaction_tags.go b/pkg/api/transaction_tags.go index 606aab29..de0f9262 100644 --- a/pkg/api/transaction_tags.go +++ b/pkg/api/transaction_tags.go @@ -203,7 +203,7 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (interface{}, *er } uid := c.GetCurrentUid() - err = a.tags.DeleteTags(uid, []int64{tagDeleteReq.Id}) + err = a.tags.DeleteTag(uid, tagDeleteReq.Id) if err != nil { log.ErrorfWithRequestId(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error()) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 59e92e6f..8e00e77d 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -8,15 +8,18 @@ import ( "github.com/mayswind/lab/pkg/log" "github.com/mayswind/lab/pkg/models" "github.com/mayswind/lab/pkg/services" + "github.com/mayswind/lab/pkg/utils" ) type TransactionsApi struct { - transactions *services.TransactionService + transactions *services.TransactionService + transactionTags *services.TransactionTagService } var ( Transactions = &TransactionsApi{ - transactions: services.Transactions, + transactions: services.Transactions, + transactionTags: services.TransactionTags, } ) @@ -43,11 +46,25 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{}, finalCount = len(transactions) } + transactionIds := make([]int64, finalCount) + + for i := 0; i < finalCount; i++ { + transactionIds[i] = transactions[i].TransactionId + } + + allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, transactionIds) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + transactionResps := &models.TransactionInfoPageWrapperResponse{} transactionResps.Items = make(models.TransactionInfoResponseSlice, finalCount) for i := 0; i < finalCount; i++ { - transactionResps.Items[i] = transactions[i].ToTransactionInfoResponse(nil) + transactionTagIds := allTransactionTagIds[transactions[i].TransactionId] + transactionResps.Items[i] = transactions[i].ToTransactionInfoResponse(transactionTagIds) } sort.Sort(transactionResps.Items) @@ -76,10 +93,24 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac return nil, errs.ErrOperationFailed } + transactionIds := make([]int64, len(transactions)) + + for i := 0; i < len(transactions); i++ { + transactionIds[i] = transactions[i].TransactionId + } + + allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, transactionIds) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + transactionResps := make([]*models.TransactionInfoResponse, len(transactions)) for i := 0; i < len(transactions); i++ { - transactionResps[i] = transactions[i].ToTransactionInfoResponse(nil) + transactionTagIds := allTransactionTagIds[transactions[i].TransactionId] + transactionResps[i] = transactions[i].ToTransactionInfoResponse(transactionTagIds) } return transactionResps, nil @@ -102,7 +133,15 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, * return nil, errs.ErrOperationFailed } - transactionResp := transaction.ToTransactionInfoResponse(nil) + allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId}) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + transactionTagIds := allTransactionTagIds[transaction.TransactionId] + transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds) return transactionResp, nil } @@ -142,7 +181,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{} uid := c.GetCurrentUid() transaction := a.createNewTransactionModel(uid, &transactionCreateReq) - err = a.transactions.CreateTransaction(transaction) + err = a.transactions.CreateTransaction(transaction, transactionCreateReq.TagIds) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error()) @@ -173,6 +212,17 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} return nil, errs.Or(err, errs.ErrOperationFailed) } + allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(uid, []int64{transaction.TransactionId}) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + transactionTagIds := allTransactionTagIds[transaction.TransactionId] + addTransactionTagIds := utils.Int64SliceMinus(transactionModifyReq.TagIds, transactionTagIds) + removeTransactionTagIds := utils.Int64SliceMinus(transactionTagIds, transactionModifyReq.TagIds) + newTransaction := &models.Transaction{ TransactionId: transaction.TransactionId, Uid: uid, @@ -191,11 +241,13 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} newTransaction.DestinationAccountId == transaction.DestinationAccountId && newTransaction.SourceAmount == transaction.SourceAmount && newTransaction.DestinationAmount == transaction.DestinationAmount && - newTransaction.Comment == transaction.Comment { + newTransaction.Comment == transaction.Comment && + len(addTransactionTagIds) < 1 && + len(removeTransactionTagIds) < 1 { return nil, errs.ErrNothingWillBeUpdated } - err = a.transactions.ModifyTransaction(newTransaction) + err = a.transactions.ModifyTransaction(newTransaction, addTransactionTagIds, removeTransactionTagIds) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to update transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error()) diff --git a/pkg/errs/transaction_tag.go b/pkg/errs/transaction_tag.go index e7d098ea..ae3f615c 100644 --- a/pkg/errs/transaction_tag.go +++ b/pkg/errs/transaction_tag.go @@ -3,8 +3,9 @@ package errs import "net/http" var ( - ErrTransactionTagIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_TAG, 0, http.StatusBadRequest, "transaction tag id is invalid") - ErrTransactionTagNotFound = NewNormalError(NORMAL_SUBCATEGORY_TAG, 1, http.StatusBadRequest, "transaction tag not found") - ErrTransactionTagNameIsEmpty = NewNormalError(NORMAL_SUBCATEGORY_TAG, 2, http.StatusBadRequest, "transaction tag name is empty") - ErrTransactionTagNameAlreadyExists = NewNormalError(NORMAL_SUBCATEGORY_TAG, 3, http.StatusBadRequest, "transaction tag name already exists") + ErrTransactionTagIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_TAG, 0, http.StatusBadRequest, "transaction tag id is invalid") + ErrTransactionTagNotFound = NewNormalError(NORMAL_SUBCATEGORY_TAG, 1, http.StatusBadRequest, "transaction tag not found") + ErrTransactionTagNameIsEmpty = NewNormalError(NORMAL_SUBCATEGORY_TAG, 2, http.StatusBadRequest, "transaction tag name is empty") + ErrTransactionTagNameAlreadyExists = NewNormalError(NORMAL_SUBCATEGORY_TAG, 3, http.StatusBadRequest, "transaction tag name already exists") + ErrTransactionTagInUseCannotBeDeleted = NewNormalError(NORMAL_SUBCATEGORY_TAG, 4, http.StatusBadRequest, "transaction tag is in use and cannot be deleted") ) diff --git a/pkg/models/transaction_tag_index.go b/pkg/models/transaction_tag_index.go new file mode 100644 index 00000000..a3153506 --- /dev/null +++ b/pkg/models/transaction_tag_index.go @@ -0,0 +1,10 @@ +package models + +type TransactionTagIndex struct { + Uid int64 `xorm:"PK INDEX(IDX_transaction_tag_index_uid_tag_id_transaction_time) INDEX(IDX_transaction_tag_index_uid_transaction_id)"` + TagId int64 `xorm:"PK INDEX(IDX_transaction_tag_index_uid_tag_id_transaction_time)"` + TransactionId int64 `xorm:"PK INDEX(IDX_transaction_tag_index_uid_transaction_id)"` + TransactionTime int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_tag_id_transaction_time) NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 +} diff --git a/pkg/services/transaction_tags.go b/pkg/services/transaction_tags.go index 9ce58330..8e2cefcf 100644 --- a/pkg/services/transaction_tags.go +++ b/pkg/services/transaction_tags.go @@ -78,6 +78,32 @@ func (s *TransactionTagService) GetMaxDisplayOrder(uid int64) (int, error) { } } +func (s *TransactionTagService) GetAllTagIdsOfTransactions(uid int64, transactionIds []int64) (map[int64][]int64, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + var tagIndexs []*models.TransactionTagIndex + err := s.UserDataDB(uid).Where("uid=?", uid).In("transaction_id", transactionIds).Find(&tagIndexs) + + allTransactionTagIds := make(map[int64][]int64) + + for i := 0; i < len(tagIndexs); i++ { + tagIndex := tagIndexs[i] + + var transactionTagIds []int64 + + if _, exists := allTransactionTagIds[tagIndex.TransactionId]; exists { + transactionTagIds = allTransactionTagIds[tagIndex.TransactionId] + } + + transactionTagIds = append(transactionTagIds, tagIndex.TagId) + allTransactionTagIds[tagIndex.TransactionId] = transactionTagIds + } + + return allTransactionTagIds, err +} + func (s *TransactionTagService) CreateTag(tag *models.TransactionTag) error { if tag.Uid <= 0 { return errs.ErrUserIdInvalid @@ -180,13 +206,19 @@ func (s *TransactionTagService) ModifyTagDisplayOrders(uid int64, tags []*models }) } -func (s *TransactionTagService) DeleteTags(uid int64, ids []int64) error { +func (s *TransactionTagService) DeleteTag(uid int64, tagId int64) error { if uid <= 0 { return errs.ErrUserIdInvalid } return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { - deletedRows, err := sess.In("tag_id", ids).Where("uid=?", uid).Delete(&models.TransactionTag{}) + exists, err := sess.Cols("uid", "tag_id").Where("uid=? AND tag_id=?", uid, tagId).Limit(1).Exist(&models.TransactionTagIndex{}) + + if exists { + return errs.ErrTransactionTagInUseCannotBeDeleted + } + + deletedRows, err := sess.ID(tagId).Where("uid=?", uid).Delete(&models.TransactionTag{}) if err != nil { return err diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index d73751f1..c584267c 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -122,7 +122,7 @@ func (s *TransactionService) GetMonthTransactionCount(uid int64, year int64, mon return s.UserDataDB(uid).Where("uid=? AND deleted=? AND transaction_time>=? AND transaction_time 0 { + var tags []*models.TransactionTag + err := sess.Where("uid=? AND deleted=?", transaction.Uid, false).In("tag_ids", tagIds).Find(&tags) + + if err != nil { + return err + } + + tagMap := make(map[int64]*models.TransactionTag) + + for i := 0; i < len(tags); i++ { + tagMap[tags[i].TagId] = tags[i] + } + + for i := 0; i < len(transactionTagIndexs); i++ { + if _, exists := tagMap[transactionTagIndexs[i].TagId]; !exists { + return errs.ErrTransactionTagNotFound + } + } + } + // Verify balance modification transaction and calculate real amount if transaction.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE { otherTransactionExists, err := sess.Where("uid=? AND deleted=? AND destination_account_id=?", transaction.Uid, false, destinationAccount.AccountId).Limit(1).Exist(&models.Transaction{}) @@ -245,6 +282,18 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction) } } + // Insert transaction tag index + if len(transactionTagIndexs) > 0 { + for i := 0; i < len(transactionTagIndexs); i++ { + transactionTagIndex := transactionTagIndexs[i] + _, err := sess.Insert(transactionTagIndex) + + if err != nil { + return err + } + } + } + // Update account table if transaction.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE { destinationAccount.UpdatedUnixTime = time.Now().Unix() @@ -297,7 +346,7 @@ func (s *TransactionService) CreateTransaction(transaction *models.Transaction) }) } -func (s *TransactionService) ModifyTransaction(transaction *models.Transaction) error { +func (s *TransactionService) ModifyTransaction(transaction *models.Transaction, addTagIds []int64, removeTagIds []int64) error { if transaction.Uid <= 0 { return errs.ErrUserIdInvalid } @@ -310,6 +359,21 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction) transaction.UpdatedUnixTime = now updateCols = append(updateCols, "updated_unix_time") + addTagIds = utils.ToUniqueInt64Slice(addTagIds) + removeTagIds = utils.ToUniqueInt64Slice(removeTagIds) + + transactionTagIndexs := make([]*models.TransactionTagIndex, len(addTagIds)) + + for i := 0; i < len(addTagIds); i++ { + transactionTagIndexs[i] = &models.TransactionTagIndex{ + Uid: transaction.Uid, + TagId: addTagIds[i], + TransactionId: transaction.TransactionId, + CreatedUnixTime: now, + UpdatedUnixTime: now, + } + } + err := s.UserDB().DoTransaction(func(sess *xorm.Session) error { // Get and verify current transaction oldTransaction := &models.Transaction{} @@ -435,6 +499,28 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction) updateCols = append(updateCols, "comment") } + // Get and verify tags + if len(transactionTagIndexs) > 0 { + var tags []*models.TransactionTag + err := sess.Where("uid=? AND deleted=?", transaction.Uid, false).In("tag_ids", addTagIds).Find(&tags) + + if err != nil { + return err + } + + tagMap := make(map[int64]*models.TransactionTag) + + for i := 0; i < len(tags); i++ { + tagMap[tags[i].TagId] = tags[i] + } + + for i := 0; i < len(transactionTagIndexs); i++ { + if _, exists := tagMap[transactionTagIndexs[i].TagId]; !exists { + return errs.ErrTransactionTagNotFound + } + } + } + // Update transaction row updatedRows, err := sess.ID(transaction.TransactionId).Cols(updateCols...).Where("uid=? AND deleted=?", transaction.Uid, false).Update(transaction) @@ -444,6 +530,30 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction) return errs.ErrTransactionNotFound } + // Update transaction tag index + if len(removeTagIds) > 0 { + deletedRows, err := sess.Where("uid=?", transaction.Uid).In("tag_id", removeTagIds).Delete(&models.TransactionTagIndex{}) + + if err != nil { + return err + } else if deletedRows < 1 { + return errs.ErrTransactionTagNotFound + } + + return err + } + + if len(transactionTagIndexs) > 0 { + for i := 0; i < len(transactionTagIndexs); i++ { + transactionTagIndex := transactionTagIndexs[i] + _, err := sess.Insert(transactionTagIndex) + + if err != nil { + return err + } + } + } + // Update account table if oldTransaction.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE { if transaction.SourceAccountId != oldTransaction.SourceAccountId { diff --git a/pkg/utils/slices.go b/pkg/utils/slices.go index a127d24f..dc2e40cc 100644 --- a/pkg/utils/slices.go +++ b/pkg/utils/slices.go @@ -17,3 +17,40 @@ func IsStringSliceEuqals(s1, s2 []string) bool { return true } + +func Int64SliceMinus(s1, s2 []int64) []int64 { + if s1 == nil { + return nil + } + + s2ItemsMap := make(map[int64]bool) + var ret []int64 + + for i := 0; i < len(s2); i++ { + s2ItemsMap[s2[i]] = true + } + + for i := 0; i < len(s1); i++ { + if _, exists := s2ItemsMap[s1[i]]; !exists { + ret = append(ret, s1[i]) + } + } + + return ret +} + +func ToUniqueInt64Slice(items []int64) []int64 { + var uniqueItems []int64 + itemExistMap := make(map[int64]bool) + + for i := 0; i < len(items); i++ { + item := items[i] + + if _, exists := itemExistMap[item]; !exists { + uniqueItems = append(uniqueItems, item) + itemExistMap[item] = true + } + } + + return uniqueItems +} diff --git a/src/locales/en.js b/src/locales/en.js index 40f1f564..253062be 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -329,6 +329,7 @@ export default { 'transaction tag not found': 'Transaction tag is not found', 'transaction tag name is empty': 'Transaction tag title is empty', 'transaction tag name already exists': 'Transaction tag title already exists', + 'transaction tag is in use and cannot be deleted': 'Transaction tag is in use and it cannot be deleted', }, 'parameter': { 'id': 'ID', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 4b2310c5..d8a0b74a 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -329,6 +329,7 @@ export default { 'transaction tag not found': '交易标签不存在', 'transaction tag name is empty': '交易标签标题不能为空', 'transaction tag name already exists': '交易标签标题已经存在', + 'transaction tag is in use and cannot be deleted': '交易标签正在被使用,无法删除', }, 'parameter': { 'id': 'ID',