From 805d77e7212ff7a58e714978432cd10b8df77223 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Wed, 18 Nov 2020 01:19:57 +0800 Subject: [PATCH] support exchange rate --- cmd/webserver.go | 3 + pkg/api/exchange_rates.go | 54 +++++++++ pkg/errs/global.go | 1 + pkg/models/exchange_rate.go | 67 +++++++++++ src/lib/exchangeRates.js | 21 ++++ src/lib/services.js | 17 +++ src/lib/settings.js | 3 + src/locales/en.js | 13 +++ src/locales/zh_Hans.js | 13 +++ src/mobile-main.js | 7 ++ src/router/mobile.js | 6 + src/views/mobile/ExchangeRates.vue | 175 +++++++++++++++++++++++++++++ src/views/mobile/Login.vue | 10 ++ src/views/mobile/Settings.vue | 20 ++++ 14 files changed, 410 insertions(+) create mode 100644 pkg/api/exchange_rates.go create mode 100644 pkg/models/exchange_rate.go create mode 100644 src/lib/exchangeRates.js create mode 100644 src/views/mobile/ExchangeRates.vue diff --git a/cmd/webserver.go b/cmd/webserver.go index d1daa650..01a62a6b 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -168,6 +168,9 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/accounts/hide.json", bindApi(api.Accounts.AccountHideHandler)) apiV1Route.POST("/accounts/move.json", bindApi(api.Accounts.AccountMoveHandler)) apiV1Route.POST("/accounts/delete.json", bindApi(api.Accounts.AccountDeleteHandler)) + + // Exchange Rates + apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler)) } } diff --git a/pkg/api/exchange_rates.go b/pkg/api/exchange_rates.go new file mode 100644 index 00000000..c06b518c --- /dev/null +++ b/pkg/api/exchange_rates.go @@ -0,0 +1,54 @@ +package api + +import ( + "encoding/xml" + "io/ioutil" + "net/http" + + "github.com/mayswind/lab/pkg/core" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/models" +) + +const EuroCentralBankExchangeRateUrl = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" + +type ExchangeRatesApi struct {} + +var ( + ExchangeRates = &ExchangeRatesApi{} +) + +func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (interface{}, *errs.Error) { + uid := c.GetCurrentUid() + resp, err := http.Get(EuroCentralBankExchangeRateUrl) + + if err != nil { + log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrFailedToRequestRemoteApi + } + + if resp.StatusCode != 200 { + log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to get latest exchange rate data response for user \"uid:%d\", because response code is not 200", uid) + return nil, errs.ErrFailedToRequestRemoteApi + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + euroCentralBankData := &models.EuroCentralBankExchangeRateData{} + err = xml.Unmarshal(body, euroCentralBankData) + + if err != nil { + log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse xml data for user \"uid:%d\", response is %s, because %s", uid, string(body), err.Error()) + return nil, errs.ErrFailedToRequestRemoteApi + } + + latestExchangeRateResponse := euroCentralBankData.ToLatestExchangeRateResponse() + + if latestExchangeRateResponse == nil { + log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse latest exchange rate data for user \"uid:%d\", response is %s,", uid, string(body)) + return nil, errs.ErrFailedToRequestRemoteApi + } + + return latestExchangeRateResponse, nil +} diff --git a/pkg/errs/global.go b/pkg/errs/global.go index e8cfcd47..45af222a 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -11,6 +11,7 @@ var ( ErrRequestIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 2, http.StatusInternalServerError, "request id is invalid") ErrCiphertextInvalid = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 3, http.StatusInternalServerError, "ciphertext is invalid") ErrNothingWillBeUpdated = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 4, http.StatusBadRequest, "nothing will be updated") + ErrFailedToRequestRemoteApi = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 5, http.StatusBadRequest, "failed to request third party api") ) func GetParameterInvalidMessage(field string) string { diff --git a/pkg/models/exchange_rate.go b/pkg/models/exchange_rate.go new file mode 100644 index 00000000..922b819b --- /dev/null +++ b/pkg/models/exchange_rate.go @@ -0,0 +1,67 @@ +package models + +import "encoding/xml" + +const EuroCentralBankDataSource = "European Central Bank" +const EuroCentralBankBaseCurrency = "EUR" + +type LatestExchangeRateResponse struct { + DataSource string `json:"dataSource"` + Date string `json:"date"` + BaseCurrency string `json:"baseCurrency"` + ExchangeRates []*LatestExchangeRate `json:"exchangeRates"` +} + +type LatestExchangeRate struct { + Currency string `json:"currency"` + Rate string `json:"rate"` +} + +type EuroCentralBankExchangeRateData struct { + XMLName xml.Name `xml:"Envelope"` + AllExchangeRates []*EuroCentralBankExchangeRates `xml:"Cube>Cube"` +} + +type EuroCentralBankExchangeRates struct { + Date string `xml:"time,attr"` + ExchangeRates []*EuroCentralBankExchangeRate `xml:"Cube"` +} + +type EuroCentralBankExchangeRate struct { + Currency string `xml:"currency,attr"` + Rate string `xml:"rate,attr"` +} + +func (e EuroCentralBankExchangeRateData) ToLatestExchangeRateResponse() *LatestExchangeRateResponse { + if len(e.AllExchangeRates) < 1 { + return nil + } + + latestEuroCentralBankExchangeRate := e.AllExchangeRates[0] + + if len(latestEuroCentralBankExchangeRate.ExchangeRates) < 1 { + return nil + } + + exchangeRates := make([]*LatestExchangeRate, len(latestEuroCentralBankExchangeRate.ExchangeRates)) + + for i := 0; i < len(latestEuroCentralBankExchangeRate.ExchangeRates); i++ { + exchangeRates[i] = latestEuroCentralBankExchangeRate.ExchangeRates[i].ToLatestExchangeRate() + } + + latestExchangeRateResp := &LatestExchangeRateResponse{ + DataSource: EuroCentralBankDataSource, + Date: latestEuroCentralBankExchangeRate.Date, + BaseCurrency: EuroCentralBankBaseCurrency, + ExchangeRates: exchangeRates, + } + + return latestExchangeRateResp +} + +func (e EuroCentralBankExchangeRate) ToLatestExchangeRate() *LatestExchangeRate { + return &LatestExchangeRate{ + Currency: e.Currency, + Rate: e.Rate, + } +} diff --git a/src/lib/exchangeRates.js b/src/lib/exchangeRates.js new file mode 100644 index 00000000..a32e6e80 --- /dev/null +++ b/src/lib/exchangeRates.js @@ -0,0 +1,21 @@ +const exchangeRatesLocalStorageKey = 'lab_exchange_rates'; + +function getExchangeRates() { + const storageData = localStorage.getItem(exchangeRatesLocalStorageKey) || '{}'; + return JSON.parse(storageData); +} + +function setExchangeRates(value) { + const storageData = JSON.stringify(value); + localStorage.setItem(exchangeRatesLocalStorageKey, storageData); +} + +function clearExchangeRates() { + localStorage.removeItem(exchangeRatesLocalStorageKey); +} + +export default { + getExchangeRates, + setExchangeRates, + clearExchangeRates, +}; diff --git a/src/lib/services.js b/src/lib/services.js index b04cc2aa..b07a46ce 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -1,5 +1,6 @@ import axios from 'axios'; import userState from "./userstate.js"; +import exchangeRates from "./exchangeRates.js"; let needBlockRequest = false; let blockedRequests = []; @@ -209,4 +210,20 @@ export default { id }); }, + getLatestExchangeRates: () => { + return axios.get('v1/exchange_rates/latest.json'); + }, + refreshLatestExchangeRates: () => { + return axios.get('v1/exchange_rates/latest.json', { + ignoreError: true + }).then(response => { + const data = response.data; + + if (data && data.success && data.result && data.result.exchangeRates) { + exchangeRates.setExchangeRates(data.result); + } + + return data.result; + }); + }, }; diff --git a/src/lib/settings.js b/src/lib/settings.js index 448914a1..5aa9ff0f 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -5,6 +5,7 @@ const serverSettingsCookieKey = 'ACP_SETTINGS'; const defaultSettings = { lang: 'en', + autoUpdateExchangeRatesData: true, thousandsSeparator: true, currencyDisplayMode: 'code', // or 'none' or 'name' showAccountBalance: true, @@ -72,6 +73,8 @@ function clearSettings() { export default { getLanguage: () => getOriginalOption('lang'), setLanguage: value => setOption('lang', value), + isAutoUpdateExchangeRatesData: () => getOption('autoUpdateExchangeRatesData'), + setAutoUpdateExchangeRatesData: value => setOption('autoUpdateExchangeRatesData', 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 f7f5b8dd..c9721c4f 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -8,6 +8,9 @@ export default { 'currency': 'USD', }, 'format': { + 'date': { + 'long': 'MM/DD/YYYY' + }, 'datetime': { 'long': 'MM/DD/YYYY HH:mm:ss', } @@ -178,6 +181,7 @@ export default { 'incomplete or incorrect submission': 'Incomplete or incorrect submission', 'operation failed': 'Operation failed', 'nothing will be updated': 'Nothing will be updated', + 'failed to request third party api': 'Failed to request third party api', 'user id is invalid': 'User id is invalid', 'username is empty': 'Username is empty', 'email is empty': 'Email is empty', @@ -359,6 +363,7 @@ export default { 'Unable to delete this account': 'Unable to delete this account', 'User Profile': 'User Profile', 'Language': 'Language', + 'Auto Update Exchange Rates Data': 'Auto Update Exchange Rates Data', 'Enable Thousands Separator': 'Enable Thousands Separator', 'Currency Display Mode': 'Currency Display Mode', 'Currency Code': 'Currency Code', @@ -399,6 +404,14 @@ export default { 'Log Out': 'Log Out', 'Are you sure you want to log out?': 'Are you sure you want to log out?', 'Unable to logout': 'Unable to logout', + 'Exchange Rates Data': 'Exchange Rates Data', + 'Base Currency': 'Base Currency', + 'Last Updated': 'Last Updated', + 'Data source': 'Data source', + 'No exchange rates data': 'No exchange rates data', + 'There is no exchange rates data for your default currency': 'There is no exchange rates data for your default currency', + 'Exchange rates data has been updated': 'Exchange rates data has been updated', + 'Unable to get exchange rates data': 'Unable to get exchange rates data', 'About': 'About', 'Official Website': 'Official Website', 'License': 'License', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index bb754c16..714919bb 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -8,6 +8,9 @@ export default { 'currency': 'CNY', }, 'format': { + 'date': { + 'long': 'YYYY年MM月DD日' + }, 'datetime': { 'long': 'YYYY年MM月DD日 HH:mm:ss', } @@ -178,6 +181,7 @@ export default { 'incomplete or incorrect submission': '提交不完整或不正确', 'operation failed': '操作失败', 'nothing will be updated': '没有内容更新', + 'failed to request third party api': '请求第三方接口失败', 'user id is invalid': '用户ID无效', 'username is empty': '用户名为空', 'email is empty': '电子邮箱为空', @@ -359,6 +363,7 @@ export default { 'Unable to delete this account': '无法删除该账户', 'User Profile': '用户信息', 'Language': '语言', + 'Auto Update Exchange Rates Data': '自动更新汇率数据', 'Enable Thousands Separator': '启用千位分隔符', 'Currency Display Mode': '货币显示模式', 'Currency Code': '货币代码', @@ -399,6 +404,14 @@ export default { 'Log Out': '退出登录', 'Are you sure you want to log out?': '您确定是否要退出登录?', 'Unable to logout': '无法退出登录', + 'Exchange Rates Data': '汇率数据', + 'Base Currency': '基准货币', + 'Last Updated': '最后更新', + 'Data source': '数据来源', + 'No exchange rates data': '没有汇率数据', + 'There is no exchange rates data for your default currency': '没有您默认货币的汇率数据', + 'Exchange rates data has been updated': '汇率数据已更新', + 'Unable to get exchange rates data': '无法获取汇率数据', 'About': '关于', 'Official Website': '官方网站', 'License': '许可协议', diff --git a/src/mobile-main.js b/src/mobile-main.js index bb7a19c5..a21effbe 100644 --- a/src/mobile-main.js +++ b/src/mobile-main.js @@ -22,6 +22,7 @@ import version from './lib/version.js'; import settings from './lib/settings.js'; import services from './lib/services.js'; import userstate from './lib/userstate.js'; +import exchangeRates from './lib/exchangeRates.js'; import utils from './lib/utils.js'; import currencyFilter from './filters/currency.js'; import accountIconFilter from './filters/accountIcon.js'; @@ -149,6 +150,7 @@ Vue.prototype.$hideLoading = function () { }; Vue.prototype.$services = services; +Vue.prototype.$exchangeRates = exchangeRates; Vue.prototype.$user = userstate; Vue.filter('currency', (value, currencyCode) => currencyFilter({ i18n }, value, currencyCode)); @@ -163,6 +165,11 @@ if (userstate.isUserLogined()) { services.refreshToken(); } +// auto refresh exchange rates data +if (settings.isAutoUpdateExchangeRatesData()) { + services.refreshLatestExchangeRates(); +} + new Vue({ el: '#app', i18n: i18n, diff --git a/src/router/mobile.js b/src/router/mobile.js index 8ed3a59e..6a7eb6f8 100644 --- a/src/router/mobile.js +++ b/src/router/mobile.js @@ -13,6 +13,7 @@ import AccountEditPage from '../views/mobile/accounts/AccountEdit.vue'; import StatisticsOverviewPage from '../views/mobile/statistics/Overview.vue'; import SettingsPage from '../views/mobile/Settings.vue'; +import ExchangeRatesPage from "../views/mobile/ExchangeRates.vue"; import AboutPage from "../views/mobile/About.vue"; import UserProfilePage from "../views/mobile/users/UserProfile.vue"; import TwoFactorAuthPage from "../views/mobile/users/TwoFactorAuth.vue"; @@ -93,6 +94,11 @@ const routes = [ component: SettingsPage, beforeEnter: checkLogin }, + { + path: '/exchange_rates', + component: ExchangeRatesPage, + beforeEnter: checkLogin + }, { path: '/about', component: AboutPage, diff --git a/src/views/mobile/ExchangeRates.vue b/src/views/mobile/ExchangeRates.vue new file mode 100644 index 00000000..f3e8462e --- /dev/null +++ b/src/views/mobile/ExchangeRates.vue @@ -0,0 +1,175 @@ + + + diff --git a/src/views/mobile/Login.vue b/src/views/mobile/Login.vue index 6b858391..e9d27995 100644 --- a/src/views/mobile/Login.vue +++ b/src/views/mobile/Login.vue @@ -186,6 +186,11 @@ export default { } self.$user.updateTokenAndUserInfo(data.result); + + if (self.$settings.isAutoUpdateExchangeRatesData()) { + self.$services.refreshLatestExchangeRates(); + } + router.navigate('/'); }).catch(error => { self.logining = false; @@ -252,6 +257,11 @@ export default { } self.$user.updateTokenAndUserInfo(data.result); + + if (self.$settings.isAutoUpdateExchangeRatesData()) { + self.$services.refreshLatestExchangeRates(); + } + self.show2faSheet = false; router.navigate('/'); }).catch(error => { diff --git a/src/views/mobile/Settings.vue b/src/views/mobile/Settings.vue index d6f56bc2..cca66766 100644 --- a/src/views/mobile/Settings.vue +++ b/src/views/mobile/Settings.vue @@ -29,6 +29,13 @@ + + + + {{ $t('Auto Update Exchange Rates Data') }} + + + {{ $t('Enable Thousands Separator') }} @@ -93,6 +100,18 @@ export default { this.$setLanguage(value); } }, + exchangeRatesLastUpdateDate() { + const exchangeRates = this.$exchangeRates.getExchangeRates(); + return exchangeRates && exchangeRates.date ? this.$moment(exchangeRates.date).format(this.$t('format.date.long')) : ''; + }, + isAutoUpdateExchangeRatesData: { + get: function () { + return this.$settings.isAutoUpdateExchangeRatesData(); + }, + set: function (value) { + this.$settings.setAutoUpdateExchangeRatesData(value); + } + }, isEnableThousandsSeparator: { get: function () { return this.$settings.isEnableThousandsSeparator(); @@ -161,6 +180,7 @@ export default { self.$user.clearTokenAndUserInfo(); self.$settings.clearSettings(); + self.$exchangeRates.clearExchangeRates(); router.navigate('/'); }).catch(error => { self.logouting = false;