跳到主要内容
  1. 所有文章/

写一个好玩的 Hugo 货币汇率转换短代码

·2026 字·约 5 分钟

前言 #

有时候,在文章中介绍一些产品时,由于使用不同国家的货币,它们可能被标价为美元、欧元、英镑等等。如果读者对最近的汇率不太了解,可能会很难理解这些产品的价值。是否有一种方法可以自动将它们转换为人民币显示呢?

对于静态博客,比如 Hugo,也许我们可以使用短代码 (ShortCodes) 解决?

接口 #

首先,我们需要一个公开的且足够稳定的汇率转换接口,当然它还得有免费层够我们使用。很快啊,我找到了 fixer.io,这家看起来就感觉挺 (hěn) 稳 (zhuàn) 定 (qián) 的 API。

fixer.io
fixer.io

免费层、无需信用卡、调用限制 100 次/月。

价格
价格

注册帐号后,免费层用户用 GET 请求 API

http://data.fixer.io/api/latest?access_key=n4ncmjcz167y4dh2kskyfhtzh52dcmzd 

可以得到一个包含很多国家货币汇率的 JSON 数据

{"success":true,"timestamp":1687080243,"base":"EUR","date":"2023-06-10","rates":{"AED":4.02576,"AFN":93.95886,"ALL":108.152645,"AMD":422.914826,"ANG":1.971737,"AOA":778.762158,"ARS":271.852069,"AUD":1.593589,"AWG":1.972926,"AZN":1.867671,"BAM":1.954944,"BBD":2.208933,"BDT":118.428146,"BGN":1.958739,"BHD":0.41202,"BIF":3090.246932,"BMD":1.09607,"BND":1.46226,"BOB":7.55969,"BRL":5.276098,"BSD":1.094021,"BTC":4.141012e-5,"BTN":89.571381,"BWP":14.471664,"BYN":2.761491,"BYR":21482.973654,"BZD":2.205234,"CAD":1.44917,"CDF":2603.166847,"CHF":0.979833,"CLF":0.031466,"CLP":868.23984,"CNY":7.810929,"COP":4540.212063,"CRC":591.341081,"CUC":1.09607,"CUP":29.045857,"CVE":110.221739,"CZK":23.77793,"DJF":194.794709,"DKK":7.461721,"DOP":60.083692,"DZD":148.285073,"EGP":33.774212,"ERN":16.441051,"ETB":59.584011,"EUR":1,"FJD":2.410043,"FKP":0.854681,"GBP":0.855002,"GEL":2.855307,"GGP":0.854681,"GHS":12.362587,"GIP":0.854681,"GMD":65.110787,"GNF":9405.881623,"GTQ":8.571247,"GYD":231.388686,"HKD":8.572091,"HNL":26.934207,"HRK":7.646308,"HTG":152.613178,"HUF":374.236733,"IDR":16398.797764,"ILS":3.89922,"IMP":0.854681,"INR":89.785133,"IQD":1433.172484,"IRR":46322.666126,"ISK":149.657832,"JEP":0.854681,"JMD":168.896049,"JOD":0.777557,"JPY":155.483065,"KES":152.283323,"KGS":96.0052,"KHR":4505.027468,"KMF":493.396369,"KPW":986.463076,"KRW":1400.014475,"KWD":0.336341,"KYD":0.911701,"KZT":489.935481,"LAK":20153.175907,"LBP":16421.409865,"LKR":339.151502,"LRD":192.771369,"LSL":19.926972,"LTL":3.23641,"LVL":0.663002,"LYD":5.265694,"MAD":10.890931,"MDL":19.632404,"MGA":4947.833585,"MKD":61.593031,"MMK":2297.494039,"MNT":3768.298925,"MOP":8.817539,"MRO":391.296832,"MUR":49.618275,"MVR":16.830199,"MWK":1122.908333,"MXN":18.720991,"MYR":5.05782,"MZN":69.326847,"NAD":19.926968,"NGN":719.574095,"NIO":40.01248,"NOK":11.494054,"NPR":143.31425,"NZD":1.75779,"OMR":0.420806,"PAB":1.094021,"PEN":3.983756,"PGK":3.897294,"PHP":61.067587,"PKR":314.106468,"PLN":4.474544,"PYG":7922.53111,"QAR":3.990833,"RON":4.969038,"RSD":117.127715,"RUB":91.525896,"RWF":1248.453363,"SAR":4.108807,"SBD":9.134989,"SCR":14.988437,"SDG":659.290187,"SEK":11.676271,"SGD":1.468351,"SHP":1.333644,"SLE":24.672455,"SLL":21647.384537,"SOS":623.119859,"SRD":41.209499,"STD":22686.43779,"SVC":9.572809,"SYP":2753.909759,"SZL":19.871299,"THB":37.993119,"TJS":11.952267,"TMT":3.847206,"TND":3.374256,"TOP":2.562287,"TRY":25.889618,"TTD":7.424749,"TWD":33.607924,"TZS":2611.456571,"UAH":40.40251,"UGX":4027.236669,"USD":1.09607,"UYU":41.84168,"UZS":12540.509123,"VEF":2976199.545972,"VES":29.86124,"VND":25790.529085,"VUV":127.64564,"WST":2.940878,"XAF":655.669914,"XAG":0.0453,"XAU":0.00056,"XCD":2.962185,"XDR":0.820541,"XOF":655.669914,"XPF":119.965269,"YER":274.401544,"ZAR":19.936112,"ZMK":9865.949977,"ZMW":21.141743,"ZWL":352.93412}}

唯一的缺点就是免费层的 API 汇率默认基于欧元 (EUR) 的且不能修改,行吧,又不是不能用。

实现 #

在 Hugo 站点根目录新建一个 Hugo 短代码,姑且称它为 rate.html 吧,API 获取的汇率是基于欧元的,所以我们还要将各国货币相对欧元汇率转换为各国货币相对人民币汇率 (或其他币种)。

短代码如下:

<!-- layouts/shortcodes/rate.html -->

{{- $amount := .Get "amount" -}}
{{- $currency := .Get "currency" -}}
{{- $apiUrl := "http://data.fixer.io/api/latest?access_key=n4ncmjcz167y4dh2kskyfhtzh52dcmzd" -}}

{{- $resp := getJSON $apiUrl -}}
{{- $baseRate := 1.0 -}}
{{- $baseCurrency := "EUR" -}}
{{- $targetRate := index $resp.rates "CNY" -}}
{{- $amountInEUR := div (float (print $amount)) (index $resp.rates $currency) -}}
{{- $amountInBaseCurrency := mul $amountInEUR (div $baseRate (index $resp.rates $baseCurrency)) -}}
{{- $result := mul $amountInBaseCurrency $targetRate -}}

{{- printf "%.2f 元" $result -}}

使用 #

下文中的 \\ 用于转义 Hugo 短代码,使用时请把它去掉。

Proton Mail 支持美元 (USD)、欧元 (EUR) 和瑞士法朗 (CHF) 支付,但是价格在数字上却是一样的。以 Proton Unlimited 套餐两年为例,使用哪种货币支付最划算呢?

- 以美元支付的话两年需要 {{\\< rate amount="191.76" currency="USD" >\\}}折合每月 {{\\< rate amount="7.99" currency="USD" >\\}}
- 以欧元支付的话两年需要 {{\\< rate amount="191.76" currency="EUR" >\\}}折合每月 {{\\< rate amount="7.99" currency="EUR" >\\}}
- 以瑞士法郎支付两年需要 {{\\< rate amount="191.76" currency="CHF" >\\}}折合每月 {{\\< rate amount="7.99" currency="CHF" >\\}}

效果如下,这样是不是很直观了呢?

  • 以美元支付的话,两年需要 1396.32 元,折合每月 58.18 元
  • 以欧元支付的话,两年需要 1520.38 元,折合每月 63.35 元
  • 以瑞士法郎支付,两年需要 1583.04 元,折合每月 65.96 元

当然,现实中不会有很多服务或产品像这样对不同国家的货币使用一样的价格 (单纯的数字上) 。使用短代码的一个好处就是,每次我们更新 Hugo 网站,都可以从 API 获取更新的汇率,构建静态网页的时候也会更新最终显示的价格,仅此而已。

所以我说是「好玩,但是应该没什么用」,本文到这里差不多就已经结束了。

缓存 #

然鹅,免费层的 API 调用次数限制为 100次/月,无论是静态网站或者动态网站,这个数量级用起来都很捉急。

之前的想法是调用 API 的时候,将 JSON 数据缓存到 Hugo 的 data/rate.json 里,根据 JSON 里的数据更新时间 date 或时间戳 timestamp 来决定直接使用缓存还是请求更新数据 (比如以 7 天为限)。

奈何对 Hugo 函数不是很熟悉,这个读写流和时间比较不知道怎么下手。退而求其次,咱们换个「笨」一点的方法,用 GitHub Actions 来跑一个 Workflow 吧,每天调用一次接口,然后将 JSON 数据发布到 GitHub Releases,能用就行。

示例 Workflow (粗糙,但能跑就行)

name: Get Exchange Rates JSON Update

on:
  schedule:
    - cron: '0 16 * * *'
    
jobs:
  update-rates-json:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up environment variables
        run: |
          echo "API_KEY=${{ secrets.API_KEY }}" >> $GITHUB_ENV
          echo "API_URL=${{ secrets.API_URL }}" >> $GITHUB_ENV
          echo "TAG_NAME=$(date +%Y%m%d)" >> $GITHUB_ENV          

      - name: Update API JSON
        run: |
          curl -o foreign-exchange-rates.json "${API_URL}?access_key=${API_KEY}"          

      - name: Release
        id: create_release
        uses: softprops/action-gh-release@v1
        with:
          files: ./foreign-exchange-rates.json
          tag_name: ${{ env.TAG_NAME }}
          body: "Release ${{ env.TAG_NAME }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Remove Older Releases
        uses: dev-drprasad/delete-older-releases@v0.2.0
        with:
            keep_latest: 3 # 保留历史版本
            delete_tags: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

在运行该 Workflow 的 GitHub 存储库的 Actions secrets and variables 下添加两个 Repository secrets

  • API_URL : http://data.fixer.io/api/latest
  • API_KEY : fixer.io 的 API 访问密钥

可以通过下面的链接获取最新版 JSON 数据:

https://github.com/<用户名>/<存储库>/releases/latest/download/rate.json

最后修改下 Hugo 短代码中的 JSON 请求 URL:

<!-- layouts/shortcodes/rate.html -->

{{- $amount := .Get "amount" -}}
{{- $currency := .Get "currency" -}}
{{- $apiUrl := "https://github.com/<用户名>/<存储库>/releases/latest/download/rate.json" -}}

{{- $resp := getJSON $apiUrl -}}
{{- $baseRate := 1.0 -}}
{{- $baseCurrency := "EUR" -}}
{{- $targetRate := index $resp.rates "CNY" -}}
{{- $amountInEUR := div (float (print $amount)) (index $resp.rates $currency) -}}
{{- $amountInBaseCurrency := mul $amountInEUR (div $baseRate (index $resp.rates $baseCurrency)) -}}
{{- $result := mul $amountInBaseCurrency $targetRate -}}

{{- printf "%.2f 元" $result -}}

2023 年 6 月 20 日更新:

使用几天后,我发现价格怎么一直没变呢?摔!Releases 链接被 GitHub CDN 缓存了。我们可以在 API URL 后面加一个随机查询字符串,比如:

https://github.com/<用户名>/<存储库>/releases/latest/download/rate.json?1687243683

这样就可以获取最新的 JSON 数据且不会被 CDN 缓存影响,下面介绍几种方法:

时间戳 #

使用 Unix 时间戳做为 API URL 的查询字符串,修改 Hugo 短代码

<!-- layouts/shortcodes/rate.html -->

{{- $timestamp := now.Unix -}}
{{- $apiUrl := printf "https://github.com/<用户名>/<存储库>/releases/latest/download/rate.json?%d" $timestamp -}}

缺点是这个速度太慢了

日期 #

使用日期,精确到分钟就够了,修改 Hugo 短代码

<!-- layouts/shortcodes/rate.html -->

{{- $date := now.Format "200601021504" -}}
{{- $apiUrl := printf "https://github.com/<用户名>/<存储库>/releases/latest/download/rate.json?%s" $date -}}

这个速度非常快,推荐!

Dejavu Moe
作者
Dejavu Moe
Not for success, just for growing.