tag filter supports selecting both included and excluded tags simultaneously

This commit is contained in:
MaysWind
2025-11-24 02:12:44 +08:00
parent 45be96cf68
commit 6430a52027
45 changed files with 1151 additions and 706 deletions
+8 -9
View File
@@ -24,13 +24,12 @@ type DataStatisticsResponse struct {
// ExportTransactionDataRequest represents export transaction request
type ExportTransactionDataRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Unix timestamp in seconds
MinTime int64 `form:"min_time" binding:"min=0"` // Unix timestamp in seconds
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Unix timestamp in seconds
MinTime int64 `form:"min_time" binding:"min=0"` // Unix timestamp in seconds
}
+97 -48
View File
@@ -104,6 +104,9 @@ func (t TransactionDbType) ToTransactionRelatedAccountType() (TransactionRelated
}
}
// TransactionTagFilterValue represents transaction tag filter value for no tag
const TransactionNoTagFilterValue = "none"
// TransactionTagFilterType represents transaction tag filter type
type TransactionTagFilterType byte
@@ -199,54 +202,56 @@ type TransactionImportProcessRequest struct {
ClientSessionId string `form:"client_session_id"`
}
type TransactionTagFilter struct {
TagIds []int64
Type TransactionTagFilterType
}
// TransactionCountRequest represents transaction count request
type TransactionCountRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
}
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
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"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
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"`
}
// TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request
type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
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"`
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
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"`
}
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
@@ -258,21 +263,19 @@ type TransactionReconciliationStatementRequest struct {
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionStatisticTrendsRequest represents all parameters of transaction statistic trends request
type TransactionStatisticTrendsRequest struct {
YearMonthRangeRequest
TagIds string `form:"tag_ids"`
TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionStatisticAssetTrendsRequest represents all parameters of transaction statistic asset trends request
@@ -445,6 +448,52 @@ type TransactionAmountsResponseItemAmountInfo struct {
ExpenseAmount int64 `json:"expenseAmount"`
}
// ParseTransactionTagFilter parses transaction tag filter from string
func ParseTransactionTagFilter(tagFilterStr string) ([]*TransactionTagFilter, error) {
if tagFilterStr == "" || tagFilterStr == TransactionNoTagFilterValue {
return []*TransactionTagFilter{}, nil
}
filters := strings.Split(tagFilterStr, ";")
transactionTagFilters := make([]*TransactionTagFilter, 0, len(filters))
for _, filter := range filters {
tagFilterItem := strings.Split(filter, ":")
if len(tagFilterItem) != 2 {
return nil, errs.ErrFormatInvalid
}
tagFilterType, err := utils.StringToInt(tagFilterItem[0])
if err != nil || (tagFilterType < int(TRANSACTION_TAG_FILTER_HAS_ANY) || tagFilterType > int(TRANSACTION_TAG_FILTER_NOT_HAS_ALL)) {
return nil, errs.ErrFormatInvalid
}
textualTagIds := strings.Split(tagFilterItem[1], ",")
tagIds := make([]int64, 0, len(textualTagIds))
for _, tagIdStr := range textualTagIds {
tagId, err := utils.StringToInt64(tagIdStr)
if err != nil {
return nil, errs.ErrTransactionTagIdInvalid
}
tagIds = append(tagIds, tagId)
}
transactionTagFilter := &TransactionTagFilter{
TagIds: tagIds,
Type: TransactionTagFilterType(tagFilterType),
}
transactionTagFilters = append(transactionTagFilters, transactionTagFilter)
}
return transactionTagFilters, nil
}
// IsEditable returns whether this transaction can be edited
func (t *Transaction) IsEditable(currentUser *User, utcOffset int16, account *Account, relatedAccount *Account) bool {
if currentUser == nil || !currentUser.CanEditTransactionByTransactionTime(t.TransactionTime, utcOffset) {
+95
View File
@@ -9,6 +9,101 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
func TestParseTransactionTagFilter_EmptyTagFilter(t *testing.T) {
actualValue, err := ParseTransactionTagFilter("")
assert.Nil(t, err)
assert.Equal(t, 0, len(actualValue))
}
func TestParseTransactionTagFilter_NoTag(t *testing.T) {
actualValue, err := ParseTransactionTagFilter("none")
assert.Nil(t, err)
assert.Equal(t, 0, len(actualValue))
}
func TestParseTransactionTagFilter_NoValidFilter(t *testing.T) {
_, err := ParseTransactionTagFilter(";")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
_, err = ParseTransactionTagFilter(";;")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
}
func TestParseTransactionTagFilter_ValidOneFilterInTagFilters(t *testing.T) {
actualValue, err := ParseTransactionTagFilter("0:1")
assert.Nil(t, err)
assert.Equal(t, 1, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type)
assert.Equal(t, 1, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1}, actualValue[0].TagIds)
actualValue, err = ParseTransactionTagFilter("0:1,2,3")
assert.Nil(t, err)
assert.Equal(t, 1, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type)
assert.Equal(t, 3, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds)
actualValue, err = ParseTransactionTagFilter("1:1,2,3")
assert.Nil(t, err)
assert.Equal(t, 1, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ALL, actualValue[0].Type)
assert.Equal(t, 3, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds)
actualValue, err = ParseTransactionTagFilter("2:1,2,3")
assert.Nil(t, err)
assert.Equal(t, 1, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ANY, actualValue[0].Type)
assert.Equal(t, 3, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds)
actualValue, err = ParseTransactionTagFilter("3:1,2,3")
assert.Nil(t, err)
assert.Equal(t, 1, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ALL, actualValue[0].Type)
assert.Equal(t, 3, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds)
}
func TestParseTransactionTagFilter_InvalidTagFilterType(t *testing.T) {
_, err := ParseTransactionTagFilter("a:1,2,3")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
_, err = ParseTransactionTagFilter("-1:1,2,3")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
_, err = ParseTransactionTagFilter("4:1,2,3")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
}
func TestParseTransactionTagFilter_NoTagIdsInFilter(t *testing.T) {
_, err := ParseTransactionTagFilter("0")
assert.EqualError(t, err, errs.ErrFormatInvalid.Message)
_, err = ParseTransactionTagFilter("0:")
assert.EqualError(t, err, errs.ErrTransactionTagIdInvalid.Message)
}
func TestParseTransactionTagFilter_InvalidTagIdsInFilter(t *testing.T) {
_, err := ParseTransactionTagFilter("0:abc")
assert.EqualError(t, err, errs.ErrTransactionTagIdInvalid.Message)
}
func TestParseTransactionTagFilter_ValidTwoFilterInTagFilters(t *testing.T) {
actualValue, err := ParseTransactionTagFilter("0:1,2,3;2:4,5,6")
assert.Nil(t, err)
assert.Equal(t, 2, len(actualValue))
assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type)
assert.Equal(t, 3, len(actualValue[0].TagIds))
assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds)
assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ANY, actualValue[1].Type)
assert.Equal(t, 3, len(actualValue[1].TagIds))
assert.Equal(t, []int64{4, 5, 6}, actualValue[1].TagIds)
}
func TestTransactionAmountsRequestGetTransactionAmountsRequestItems(t *testing.T) {
transactionAmountsRequest := &TransactionAmountsRequest{
Query: "name1_1234567890_1234567891|name2_1234567900_1234567901",