support exchange rate

This commit is contained in:
MaysWind
2020-11-18 01:19:57 +08:00
parent 9179d359bc
commit 805d77e721
14 changed files with 410 additions and 0 deletions
+3
View File
@@ -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))
}
}
+54
View File
@@ -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
}
+1
View File
@@ -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 {
+67
View File
@@ -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,
}
}
+21
View File
@@ -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,
};
+17
View File
@@ -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;
});
},
};
+3
View File
@@ -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'),
+13
View File
@@ -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',
+13
View File
@@ -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': '许可协议',
+7
View File
@@ -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,
+6
View File
@@ -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,
+175
View File
@@ -0,0 +1,175 @@
<template>
<f7-page ptr @ptr:refresh="update">
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Exchange Rates Data')"></f7-nav-title>
<f7-nav-right>
<f7-link :class="{ 'disabled': updating }" :text="$t('Update')" @click="update(null)"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-card>
<f7-card-content :padding="false" v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item
:title="$t('Base Currency')"
smart-select :smart-select-params="{ openIn: 'sheet', closeOnSelect: true, sheetCloseLinkText: $t('Done') }">
<select v-model="baseCurrency">
<option v-for="exchangeRate in availableExchangeRates"
:key="exchangeRate.currencyCode"
:value="exchangeRate.currencyCode">{{ exchangeRate.currencyDisplayName }}</option>
</select>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card>
<f7-card-content :padding="false" v-if="!exchangeRatesData.exchangeRates || !exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item :title="$t('No exchange rates data')"></f7-list-item>
</f7-list>
</f7-card-content>
<f7-card-content :padding="false" v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item v-for="exchangeRate in availableExchangeRates" :key="exchangeRate.currencyCode"
:title="exchangeRate.currencyDisplayName"
:after="exchangeRate.rate | exchangeRate(exchangeRatesData.exchangeRates, exchangeRatesData.baseCurrency, baseCurrency)"></f7-list-item>
</f7-list>
</f7-card-content>
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<span>{{ $t('Last Updated') }}</span>
<span>{{ exchangeRatesData.date | moment($t('format.date.long')) }}</span>
</f7-card-footer>
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<span>{{ $t('Data source') }}</span>
<span>{{ exchangeRatesData.dataSource }}</span>
</f7-card-footer>
</f7-card>
</f7-page>
</template>
<script>
export default {
data() {
const self = this;
return {
baseCurrency: self.$user.getUserInfo() ? self.$user.getUserInfo().defaultCurrency : self.$t('default.currency'),
exchangeRatesData: self.$exchangeRates.getExchangeRates(),
updating: false
};
},
computed: {
availableExchangeRates() {
if (!this.exchangeRatesData || !this.exchangeRatesData.exchangeRates) {
return [];
}
const availableExchangeRates = [];
for (let i = 0; i < this.exchangeRatesData.exchangeRates.length; i++) {
const exchangeRate = this.exchangeRatesData.exchangeRates[i];
availableExchangeRates.push({
currencyCode: exchangeRate.currency,
currencyDisplayName: this.$t(`currency.${exchangeRate.currency}`),
rate: exchangeRate.rate
});
}
availableExchangeRates.sort(function(c1, c2){
return c1.currencyDisplayName.localeCompare(c2.currencyDisplayName);
})
return availableExchangeRates;
}
},
created() {
if (!this.exchangeRatesData || !this.exchangeRatesData.exchangeRates) {
return;
}
for (let i = 0; i < this.exchangeRatesData.exchangeRates.length; i++) {
const exchangeRate = this.exchangeRatesData.exchangeRates[i];
if (exchangeRate.currency === this.baseCurrency) {
return;
}
}
this.$toast('There is no exchange rates data for your default currency');
},
methods: {
update(done) {
const self = this;
if (self.updating) {
if (done) {
done();
}
return;
}
self.updating = true;
self.$services.getLatestExchangeRates().then(response => {
if (done) {
done();
}
self.updating = false;
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to get exchange rates data');
return;
}
self.exchangeRatesData = data.result;
self.$exchangeRates.setExchangeRates(data.result);
self.$toast('Exchange rates data has been updated');
}).catch(error => {
if (done) {
done();
}
self.updating = false;
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to get exchange rates data');
}
});
}
},
filters: {
exchangeRate(oldRate, exchangeRates, baseCurrency, currentCurrency) {
const exchangeRateMap = {};
for (let i = 0; i < exchangeRates.length; i++) {
const exchangeRate = exchangeRates[i];
exchangeRateMap[exchangeRate.currency] = exchangeRate;
}
const toCurrencyExchangeRate = exchangeRateMap[currentCurrency];
if (!toCurrencyExchangeRate) {
return '';
}
const newRate = parseFloat(oldRate) / parseFloat(toCurrencyExchangeRate.rate);
const newRateStr = newRate.toString();
if (newRateStr.indexOf('.') < 0) {
return newRateStr;
} else {
return newRateStr.substr(0, Math.max(6, newRateStr.indexOf('.') + 2));
}
}
}
}
</script>
+10
View File
@@ -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 => {
+20
View File
@@ -29,6 +29,13 @@
</select>
</f7-list-item>
<f7-list-item :title="$t('Exchange Rates Data')" :after="exchangeRatesLastUpdateDate" link="/exchange_rates"></f7-list-item>
<f7-list-item>
<span>{{ $t('Auto Update Exchange Rates Data') }}</span>
<f7-toggle :checked="isAutoUpdateExchangeRatesData" @toggle:change="isAutoUpdateExchangeRatesData = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Thousands Separator') }}</span>
<f7-toggle :checked="isEnableThousandsSeparator" @toggle:change="isEnableThousandsSeparator = $event"></f7-toggle>
@@ -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;