From e71ffd1a77c437300ea165557344d535a746c2d4 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 28 Apr 2023 21:22:12 +0800 Subject: [PATCH] support storing geo location in transaction --- pkg/api/transactions.go | 24 +++++- pkg/models/transaction.go | 77 ++++++++++++------ pkg/services/transactions.go | 5 ++ src/lib/services.js | 6 +- src/lib/settings.js | 3 + src/locales/en.js | 7 ++ src/locales/zh_Hans.js | 7 ++ src/views/mobile/SettingsPage.vue | 13 +++ src/views/mobile/transactions/EditPage.vue | 95 +++++++++++++++++++++- 9 files changed, 207 insertions(+), 30 deletions(-) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index dc1dbc6b..f9351b4a 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "sort" "strings" @@ -572,7 +573,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, * transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId]) transactionTagIds := allTransactionTagIds[transaction.TransactionId] - transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable) + transactionResp := transaction.ToTransactionInfoResponse(c, transactionTagIds, transactionEditable) if !transactionGetReq.TrimAccount { if sourceAccount := accountMap[transaction.AccountId]; sourceAccount != nil { @@ -664,7 +665,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{} log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId) - transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable) + transactionResp := transaction.ToTransactionInfoResponse(c, tagIds, transactionEditable) return transactionResp, nil } @@ -722,6 +723,12 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} transactionTagIds = make([]int64, 0, 0) } + var geoLocation []byte + + if transactionModifyReq.GeoLocation != nil { + geoLocation, _ = json.Marshal(transactionModifyReq.GeoLocation) + } + newTransaction := &models.Transaction{ TransactionId: transaction.TransactionId, Uid: uid, @@ -732,6 +739,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} Amount: transactionModifyReq.SourceAmount, HideAmount: transactionModifyReq.HideAmount, Comment: transactionModifyReq.Comment, + GeoLocation: string(geoLocation), } if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { @@ -748,6 +756,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} (transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) && newTransaction.HideAmount == transaction.HideAmount && newTransaction.Comment == transaction.Comment && + newTransaction.GeoLocation == transaction.GeoLocation && utils.Int64SliceEquals(tagIds, transactionTagIds) { return nil, errs.ErrNothingWillBeUpdated } @@ -777,7 +786,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{} log.InfofWithRequestId(c, "[transactions.TransactionModifyHandler] user \"uid:%d\" has updated transaction \"id:%d\" successfully", uid, transactionModifyReq.Id) newTransaction.Type = transaction.Type - newTransactionResp := newTransaction.ToTransactionInfoResponse(tagIds, transactionEditable) + newTransactionResp := newTransaction.ToTransactionInfoResponse(c, tagIds, transactionEditable) return newTransactionResp, nil } @@ -1004,7 +1013,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId]) transactionTagIds := allTransactionTagIds[transaction.TransactionId] - result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable) + result[i] = transaction.ToTransactionInfoResponse(c, transactionTagIds, transactionEditable) if !trimAccount { if sourceAccount := allAccounts[transaction.AccountId]; sourceAccount != nil { @@ -1045,6 +1054,12 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT } + var geoLocation []byte + + if transactionCreateReq.GeoLocation != nil { + geoLocation, _ = json.Marshal(transactionCreateReq.GeoLocation) + } + transaction := &models.Transaction{ Uid: uid, Type: transactionDbType, @@ -1055,6 +1070,7 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate Amount: transactionCreateReq.SourceAmount, HideAmount: transactionCreateReq.HideAmount, Comment: transactionCreateReq.Comment, + GeoLocation: string(geoLocation), CreatedIp: clientIp, } diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index ac5bdb78..a074bc73 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -1,11 +1,14 @@ package models import ( + "encoding/json" "fmt" "strings" "time" + "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/utils" ) @@ -48,40 +51,49 @@ type Transaction struct { RelatedAccountAmount int64 `xorm:"NOT NULL"` HideAmount bool `xorm:"NOT NULL"` Comment string `xorm:"VARCHAR(255) NOT NULL"` + GeoLocation string `xorm:"VARCHAR(255)"` CreatedIp string `xorm:"VARCHAR(39)"` CreatedUnixTime int64 UpdatedUnixTime int64 DeletedUnixTime int64 } +// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request +type TransactionGeoLocationRequest struct { + Latitude float64 `json:"latitude" binding:"required"` + Longitude float64 `json:"longitude" binding:"required"` +} + // TransactionCreateRequest represents all parameters of transaction creation request type TransactionCreateRequest struct { - Type TransactionType `json:"type" binding:"required"` - CategoryId int64 `json:"categoryId,string"` - Time int64 `json:"time" binding:"required,min=1"` - UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"` - SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` - DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"` - SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"` - DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` - HideAmount bool `json:"hideAmount"` - TagIds []string `json:"tagIds"` - Comment string `json:"comment" binding:"max=255"` + Type TransactionType `json:"type" binding:"required"` + CategoryId int64 `json:"categoryId,string"` + Time int64 `json:"time" binding:"required,min=1"` + UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"` + SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` + DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"` + SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"` + DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` + HideAmount bool `json:"hideAmount"` + TagIds []string `json:"tagIds"` + Comment string `json:"comment" binding:"max=255"` + GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"` } // TransactionModifyRequest represents all parameters of transaction modification request type TransactionModifyRequest struct { - Id int64 `json:"id,string" binding:"required,min=1"` - CategoryId int64 `json:"categoryId,string"` - Time int64 `json:"time" binding:"required,min=1"` - UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"` - SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` - DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"` - SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"` - DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` - HideAmount bool `json:"hideAmount"` - TagIds []string `json:"tagIds"` - Comment string `json:"comment" binding:"max=255"` + Id int64 `json:"id,string" binding:"required,min=1"` + CategoryId int64 `json:"categoryId,string"` + Time int64 `json:"time" binding:"required,min=1"` + UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"` + SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` + DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"` + SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"` + DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` + HideAmount bool `json:"hideAmount"` + TagIds []string `json:"tagIds"` + Comment string `json:"comment" binding:"max=255"` + GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"` } // TransactionCountRequest represents transaction count request @@ -170,6 +182,12 @@ type TransactionAccountAmount struct { TotalExpenseAmount int64 } +// TransactionGeoLocationResponse represents a view-object of transaction geographic location info +type TransactionGeoLocationResponse struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + // TransactionInfoResponse represents a view-object of transaction type TransactionInfoResponse struct { Id int64 `json:"id,string"` @@ -189,6 +207,7 @@ type TransactionInfoResponse struct { TagIds []string `json:"tagIds"` Tags []*TransactionTagInfoResponse `json:"tags,omitempty"` Comment string `json:"comment"` + GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"` Editable bool `json:"editable"` } @@ -264,7 +283,7 @@ func (t *Transaction) IsEditable(currentUser *User, utcOffset int16, account *Ac } // ToTransactionInfoResponse returns a view-object according to database model -func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *TransactionInfoResponse { +func (t *Transaction) ToTransactionInfoResponse(c *core.Context, tagIds []int64, editable bool) *TransactionInfoResponse { var transactionType TransactionType if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE { @@ -298,6 +317,17 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) * destinationAmount = t.Amount } + geoLocation := &TransactionGeoLocationResponse{} + + if t.GeoLocation != "" { + err := json.Unmarshal([]byte(t.GeoLocation), geoLocation) + if err != nil { + log.WarnfWithRequestId(c, "[transaction.ToTransactionInfoResponse] cannot unmarshal geo location \"%s\", because %s", t.GeoLocation, err.Error()) + } + } else { + geoLocation = nil + } + return &TransactionInfoResponse{ Id: t.TransactionId, TimeSequenceId: t.TransactionTime, @@ -312,6 +342,7 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) * HideAmount: t.HideAmount, TagIds: utils.Int64ArrayToStringArray(tagIds), Comment: t.Comment, + GeoLocation: geoLocation, Editable: editable, } } diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index b52cdda7..4baabd8b 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -542,6 +542,10 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction, updateCols = append(updateCols, "comment") } + if transaction.GeoLocation != oldTransaction.GeoLocation { + updateCols = append(updateCols, "geo_location") + } + // Get and verify tags err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds) @@ -955,6 +959,7 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction * RelatedAccountId: originalTransaction.AccountId, RelatedAccountAmount: originalTransaction.Amount, Comment: originalTransaction.Comment, + GeoLocation: originalTransaction.GeoLocation, CreatedIp: originalTransaction.CreatedIp, CreatedUnixTime: originalTransaction.CreatedUnixTime, UpdatedUnixTime: originalTransaction.UpdatedUnixTime, diff --git a/src/lib/services.js b/src/lib/services.js index 48597ad5..bad91bab 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -266,7 +266,7 @@ export default { getTransaction: ({ id }) => { return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`); }, - addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, utcOffset }) => { + addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => { return axios.post('v1/transactions/add.json', { type, categoryId, @@ -278,10 +278,11 @@ export default { hideAmount, tagIds, comment, + geoLocation, utcOffset }); }, - modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, utcOffset }) => { + modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => { return axios.post('v1/transactions/modify.json', { id, type, @@ -294,6 +295,7 @@ export default { hideAmount, tagIds, comment, + geoLocation, utcOffset }); }, diff --git a/src/lib/settings.js b/src/lib/settings.js index 1ee19d55..935b9042 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -13,6 +13,7 @@ const defaultSettings = { applicationLock: false, applicationLockWebAuthn: false, autoUpdateExchangeRatesData: true, + autoGetCurrentGeoLocation: false, thousandsSeparator: true, currencyDisplayMode: currencyConstants.defaultCurrencyDisplayMode, showAmountInHomePage: true, @@ -140,6 +141,8 @@ export default { setEnableApplicationLockWebAuthn: value => setOption('applicationLockWebAuthn', value), isAutoUpdateExchangeRatesData: () => getOption('autoUpdateExchangeRatesData'), setAutoUpdateExchangeRatesData: value => setOption('autoUpdateExchangeRatesData', value), + isAutoGetCurrentGeoLocation: () => getOption('autoGetCurrentGeoLocation'), + setAutoGetCurrentGeoLocation: value => setOption('autoGetCurrentGeoLocation', value), isEnableThousandsSeparator: () => getOption('thousandsSeparator'), setEnableThousandsSeparator: value => setOption('thousandsSeparator', value), getCurrencyDisplayMode: () => getOption('currencyDisplayMode'), diff --git a/src/locales/en.js b/src/locales/en.js index 8ad468da..bf87ee50 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -830,6 +830,12 @@ export default { 'Destination Account': 'Destination Account', 'Transaction Time': 'Transaction Time', 'Transaction Time Zone': 'Transaction Time Zone', + 'Geographic Location': 'Geographic Location', + 'No Location': 'No Location', + 'Getting Location...': 'Getting Location...', + 'Update Geographic Location': 'Update Geographic Location', + 'Clear Geographic Location': 'Clear Geographic Location', + 'Unable to get current position': 'Unable to get current position', 'Tags': 'Tags', 'Your transaction description (optional)': 'Your transaction description (optional)', 'Are you sure you want to save this transaction whose amount is 0?': 'Are you sure you want to save this transaction whose amount is 0?', @@ -881,6 +887,7 @@ export default { 'Timezone': 'Timezone', 'System Default': 'System Default', 'Auto Update Exchange Rates Data': 'Auto Update Exchange Rates Data', + 'Auto Get Current Geographic Location': 'Auto Get Current Geographic Location', 'Enable Thousands Separator': 'Enable Thousands Separator', 'Currency Display Mode': 'Currency Display Mode', 'Currency Code': 'Currency Code', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 2562dbac..1a78a470 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -830,6 +830,12 @@ export default { 'Destination Account': '目标账户', 'Transaction Time': '交易时间', 'Transaction Time Zone': '交易时区', + 'Geographic Location': '地理位置', + 'No Location': '没有位置', + 'Getting Location...': '正在获取位置...', + 'Update Geographic Location': '更新地理位置', + 'Clear Geographic Location': '清除地理位置', + 'Unable to get current position': '无法获取当前地理位置', 'Tags': '标签', 'Your transaction description (optional)': '你的交易描述 (可选)', 'Are you sure you want to save this transaction whose amount is 0?': '您确定要保存这个金额为0的交易?', @@ -881,6 +887,7 @@ export default { 'Timezone': '时区', 'System Default': '系统默认', 'Auto Update Exchange Rates Data': '自动更新汇率数据', + 'Auto Get Current Geographic Location': '自动获取当前地理位置', 'Enable Thousands Separator': '启用千位分隔符', 'Currency Display Mode': '货币显示模式', 'Currency Code': '货币代码', diff --git a/src/views/mobile/SettingsPage.vue b/src/views/mobile/SettingsPage.vue index 3fa7b2cb..5eb50d1f 100644 --- a/src/views/mobile/SettingsPage.vue +++ b/src/views/mobile/SettingsPage.vue @@ -46,6 +46,11 @@ + + {{ $t('Auto Get Current Geographic Location') }} + + + {{ $t('Enable Thousands Separator') }} @@ -149,6 +154,14 @@ export default { this.$settings.setAutoUpdateExchangeRatesData(value); } }, + isAutoGetCurrentGeoLocation: { + get: function () { + return this.$settings.isAutoGetCurrentGeoLocation(); + }, + set: function (value) { + this.$settings.setAutoGetCurrentGeoLocation(value); + } + }, isEnableThousandsSeparator: { get: function () { return this.$settings.isEnableThousandsSeparator(); diff --git a/src/views/mobile/transactions/EditPage.vue b/src/views/mobile/transactions/EditPage.vue index 62b5558f..58e7ed02 100644 --- a/src/views/mobile/transactions/EditPage.vue +++ b/src/views/mobile/transactions/EditPage.vue @@ -27,6 +27,7 @@ + + + + + + + + {{ $t('Update Geographic Location') }} + {{ $t('Clear Geographic Location') }} + + + {{ $t('Cancel') }} + + + {{ $t('Show Amount') }} @@ -330,12 +355,16 @@ export default { destinationAmount: 0, hideAmount: false, tagIds: [], - comment: '' + comment: '', + geoLocation: null }, loading: true, loadingError: null, + geoLocationStatus: null, submitting: false, + isSupportGeoLocation: !!navigator.geolocation, showAccountBalance: self.$settings.isShowAccountBalance(), + showGeoLocationActionSheet: false, showMoreActionSheet: false, showSourceAmountSheet: false, showDestinationAmountSheet: false, @@ -507,6 +536,15 @@ export default { destinationAmountFontSize() { return this.getFontSizeByAmount(this.transaction.destinationAmount); }, + geoLocationStatusInfo() { + if (this.geoLocationStatus === 'success') { + return ''; + } else if (this.geoLocationStatus === 'getting') { + return this.$t('Getting Location...'); + } else { + return this.$t('No Location'); + } + }, inputIsEmpty() { return !!this.inputEmptyProblemMessage; }, @@ -711,6 +749,10 @@ export default { self.transaction.hideAmount = transaction.hideAmount; self.transaction.tagIds = transaction.tagIds || []; self.transaction.comment = transaction.comment; + + if (self.mode === 'edit' || self.mode === 'view') { + self.transaction.geoLocation = transaction.geoLocation; + } } self.loading = false; @@ -728,6 +770,11 @@ export default { methods: { onPageAfterIn() { this.$routeBackOnError(this.f7router, 'loadingError'); + + if (this.$settings.isAutoGetCurrentGeoLocation() && this.mode === 'add' + && !this.geoLocationStatus && !this.transaction.geoLocation) { + this.updateGeoLocation(false); + } }, save() { const self = this; @@ -747,6 +794,7 @@ export default { hideAmount: self.transaction.hideAmount, tagIds: self.transaction.tagIds, comment: self.transaction.comment, + geoLocation: self.transaction.geoLocation, utcOffset: self.transaction.utcOffset }; @@ -803,6 +851,51 @@ export default { doSubmit(); } }, + updateGeoLocation(forceUpdate) { + const self = this; + + if (!self.isSupportGeoLocation) { + self.$logger.warn('this browser does not support geo location'); + + if (forceUpdate) { + self.$toast('Unable to get current position'); + } + return; + } + + navigator.geolocation.getCurrentPosition(function (position) { + if (!position || !position.coords) { + self.$logger.error('current position is null'); + self.geoLocationStatus = 'error'; + + if (forceUpdate) { + self.$toast('Unable to get current position'); + } + + return; + } + + self.geoLocationStatus = 'success'; + + self.transaction.geoLocation = { + latitude: position.coords.latitude, + longitude: position.coords.longitude + }; + }, function (err) { + self.$logger.error('cannot get current position', err); + self.geoLocationStatus = 'error'; + + if (forceUpdate) { + self.$toast('Unable to get current position'); + } + }); + + self.geoLocationStatus = 'getting'; + }, + clearGeoLocation() { + this.geoLocationStatus = null; + this.transaction.geoLocation = null; + }, isCategoryIdAvailable(categories, categoryId) { if (!categories || !categories.length) { return false;