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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Last Updated') }}
+ {{ exchangeRatesData.date | moment($t('format.date.long')) }}
+
+
+ {{ $t('Data source') }}
+ {{ exchangeRatesData.dataSource }}
+
+
+
+
+
+
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;