mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
support setting timezone type for the time range of statistical data
This commit is contained in:
+16
-2
@@ -238,8 +238,15 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime)
|
||||
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, utcOffset, statisticReq.UseTransactionTimezone)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
|
||||
@@ -292,6 +299,13 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
|
||||
return nil, errs.ErrQueryItemsTooMuch
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||
@@ -307,7 +321,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
|
||||
for i := 0; i < len(requestItems); i++ {
|
||||
requestItem := requestItems[i]
|
||||
|
||||
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime)
|
||||
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, utcOffset, transactionAmountsReq.UseTransactionTimezone)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
|
||||
|
||||
@@ -135,13 +135,15 @@ type TransactionListInMonthByPageRequest 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"`
|
||||
StartTime int64 `form:"start_time" binding:"min=0"`
|
||||
EndTime int64 `form:"end_time" binding:"min=0"`
|
||||
UseTransactionTimezone bool `form:"use_transaction_timezone"`
|
||||
}
|
||||
|
||||
// TransactionAmountsRequest represents all parameters of transaction amounts request
|
||||
type TransactionAmountsRequest struct {
|
||||
Query string `form:"query"`
|
||||
Query string `form:"query"`
|
||||
UseTransactionTimezone bool `form:"use_transaction_timezone"`
|
||||
}
|
||||
|
||||
// TransactionAmountsRequestItem represents an item of transaction amounts request
|
||||
|
||||
+151
-19
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||
)
|
||||
|
||||
const pageCountForLoadTransactionAmounts = 1000
|
||||
|
||||
// TransactionService represents transaction service
|
||||
type TransactionService struct {
|
||||
ServiceUsingDB
|
||||
@@ -1004,45 +1006,112 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
|
||||
}
|
||||
|
||||
// GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range
|
||||
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) {
|
||||
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) (map[int64]int64, map[int64]int64, error) {
|
||||
if uid <= 0 {
|
||||
return nil, nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
|
||||
startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
|
||||
endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
|
||||
|
||||
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
|
||||
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
|
||||
|
||||
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
|
||||
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
|
||||
|
||||
var transactionTotalAmounts []*models.Transaction
|
||||
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts)
|
||||
condition := "uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?"
|
||||
conditionParams := make([]any, 0, 4)
|
||||
conditionParams = append(conditionParams, uid)
|
||||
conditionParams = append(conditionParams, false)
|
||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
minTransactionTime := startTransactionTime
|
||||
maxTransactionTime := endTransactionTime
|
||||
var allTransactions []*models.Transaction
|
||||
|
||||
for maxTransactionTime > 0 {
|
||||
var transactions []*models.Transaction
|
||||
|
||||
finalConditionParams := make([]any, 0, 6)
|
||||
finalConditionParams = append(finalConditionParams, conditionParams...)
|
||||
finalConditionParams = append(finalConditionParams, minTransactionTime)
|
||||
finalConditionParams = append(finalConditionParams, maxTransactionTime)
|
||||
|
||||
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
allTransactions = append(allTransactions, transactions...)
|
||||
|
||||
if len(transactions) < pageCountForLoadTransactionAmounts {
|
||||
maxTransactionTime = 0
|
||||
break
|
||||
}
|
||||
|
||||
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
|
||||
}
|
||||
|
||||
incomeAmounts := make(map[int64]int64)
|
||||
expenseAmounts := make(map[int64]int64)
|
||||
|
||||
for i := 0; i < len(transactionTotalAmounts); i++ {
|
||||
transactionTotalAmount := transactionTotalAmounts[i]
|
||||
for i := 0; i < len(allTransactions); i++ {
|
||||
transaction := allTransactions[i]
|
||||
timeZone := clientLocation
|
||||
|
||||
if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
incomeAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
|
||||
} else if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
expenseAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
|
||||
if useTransactionTimezone {
|
||||
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
}
|
||||
|
||||
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
|
||||
|
||||
if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime {
|
||||
continue
|
||||
}
|
||||
|
||||
var amountsMap map[int64]int64
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
amountsMap = incomeAmounts
|
||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
amountsMap = expenseAmounts
|
||||
}
|
||||
|
||||
totalAmounts, exists := amountsMap[transaction.AccountId]
|
||||
|
||||
if !exists {
|
||||
totalAmounts = 0
|
||||
}
|
||||
|
||||
totalAmounts += transaction.Amount
|
||||
amountsMap[transaction.AccountId] = totalAmounts
|
||||
}
|
||||
|
||||
return incomeAmounts, expenseAmounts, nil
|
||||
}
|
||||
|
||||
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
|
||||
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
|
||||
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
|
||||
startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
|
||||
endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
|
||||
|
||||
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
|
||||
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
|
||||
|
||||
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
|
||||
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
|
||||
|
||||
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
|
||||
conditionParams := make([]any, 0, 8)
|
||||
conditionParams := make([]any, 0, 4)
|
||||
conditionParams = append(conditionParams, uid)
|
||||
conditionParams = append(conditionParams, false)
|
||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
||||
@@ -1050,19 +1119,82 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *co
|
||||
|
||||
if startUnixTime > 0 {
|
||||
condition = condition + " AND transaction_time>=?"
|
||||
conditionParams = append(conditionParams, utils.GetMinTransactionTimeFromUnixTime(startUnixTime))
|
||||
}
|
||||
|
||||
if endUnixTime > 0 {
|
||||
condition = condition + " AND transaction_time<=?"
|
||||
conditionParams = append(conditionParams, utils.GetMaxTransactionTimeFromUnixTime(endUnixTime))
|
||||
}
|
||||
|
||||
var transactionTotalAmounts []*models.Transaction
|
||||
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
|
||||
minTransactionTime := startTransactionTime
|
||||
maxTransactionTime := endTransactionTime
|
||||
var allTransactions []*models.Transaction
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for maxTransactionTime > 0 {
|
||||
var transactions []*models.Transaction
|
||||
|
||||
finalConditionParams := make([]any, 0, 6)
|
||||
finalConditionParams = append(finalConditionParams, conditionParams...)
|
||||
|
||||
if startUnixTime > 0 {
|
||||
finalConditionParams = append(finalConditionParams, minTransactionTime)
|
||||
}
|
||||
|
||||
if endUnixTime > 0 {
|
||||
finalConditionParams = append(finalConditionParams, maxTransactionTime)
|
||||
}
|
||||
|
||||
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allTransactions = append(allTransactions, transactions...)
|
||||
|
||||
if len(transactions) < pageCountForLoadTransactionAmounts {
|
||||
maxTransactionTime = 0
|
||||
break
|
||||
}
|
||||
|
||||
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
|
||||
}
|
||||
|
||||
transactionTotalAmountsMap := make(map[string]*models.Transaction)
|
||||
|
||||
for i := 0; i < len(allTransactions); i++ {
|
||||
transaction := allTransactions[i]
|
||||
timeZone := clientLocation
|
||||
|
||||
if useTransactionTimezone {
|
||||
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
}
|
||||
|
||||
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
|
||||
|
||||
if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime {
|
||||
continue
|
||||
}
|
||||
|
||||
groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId)
|
||||
totalAmounts, exists := transactionTotalAmountsMap[groupKey]
|
||||
|
||||
if !exists {
|
||||
totalAmounts = &models.Transaction{
|
||||
CategoryId: transaction.CategoryId,
|
||||
AccountId: transaction.AccountId,
|
||||
Amount: 0,
|
||||
}
|
||||
|
||||
transactionTotalAmountsMap[groupKey] = totalAmounts
|
||||
}
|
||||
|
||||
totalAmounts.Amount += transaction.Amount
|
||||
}
|
||||
|
||||
transactionTotalAmounts := make([]*models.Transaction, 0, len(transactionTotalAmountsMap))
|
||||
|
||||
for _, totalAmounts := range transactionTotalAmountsMap {
|
||||
transactionTotalAmounts = append(transactionTotalAmounts, totalAmounts)
|
||||
}
|
||||
|
||||
return transactionTotalAmounts, nil
|
||||
|
||||
@@ -44,6 +44,34 @@ func FormatUnixTimeToYearMonth(unixTime int64, timezone *time.Location) string {
|
||||
return t.Format(yearMonthDateTimeFormat)
|
||||
}
|
||||
|
||||
// FormatUnixTimeToNumericLocalDateTime returns numeric year, month, day, hour, minute and second of specified unix time
|
||||
func FormatUnixTimeToNumericLocalDateTime(unixTime int64, timezone *time.Location) int64 {
|
||||
t := parseFromUnixTime(unixTime)
|
||||
|
||||
if timezone != nil {
|
||||
t = t.In(timezone)
|
||||
}
|
||||
|
||||
localDateTime := int64(t.Year())
|
||||
localDateTime = localDateTime*100 + int64(t.Month())
|
||||
localDateTime = localDateTime*100 + int64(t.Day())
|
||||
localDateTime = localDateTime*100 + int64(t.Hour())
|
||||
localDateTime = localDateTime*100 + int64(t.Minute())
|
||||
localDateTime = localDateTime*100 + int64(t.Second())
|
||||
|
||||
return localDateTime
|
||||
}
|
||||
|
||||
// GetMinUnixTimeWithSameLocalDateTime returns the minimum UnixTime for date with the same local date
|
||||
func GetMinUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16) int64 {
|
||||
return unixTime + int64(currentUtcOffset)*60 - easternmostTimezoneUtcOffset*60
|
||||
}
|
||||
|
||||
// GetMaxUnixTimeWithSameLocalDateTime returns the maximum UnixTime for date with the same local date
|
||||
func GetMaxUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16) int64 {
|
||||
return unixTime + int64(currentUtcOffset)*60 - westernmostTimezoneUtcOffset*60
|
||||
}
|
||||
|
||||
// ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone)
|
||||
func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) {
|
||||
timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60)
|
||||
|
||||
@@ -35,6 +35,38 @@ func TestFormatUnixTimeToYearMonth(t *testing.T) {
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestFormatUnixTimeToNumericLocalDateTime(t *testing.T) {
|
||||
unixTime := int64(1617228083)
|
||||
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
||||
utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8
|
||||
|
||||
expectedValue := int64(20210331220123)
|
||||
actualValue := FormatUnixTimeToNumericLocalDateTime(unixTime, utcTimezone)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
|
||||
expectedValue = int64(20210401060123)
|
||||
actualValue = FormatUnixTimeToNumericLocalDateTime(unixTime, utc8Timezone)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestGetMinUnixTimeWithSameLocalDateTime(t *testing.T) {
|
||||
expectedValue := int64(1690797600)
|
||||
actualValue := GetMinUnixTimeWithSameLocalDateTime(1690819200, 480)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
|
||||
actualValue = GetMinUnixTimeWithSameLocalDateTime(1690873200, -420)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestGetMaxUnixTimeWithSameLocalDateTime(t *testing.T) {
|
||||
expectedValue := int64(1690891200)
|
||||
actualValue := GetMaxUnixTimeWithSameLocalDateTime(1690819200, 480)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
|
||||
actualValue = GetMaxUnixTimeWithSameLocalDateTime(1690873200, -420)
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) {
|
||||
expectedValue := int64(1690797600)
|
||||
actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00")
|
||||
|
||||
Reference in New Issue
Block a user