diff --git a/pkg/api/base.go b/pkg/api/base.go index 481f3251..1cedc645 100644 --- a/pkg/api/base.go +++ b/pkg/api/base.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "sort" "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" @@ -22,9 +23,22 @@ func (a *ApiUsingConfig) CurrentConfig() *settings.Config { } // GetTransactionPictureInfoResponse returns the view-object of transaction picture basic info according to the transaction picture model -func (a *ApiUsingConfig) GetTransactionPictureInfoResponse(picture *models.TransactionPictureInfo) *models.TransactionPictureInfoBasicResponse { - originalUrl := fmt.Sprintf(internalTransactionPictureUrlFormat, a.CurrentConfig().RootUrl, picture.PictureId, picture.PictureExtension) - return picture.ToTransactionPictureInfoBasicResponse(originalUrl) +func (a *ApiUsingConfig) GetTransactionPictureInfoResponse(pictureInfo *models.TransactionPictureInfo) *models.TransactionPictureInfoBasicResponse { + originalUrl := fmt.Sprintf(internalTransactionPictureUrlFormat, a.CurrentConfig().RootUrl, pictureInfo.PictureId, pictureInfo.PictureExtension) + return pictureInfo.ToTransactionPictureInfoBasicResponse(originalUrl) +} + +// GetTransactionPictureInfoResponseList returns the view-object list of transaction picture basic info according to the transaction picture model +func (a *ApiUsingConfig) GetTransactionPictureInfoResponseList(pictureInfos []*models.TransactionPictureInfo) models.TransactionPictureInfoBasicResponseSlice { + pictureInfoResps := make(models.TransactionPictureInfoBasicResponseSlice, len(pictureInfos)) + + for i := 0; i < len(pictureInfos); i++ { + pictureInfoResps[i] = a.GetTransactionPictureInfoResponse(pictureInfos[i]) + } + + sort.Sort(pictureInfoResps) + + return pictureInfoResps } // GetAfterRegisterNotificationContent returns the notification content displayed each time users register diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 7b7c060c..7f0de135 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -26,9 +26,9 @@ type DataManagementsApi struct { users *services.UserService accounts *services.AccountService transactions *services.TransactionService - pictures *services.TransactionPictureService categories *services.TransactionCategoryService tags *services.TransactionTagService + pictures *services.TransactionPictureService templates *services.TransactionTemplateService } @@ -44,9 +44,9 @@ var ( users: services.Users, accounts: services.Accounts, transactions: services.Transactions, - pictures: services.TransactionPictures, categories: services.TransactionCategories, tags: services.TransactionTags, + pictures: services.TransactionPictures, templates: services.TransactionTemplates, } ) @@ -158,13 +158,6 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er return nil, errs.Or(err, errs.ErrOperationFailed) } - err = a.pictures.DeleteAllPictures(c, uid) - - if err != nil { - log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction pictures, because %s", err.Error()) - return nil, errs.Or(err, errs.ErrOperationFailed) - } - err = a.transactions.DeleteAllTransactions(c, uid) if err != nil { diff --git a/pkg/api/transaction_pictures.go b/pkg/api/transaction_pictures.go index 0a81988a..33381fc0 100644 --- a/pkg/api/transaction_pictures.go +++ b/pkg/api/transaction_pictures.go @@ -147,6 +147,7 @@ func (a *TransactionPicturesApi) TransactionPictureGetHandler(c *core.WebContext func (a *TransactionPicturesApi) createNewPictureInfoModel(uid int64, fileExtension string, clientIp string) *models.TransactionPictureInfo { return &models.TransactionPictureInfo{ Uid: uid, + TransactionId: models.TransactionPictureNewPictureTransactionId, PictureExtension: fileExtension, CreatedIp: clientIp, } diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 569c77f0..c4dd3154 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -23,6 +23,7 @@ type TransactionsApi struct { transactions *services.TransactionService transactionCategories *services.TransactionCategoryService transactionTags *services.TransactionTagService + transactionPictures *services.TransactionPictureService accounts *services.AccountService users *services.UserService } @@ -39,6 +40,7 @@ var ( transactions: services.Transactions, transactionCategories: services.TransactionCategories, transactionTags: services.TransactionTags, + transactionPictures: services.TransactionPictures, accounts: services.Accounts, users: services.Users, } @@ -177,7 +179,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs transactions = transactions[:transactionListReq.Count] } - transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) + transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) if err != nil { log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) @@ -260,7 +262,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any, return nil, errs.Or(err, errs.ErrOperationFailed) } - transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) + transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) if err != nil { log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) @@ -567,6 +569,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs. var category *models.TransactionCategory var tagMap map[int64]*models.TransactionTag + var pictureInfos []*models.TransactionPictureInfo if !transactionGetReq.TrimCategory { category, err = a.transactionCategories.GetCategoryByCategoryId(c, uid, transaction.CategoryId) @@ -586,6 +589,15 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs. } } + if transactionGetReq.WithPictures { + pictureInfos, err = a.transactionPictures.GetPictureInfosByTransactionId(c, uid, transaction.TransactionId) + + if err != nil { + log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + } + transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId]) transactionTagIds := allTransactionTagIds[transaction.TransactionId] transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable) @@ -610,6 +622,10 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs. transactionResp.Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap) } + if transactionGetReq.WithPictures { + transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos) + } + return transactionResp, nil } @@ -630,6 +646,13 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er return nil, errs.ErrTransactionTagIdInvalid } + pictureIds, err := utils.StringArrayToInt64Array(transactionCreateReq.PictureIds) + + if err != nil { + log.Warnf(c, "[transactions.TransactionCreateHandler] parse picture ids failed, because %s", err.Error()) + return nil, errs.ErrTransactionPictureIdInvalid + } + if transactionCreateReq.Type < models.TRANSACTION_TYPE_MODIFY_BALANCE || transactionCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER { log.Warnf(c, "[transactions.TransactionCreateHandler] transaction type is invalid") return nil, errs.ErrTransactionTypeInvalid @@ -671,6 +694,24 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime } + var pictureInfos []*models.TransactionPictureInfo + + if len(pictureIds) > 0 { + pictureInfos, err = a.transactionPictures.GetNewPictureInfosByPictureIds(c, uid, pictureIds) + + if err != nil { + log.Errorf(c, "[transactions.TransactionCreateHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + notExistsPictureIds := utils.Int64SliceMinus(pictureIds, a.transactionPictures.GetTransactionPictureIds(pictureInfos)) + + if len(notExistsPictureIds) > 0 { + log.Errorf(c, "[transactions.TransactionCreateHandler] some pictures \"ids:%s\" does not exists for user \"uid:%d\"", strings.Join(utils.Int64ArrayToStringArray(notExistsPictureIds), ","), uid) + return nil, errs.ErrTransactionPictureNotFound + } + } + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" { found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId) @@ -687,13 +728,14 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er } transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable) + transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos) return transactionResp, nil } } } - err = a.transactions.CreateTransaction(c, transaction, tagIds) + err = a.transactions.CreateTransaction(c, transaction, tagIds, pictureIds) if err != nil { log.Errorf(c, "[transactions.TransactionCreateHandler] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error()) @@ -704,6 +746,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId)) transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable) + transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos) return transactionResp, nil } @@ -725,6 +768,13 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er return nil, errs.ErrTransactionTagIdInvalid } + pictureIds, err := utils.StringArrayToInt64Array(transactionModifyReq.PictureIds) + + if err != nil { + log.Warnf(c, "[transactions.TransactionModifyHandler] parse picture ids failed, because %s", err.Error()) + return nil, errs.ErrTransactionPictureIdInvalid + } + uid := c.GetCurrentUid() user, err := a.users.GetUserById(c, uid) @@ -761,6 +811,15 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er transactionTagIds = make([]int64, 0, 0) } + transactionPictureInfos, err := a.transactionPictures.GetPictureInfosByTransactionId(c, uid, transaction.TransactionId) + + if err != nil { + log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transaction picture infos for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + transactionPictureIds := a.transactionPictures.GetTransactionPictureIds(transactionPictureInfos) + newTransaction := &models.Transaction{ TransactionId: transaction.TransactionId, Uid: uid, @@ -794,10 +853,18 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er newTransaction.Comment == transaction.Comment && newTransaction.GeoLongitude == transaction.GeoLongitude && newTransaction.GeoLatitude == transaction.GeoLatitude && - utils.Int64SliceEquals(tagIds, transactionTagIds) { + utils.Int64SliceEquals(tagIds, transactionTagIds) && + utils.Int64SliceEquals(pictureIds, transactionPictureIds) { return nil, errs.ErrNothingWillBeUpdated } + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset) + newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset) + + if !transactionEditable || !newTransactionEditable { + return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime + } + var addTransactionTagIds []int64 var removeTransactionTagIds []int64 @@ -806,14 +873,46 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er addTransactionTagIds = tagIds } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset) - newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset) + addTransactionPictureIds := utils.Int64SliceMinus(pictureIds, transactionPictureIds) + removeTransactionPictureIds := utils.Int64SliceMinus(transactionPictureIds, pictureIds) + var newPictureInfos []*models.TransactionPictureInfo - if !transactionEditable || !newTransactionEditable { - return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime + if !utils.Int64SliceEquals(pictureIds, transactionPictureIds) { + oldAndNewPictureIds := transactionPictureIds + oldAndNewPictureInfoMap := a.transactionPictures.GetPictureInfoMapByList(transactionPictureInfos) + + if len(addTransactionPictureIds) > 0 { + addPictureInfos, err := a.transactionPictures.GetNewPictureInfosByPictureIds(c, uid, addTransactionPictureIds) + + if err != nil { + log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + oldAndNewPictureIds = append(oldAndNewPictureIds, a.transactionPictures.GetTransactionPictureIds(addPictureInfos)...) + notExistsPictureIds := utils.Int64SliceMinus(pictureIds, oldAndNewPictureIds) + + if len(notExistsPictureIds) > 0 { + log.Errorf(c, "[transactions.TransactionModifyHandler] some pictures \"ids:%s\" does not exists for user \"uid:%d\"", strings.Join(utils.Int64ArrayToStringArray(notExistsPictureIds), ","), uid) + return nil, errs.ErrTransactionPictureNotFound + } + + for i := 0; i < len(addPictureInfos); i++ { + oldAndNewPictureInfoMap[addPictureInfos[i].PictureId] = addPictureInfos[i] + } + } + + for i := 0; i < len(pictureIds); i++ { + pictureId := pictureIds[i] + pictureInfo, exists := oldAndNewPictureInfoMap[pictureId] + + if exists { + newPictureInfos = append(newPictureInfos, pictureInfo) + } + } } - err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), addTransactionTagIds, removeTransactionTagIds) + err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), addTransactionTagIds, removeTransactionTagIds, addTransactionPictureIds, removeTransactionPictureIds) if err != nil { log.Errorf(c, "[transactions.TransactionModifyHandler] failed to update transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error()) @@ -824,6 +923,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er newTransaction.Type = transaction.Type newTransactionResp := newTransaction.ToTransactionInfoResponse(tagIds, transactionEditable) + newTransactionResp.Pictures = a.GetTransactionPictureInfoResponseList(newPictureInfos) return newTransactionResp, nil } @@ -1053,7 +1153,7 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran return allTags } -func (a *TransactionsApi) getTransactionListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, utcOffset int16, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) { +func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, utcOffset int16, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) { uid := user.Uid transactionIds := make([]int64, len(transactions)) accountIds := make([]int64, 0, len(transactions)*2) @@ -1079,7 +1179,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.WebContext, user *mod allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds)) if err != nil { - log.Errorf(c, "[transactions.getTransactionListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error()) + log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error()) return nil, err } @@ -1088,18 +1188,19 @@ func (a *TransactionsApi) getTransactionListResult(c *core.WebContext, user *mod allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds) if err != nil { - log.Errorf(c, "[transactions.getTransactionListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) + log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) return nil, err } var categoryMap map[int64]*models.TransactionCategory var tagMap map[int64]*models.TransactionTag + var pictureInfoMap map[int64][]*models.TransactionPictureInfo if !trimCategory { categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds)) if err != nil { - log.Errorf(c, "[transactions.getTransactionListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error()) + log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error()) return nil, err } } @@ -1108,7 +1209,16 @@ func (a *TransactionsApi) getTransactionListResult(c *core.WebContext, user *mod tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.getTransactionTagIds(allTransactionTagIds))) if err != nil { - log.Errorf(c, "[transactions.getTransactionListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error()) + log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error()) + return nil, err + } + } + + if withPictures { + pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions))) + + if err != nil { + log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) return nil, err } } @@ -1145,6 +1255,14 @@ func (a *TransactionsApi) getTransactionListResult(c *core.WebContext, user *mod if !trimTag { result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap) } + + if withPictures { + pictureInfos, exists := pictureInfoMap[transaction.TransactionId] + + if exists { + result[i].Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos) + } + } } sort.Sort(result) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 6ed05da5..4bf9833b 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -73,6 +73,7 @@ type TransactionCreateRequest struct { DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` HideAmount bool `json:"hideAmount"` TagIds []string `json:"tagIds"` + PictureIds []string `json:"pictureIds"` Comment string `json:"comment" binding:"max=255"` GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"` ClientSessionId string `json:"clientSessionId"` @@ -90,6 +91,7 @@ type TransactionModifyRequest struct { DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` HideAmount bool `json:"hideAmount"` TagIds []string `json:"tagIds"` + PictureIds []string `json:"pictureIds"` Comment string `json:"comment" binding:"max=255"` GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"` } @@ -119,6 +121,7 @@ type TransactionListByMaxTimeRequest struct { Page int32 `form:"page" binding:"min=0"` Count int32 `form:"count" binding:"required,min=1,max=50"` WithCount bool `form:"with_count"` + WithPictures bool `form:"with_pictures"` TrimAccount bool `form:"trim_account"` TrimCategory bool `form:"trim_category"` TrimTag bool `form:"trim_tag"` @@ -134,6 +137,7 @@ type TransactionListInMonthByPageRequest struct { TagIds string `form:"tag_ids"` AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` Keyword string `form:"keyword"` + WithPictures bool `form:"with_pictures"` TrimAccount bool `form:"trim_account"` TrimCategory bool `form:"trim_category"` TrimTag bool `form:"trim_tag"` @@ -168,6 +172,7 @@ type TransactionAmountsRequestItem struct { // TransactionGetRequest represents all parameters of transaction getting request type TransactionGetRequest struct { Id int64 `form:"id,string" binding:"required,min=1"` + WithPictures bool `form:"with_pictures"` TrimAccount bool `form:"trim_account"` TrimCategory bool `form:"trim_category"` TrimTag bool `form:"trim_tag"` @@ -192,25 +197,26 @@ type TransactionGeoLocationResponse struct { // TransactionInfoResponse represents a view-object of transaction type TransactionInfoResponse struct { - Id int64 `json:"id,string"` - TimeSequenceId int64 `json:"timeSequenceId,string"` - Type TransactionType `json:"type"` - CategoryId int64 `json:"categoryId,string"` - Category *TransactionCategoryInfoResponse `json:"category,omitempty"` - Time int64 `json:"time"` - UtcOffset int16 `json:"utcOffset"` - SourceAccountId int64 `json:"sourceAccountId,string"` - SourceAccount *AccountInfoResponse `json:"sourceAccount,omitempty"` - DestinationAccountId int64 `json:"destinationAccountId,string,omitempty"` - DestinationAccount *AccountInfoResponse `json:"destinationAccount,omitempty"` - SourceAmount int64 `json:"sourceAmount"` - DestinationAmount int64 `json:"destinationAmount,omitempty"` - HideAmount bool `json:"hideAmount"` - TagIds []string `json:"tagIds"` - Tags []*TransactionTagInfoResponse `json:"tags,omitempty"` - Comment string `json:"comment"` - GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"` - Editable bool `json:"editable"` + Id int64 `json:"id,string"` + TimeSequenceId int64 `json:"timeSequenceId,string"` + Type TransactionType `json:"type"` + CategoryId int64 `json:"categoryId,string"` + Category *TransactionCategoryInfoResponse `json:"category,omitempty"` + Time int64 `json:"time"` + UtcOffset int16 `json:"utcOffset"` + SourceAccountId int64 `json:"sourceAccountId,string"` + SourceAccount *AccountInfoResponse `json:"sourceAccount,omitempty"` + DestinationAccountId int64 `json:"destinationAccountId,string,omitempty"` + DestinationAccount *AccountInfoResponse `json:"destinationAccount,omitempty"` + SourceAmount int64 `json:"sourceAmount"` + DestinationAmount int64 `json:"destinationAmount,omitempty"` + HideAmount bool `json:"hideAmount"` + TagIds []string `json:"tagIds"` + Tags []*TransactionTagInfoResponse `json:"tags,omitempty"` + Pictures TransactionPictureInfoBasicResponseSlice `json:"pictures,omitempty"` + Comment string `json:"comment"` + GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"` + Editable bool `json:"editable"` } // TransactionCountResponse represents transaction count response diff --git a/pkg/models/transaction_picture_info.go b/pkg/models/transaction_picture_info.go index b2ec6acd..57cf48cf 100644 --- a/pkg/models/transaction_picture_info.go +++ b/pkg/models/transaction_picture_info.go @@ -1,5 +1,7 @@ package models +const TransactionPictureNewPictureTransactionId = int64(0) + // TransactionPictureInfo represents transaction picture file info stored in database type TransactionPictureInfo struct { Uid int64 `xorm:"INDEX(IDX_transaction_picture_uid_deleted_transaction_id_picture_id) INDEX(IDX_transaction_picture_uid_deleted_picture_id) NOT NULL"` @@ -26,3 +28,21 @@ func (p *TransactionPictureInfo) ToTransactionPictureInfoBasicResponse(originalU OriginalUrl: originalUrl, } } + +// TransactionPictureInfoBasicResponseSlice represents the slice data structure of TransactionPictureInfoBasicResponse +type TransactionPictureInfoBasicResponseSlice []*TransactionPictureInfoBasicResponse + +// Len returns the count of items +func (s TransactionPictureInfoBasicResponseSlice) Len() int { + return len(s) +} + +// Swap swaps two items +func (s TransactionPictureInfoBasicResponseSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less reports whether the first item is less than the second one +func (s TransactionPictureInfoBasicResponseSlice) Less(i, j int) bool { + return s[i].PictureId < s[j].PictureId +} diff --git a/pkg/services/transaction_pictures.go b/pkg/services/transaction_pictures.go index 746dfcec..f426db08 100644 --- a/pkg/services/transaction_pictures.go +++ b/pkg/services/transaction_pictures.go @@ -71,6 +71,67 @@ func (s *TransactionPictureService) GetPictureInfoByPictureId(c core.Context, ui return pictureInfo, nil } +// GetNewPictureInfosByPictureIds returns new transaction picture info models according to transaction picture ids +func (s *TransactionPictureService) GetNewPictureInfosByPictureIds(c core.Context, uid int64, pictureIds []int64) ([]*models.TransactionPictureInfo, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if pictureIds == nil { + return nil, errs.ErrTransactionPictureIdInvalid + } + + var pictureInfos []*models.TransactionPictureInfo + err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND transaction_id=?", uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).OrderBy("picture_id asc").Find(&pictureInfos) + + if err != nil { + return nil, err + } + + return pictureInfos, nil +} + +// GetPictureInfosByTransactionId returns transaction picture info models according to transaction id +func (s *TransactionPictureService) GetPictureInfosByTransactionId(c core.Context, uid int64, transactionId int64) ([]*models.TransactionPictureInfo, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if transactionId <= 0 { + return nil, errs.ErrTransactionIdInvalid + } + + var pictureInfos []*models.TransactionPictureInfo + err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND transaction_id=?", uid, false, transactionId).OrderBy("picture_id asc").Find(&pictureInfos) + + if err != nil { + return nil, err + } + + return pictureInfos, nil +} + +// GetPictureInfosByTransactionIds returns transaction picture info models according to transaction ids +func (s *TransactionPictureService) GetPictureInfosByTransactionIds(c core.Context, uid int64, transactionIds []int64) (map[int64][]*models.TransactionPictureInfo, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if transactionIds == nil { + return nil, errs.ErrTransactionIdInvalid + } + + var pictureInfos []*models.TransactionPictureInfo + err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).OrderBy("picture_id asc").Find(&pictureInfos) + + if err != nil { + return nil, err + } + + pictureInfoMap := s.GetPictureInfoListMapByList(pictureInfos) + return pictureInfoMap, err +} + // GetPictureByPictureId returns the transaction picture data according to transaction picture id func (s *TransactionPictureService) GetPictureByPictureId(c core.Context, uid int64, pictureId int64, fileExtension string) ([]byte, error) { if uid <= 0 { @@ -150,26 +211,39 @@ func (s *TransactionPictureService) UploadPicture(c core.Context, pictureInfo *m }) } -// DeleteAllPictures deletes all existed transaction pictures from database -func (s *TransactionPictureService) DeleteAllPictures(c core.Context, uid int64) error { - if uid <= 0 { - return errs.ErrUserIdInvalid +// GetPictureInfoMapByList returns a transaction picture info list map by a list +func (s *TransactionPictureService) GetPictureInfoMapByList(pictureInfos []*models.TransactionPictureInfo) map[int64]*models.TransactionPictureInfo { + pictureInfoMap := make(map[int64]*models.TransactionPictureInfo) + + for i := 0; i < len(pictureInfos); i++ { + pictureInfo := pictureInfos[i] + pictureInfoMap[pictureInfo.PictureId] = pictureInfo } - now := time.Now().Unix() - - updateModel := &models.TransactionPictureInfo{ - Deleted: true, - DeletedUnixTime: now, - } - - return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { - _, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) - - if err != nil { - return err - } - - return nil - }) + return pictureInfoMap +} + +// GetPictureInfoListMapByList returns a transaction picture info list map by a list +func (s *TransactionPictureService) GetPictureInfoListMapByList(pictureInfos []*models.TransactionPictureInfo) map[int64][]*models.TransactionPictureInfo { + pictureInfoMap := make(map[int64][]*models.TransactionPictureInfo) + + for i := 0; i < len(pictureInfos); i++ { + pictureInfo := pictureInfos[i] + + pictureInfos, _ := pictureInfoMap[pictureInfo.TransactionId] + pictureInfoMap[pictureInfo.TransactionId] = append(pictureInfos, pictureInfo) + } + + return pictureInfoMap +} + +// GetTransactionPictureIds returns transaction picture ids list +func (s *TransactionPictureService) GetTransactionPictureIds(pictureInfos []*models.TransactionPictureInfo) []int64 { + pictureIds := make([]int64, len(pictureInfos)) + + for i := 0; i < len(pictureInfos); i++ { + pictureIds[i] = pictureInfos[i].PictureId + } + + return pictureIds } diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index f193e350..7712554e 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -203,7 +203,7 @@ func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxT } // CreateTransaction saves a new transaction to database -func (s *TransactionService) CreateTransaction(c core.Context, transaction *models.Transaction, tagIds []int64) error { +func (s *TransactionService) CreateTransaction(c core.Context, transaction *models.Transaction, tagIds []int64, pictureIds []int64) error { if transaction.Uid <= 0 { return errs.ErrUserIdInvalid } @@ -261,6 +261,11 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode } } + pictureUpdateModel := &models.TransactionPictureInfo{ + TransactionId: transaction.TransactionId, + UpdatedUnixTime: now, + } + return s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error { // Get and verify source and destination account sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction) @@ -296,6 +301,13 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode return err } + // Get and verify pictures + err = s.isPicturesValid(sess, transaction, pictureIds) + + if err != nil { + return err + } + // Verify balance modification transaction and calculate real amount if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { otherTransactionExists, err := sess.Cols("uid", "deleted", "account_id").Where("uid=? AND deleted=? AND account_id=?", transaction.Uid, false, sourceAccount.AccountId).Limit(1).Exist(&models.Transaction{}) @@ -376,6 +388,15 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode } } + // Update transaction picture + if len(pictureIds) > 0 { + _, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).Update(pictureUpdateModel) + + if err != nil { + return err + } + } + // Update account table if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { sourceAccount.UpdatedUnixTime = time.Now().Unix() @@ -543,7 +564,7 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current } tagIds := template.GetTagIds() - err = s.CreateTransaction(c, transaction, tagIds) + err = s.CreateTransaction(c, transaction, tagIds, nil) if err == nil { successCount++ @@ -560,7 +581,7 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current } // ModifyTransaction saves an existed transaction to database -func (s *TransactionService) ModifyTransaction(c core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error { +func (s *TransactionService) ModifyTransaction(c core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64, addPictureIds []int64, removePictureIds []int64) error { if transaction.Uid <= 0 { return errs.ErrUserIdInvalid } @@ -736,6 +757,13 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode return err } + // Get and verify pictures + err = s.isPicturesValid(sess, transaction, addPictureIds) + + if err != nil { + return err + } + // Update transaction row updatedRows, err := sess.ID(transaction.TransactionId).Cols(updateCols...).Where("uid=? AND deleted=?", transaction.Uid, false).Update(transaction) @@ -801,6 +829,35 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode } } + // Update transaction picture + if len(removePictureIds) > 0 { + pictureUpdateModel := &models.TransactionPictureInfo{ + Deleted: true, + DeletedUnixTime: now, + } + + deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).In("picture_id", removePictureIds).Update(pictureUpdateModel) + + if err != nil { + return err + } else if deletedRows < 1 { + return errs.ErrTransactionPictureNotFound + } + } + + if len(addPictureIds) > 0 { + pictureUpdateModel := &models.TransactionPictureInfo{ + TransactionId: transaction.TransactionId, + UpdatedUnixTime: now, + } + + _, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", addPictureIds).Update(pictureUpdateModel) + + if err != nil { + return err + } + } + // Update account table if oldTransaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { if transaction.AccountId != oldTransaction.AccountId { @@ -973,6 +1030,11 @@ func (s *TransactionService) DeleteTransaction(c core.Context, uid int64, transa DeletedUnixTime: now, } + pictureUpdateModel := &models.TransactionPictureInfo{ + Deleted: true, + DeletedUnixTime: now, + } + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { // Get and verify current transaction oldTransaction := &models.Transaction{} @@ -1025,6 +1087,13 @@ func (s *TransactionService) DeleteTransaction(c core.Context, uid int64, transa return err } + // Update transaction picture + _, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", uid, false, oldTransaction.TransactionId).Update(pictureUpdateModel) + + if err != nil { + return err + } + // Update account table if oldTransaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { sourceAccount.UpdatedUnixTime = time.Now().Unix() @@ -1097,6 +1166,11 @@ func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64) er DeletedUnixTime: now, } + pictureUpdateModel := &models.TransactionPictureInfo{ + Deleted: true, + DeletedUnixTime: now, + } + accountUpdateModel := &models.Account{ Balance: 0, Deleted: true, @@ -1118,6 +1192,13 @@ func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64) er return err } + // Update all transaction picture to deleted + _, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(pictureUpdateModel) + + if err != nil { + return err + } + // Update all account table to deleted _, err = sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(accountUpdateModel) @@ -1495,6 +1576,17 @@ func (s *TransactionService) GetTransactionMapByList(transactions []*models.Tran return transactionMap } +// GetTransactionIds returns transaction ids list +func (s *TransactionService) GetTransactionIds(transactions []*models.Transaction) []int64 { + transactionIds := make([]int64, len(transactions)) + + for i := 0; i < len(transactions); i++ { + transactionIds[i] = transactions[i].TransactionId + } + + return transactionIds +} + func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) { condition := "uid=? AND deleted=?" conditionParams := make([]any, 0, 16) @@ -1924,3 +2016,32 @@ func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models return nil } + +func (s *TransactionService) isPicturesValid(sess *xorm.Session, transaction *models.Transaction, pictureIds []int64) error { + if len(pictureIds) > 0 { + var pictureInfos []*models.TransactionPictureInfo + err := sess.Where("uid=? AND deleted=?", transaction.Uid, false).In("picture_id", pictureIds).Find(&pictureInfos) + + if err != nil { + return err + } + + pictureInfoMap := make(map[int64]*models.TransactionPictureInfo) + + for i := 0; i < len(pictureInfos); i++ { + if pictureInfos[i].TransactionId != models.TransactionPictureNewPictureTransactionId && pictureInfos[i].TransactionId != transaction.TransactionId { + return errs.ErrTransactionPictureIdInvalid + } + + pictureInfoMap[pictureInfos[i].PictureId] = pictureInfos[i] + } + + for i := 0; i < len(pictureIds); i++ { + if _, exists := pictureInfoMap[pictureIds[i]]; !exists { + return errs.ErrTransactionPictureNotFound + } + } + } + + return nil +} diff --git a/src/lib/services.js b/src/lib/services.js index d997c316..314d0eaa 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -394,9 +394,9 @@ export default { return axios.get(`v1/transactions/amounts.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&query=' + queryParams.join('|') : '')); }, 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}&with_pictures=true&trim_account=true&trim_category=true&trim_tag=true`); }, - addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset, clientSessionId }) => { + addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, pictureIds, comment, geoLocation, utcOffset, clientSessionId }) => { return axios.post('v1/transactions/add.json', { type, categoryId, @@ -407,13 +407,14 @@ export default { destinationAmount, hideAmount, tagIds, + pictureIds, comment, geoLocation, 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, pictureIds, comment, geoLocation, utcOffset }) => { return axios.post('v1/transactions/modify.json', { id, type, @@ -425,6 +426,7 @@ export default { destinationAmount, hideAmount, tagIds, + pictureIds, comment, geoLocation, utcOffset @@ -435,6 +437,12 @@ export default { id }); }, + uploadTransactionPicture: ({ pictureFile, clientSessionId }) => { + return axios.postForm('v1/transaction/pictures/upload.json', { + picture: pictureFile, + clientSessionId: clientSessionId + }); + }, getAllTransactionCategories: () => { return axios.get('v1/transaction/categories/list.json'); }, diff --git a/src/locales/en.json b/src/locales/en.json index 9873c56b..01767f25 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1414,6 +1414,7 @@ "Transaction": "Transaction", "Transactions": "Transactions", "Transaction Pictures": "Transaction Pictures", + "Pictures": "Pictures", "Add Transaction": "Add Transaction", "Edit Transaction": "Edit Transaction", "Add Transaction Template": "Add Transaction Template", @@ -1465,6 +1466,7 @@ "Unable to save transaction": "Unable to save transaction", "You have added a new transaction": "You have added a new transaction", "You have saved this transaction": "You have saved this transaction", + "Unable to upload transaction picture": "Unable to upload transaction picture", "Search transaction description": "Search transaction description", "Unable to retrieve transaction list": "Unable to retrieve transaction list", "Custom Date Range": "Custom Date Range", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 948ac20a..e7acb197 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1414,6 +1414,7 @@ "Transaction": "交易", "Transactions": "交易", "Transaction Pictures": "交易图片", + "Pictures": "图片", "Add Transaction": "添加交易", "Edit Transaction": "编辑交易", "Add Transaction Template": "添加交易模板", @@ -1465,6 +1466,7 @@ "Unable to save transaction": "无法保存交易", "You have added a new transaction": "您已经添加新交易", "You have saved this transaction": "您已经保存该交易", + "Unable to upload transaction picture": "无法上传交易图片", "Search transaction description": "搜索交易描述", "Unable to retrieve transaction list": "无法获取交易列表", "Custom Date Range": "自定义日期范围", diff --git a/src/stores/transaction.js b/src/stores/transaction.js index 8d9c0cfd..6d1b6674 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -864,6 +864,18 @@ export const useTransactionsStore = defineStore('transactions', { return Promise.reject('An error occurred'); } + if (transaction.pictures && transaction.pictures.length > 0) { + const pictureIds = []; + + for (let i = 0; i < transaction.pictures.length; i++) { + if (transaction.pictures[i].pictureId) { + pictureIds.push(transaction.pictures[i].pictureId); + } + } + + transaction.pictureIds = pictureIds; + } + if (isEdit) { submitTransaction.id = transaction.id; } @@ -991,6 +1003,30 @@ export const useTransactionsStore = defineStore('transactions', { }); }); }, + uploadTransactionPicture({ pictureFile, clientSessionId }) { + return new Promise((resolve, reject) => { + services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to upload transaction picture' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('Unable to upload transaction picture', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to upload transaction picture' }); + } else { + reject(error); + } + }); + }); + }, collapseMonthInTransactionList({ month, collapse }) { if (month) { month.opened = !collapse;