MetalAPI
XAU3,997.89−1.54%XAG53.936−1.51%XPT1,554.47−0.83%XPD1,047.03−0.44%XCU6.5080+2.29%WTI103.27+3.82%BRENT111.70−1.30%EUR1.120400−0.01%GBP1.337500−0.01%JPY0.0063761+0.14%CHF1.213445−0.25%TRY0.020125−0.12%BTC80,329.75−0.55%ETH2,329.36−1.13%
全部文章
教程电商TypeScriptPython

用实时金价打造首饰价格计算器

用 TypeScript 和 Python 在服务器端构建首饰价格计算器:按克计的实时金价、K 金纯度、加工费、增值税,以及不超出 API 配额的缓存策略,附完整代码。

作者:MetalAPI Team··阅读约 10 分钟

计算器的功能

首饰的定价通常取决于四项输入:饰品的重量、所用黄金的纯度、当前金价以及首饰商的加工费,最后再加上税费。要为每个商品页面或每次报价请求完成这样的计算,计算器需要一个以店铺所用货币计价的每克金价。而且,获取这个价格必须足够快、不能超出 API 配额,也不能把 API 密钥暴露给浏览器。

本教程分别用 TypeScript(Node.js)和 Python 构建一个小型服务器端服务。浏览器或您的商城后端将重量和 K 数发送到您自己的端点,服务器将其与从 MetalAPI 获取并缓存的金价相结合,返回完整的价格明细。计算公式如下:

定价公式
金属价值 = 重量(克)× 每克金价(24K)× 纯度
加工费   = 按克计、按金属价值的百分比计,或固定金额
净价     = 金属价值 + 加工费
增值税   = 净价 × 增值税税率
总价     = 净价 + 增值税

现货金价是批发参考价格。零售价格还包含设计、宝石、损耗和利润等因素,而回购价格通常低于现货价。请将本计算器视为定价中与金属相关的部分,再在此基础上叠加您自己的业务规则。

以您的货币计价的每克金价

/latest 端点可以用任意基础货币返回价格,金属价格还可以使用任意重量单位。只需一次请求,即可获得 1 克纯金的欧元价格:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU&unit=gram" \
  -H "X-API-Key: YOUR_API_KEY"
响应(示例数值)
{
  "success": true,
  "timestamp": 1792834892,
  "date": "2026-10-24",
  "base": "EUR",
  "unit": "gram",
  "rates": {
    "XAU": 0.0090909091,
    "EURXAU": 110
  },
  "rate_limit": { "limit": 10000, "remaining": 9874, "...": "..." }
}

rates.EURXAU 是 1 单位 XAU(此处为 1 克)以基础货币计的价格,也就是计算器所需的数字。rates.XAU 是它的倒数,即 1 欧元可买到的黄金克数。只要单位不是默认的金衡盎司,响应中就会包含 unit 字段;timestamp 是价格数据的时间,请在每份报价旁边显示它。

基础货币和单位在所有套餐(包括 Free)中均可使用。如果您还销售银饰或铂金饰品,同一请求中还可以加入 XAG、XPT 和 XPD。付费套餐还可以使用 carat 端点,它能直接返回 XAU22K 等按成色计的价格,或通过 carat=all 一次返回所有成色。本教程自行计算纯度,这样代码在任何套餐上都能运行,并且可以使用成色印记的数值——这正是下一节的主题。

纯度、加工费与增值税

K 数以 24 分之几来表示黄金含量,因此纯度系数为 karat / 24,/carat 端点采用的也是这一约定。不过,许多市场改用千分比成色作为黄金印记,对某些 K 数而言,两者略有差异:

K 数karat / 24常见印记
24K1.0000999
22K0.9167916
21K0.8750875
18K0.7500750
14K0.5833585
9K0.3750375

以一枚 5.2 克的 14K 戒指为例,按示例金价每克 €110 计算,用 14/24 得到的金属价值为 €333.67,用 0.585 则为 €334.62。两者都没有错。请采用与您的印记和发票一致的约定,这也是下面的代码接受纯度系数而非 K 数的原因。K 金价格指南对相关背景有更深入的介绍。

加工费通常有以下三种形式,TypeScript 版本全部支持:

  • 按克计:饰品每克收取固定金额,例如每克 €12。
  • 按比例:按金属价值的一定比例收取,例如 8%,随金价涨跌。
  • 固定费用:每件收取固定费用,常见于小件饰品和定制款。

增值税或销售税最后计算。税率及其适用范围因国家而异:有些国家对全价征税,有些对投资金和首饰区别对待,还有些只对加工费部分征税。请将税率和应税金额放在配置中,并与您的会计师确认;每一行金额都四舍五入到分,确保发票上各行金额之和等于总额。

通过缓存控制在配额之内

每个套餐按固定间隔更新价格,更新时间点从 00:00 UTC 起按该间隔的整数倍计算:Standard 套餐在 :00、:10、:20 等时刻更新,Free 套餐每天在 UTC 午夜更新一次。两个更新时间点之间的所有请求返回的价格都相同,因此理想的缓存在每个时间点刷新一次。如果全天候在每个时间点都刷新,31 天的月份所需的请求数如下:

套餐(每月配额)更新间隔每个时间点都刷新所需请求数是否在配额内?
Free(100)每日31是
Lite(1,000)30 分钟1,488否:改为每小时刷新(744)
Standard(10,000)10 分钟4,464是
Advanced(50,000)1 分钟44,640是,但留给其他调用的余量较少

另有两条规则能让缓存更加稳健。一是请求合并(single flight):当缓存在高负载下过期时,由第一个请求获取新价格,并发请求等待这个结果,而不是各自调用 API。二是出错时沿用旧价格(serve stale on error):如果无法连接 API,就在几个小时内继续按最后一次成功获取的价格报价,并每分钟重试一次,而不是让每一次结账都失败。因服务器错误而失败的请求不计费,但在故障期间也没有理由集中发出大量这类请求。

下面的缓存保存在进程内存中,非常适合单台服务器。在无服务器(serverless)平台上或运行多个实例时,每个实例都会保留自己的副本。请将价格放入 Redis 等共享存储,或使用框架自带的数据缓存,并设置相同的过期时间。

TypeScript 实现

下面的模块可在 Node.js 18 或更高版本上运行,没有任何依赖。它获取每克黄金的欧元价格,缓存到下一个套餐更新时间点,并根据重量、纯度系数和加工费计算出价格明细:

gold-quote.ts
// gold-quote.ts — Node 18+。API 密钥只保存在服务器端。
const API_KEY = process.env.METALAPI_KEY!;
const CURRENCY = "EUR";
const PLAN_INTERVAL_S = 600; // Standard 套餐:价格每 10 分钟更新一次
const MIN_TTL_S = 0;         // Lite 套餐:设为 3600,以保持在每月 1,000 次请求以内

interface GoldPrice {
  perGram: number;   // 纯金(24K),每克的 CURRENCY 价格
  timestamp: number; // 价格数据的时间(Unix 秒)
  fetchedAt: number;
  expiresAt: number;
}

let cached: GoldPrice | null = null;
let inflight: Promise<GoldPrice> | null = null;

// 价格只在套餐间隔的整数倍时刻变化(从 00:00 UTC 起算)。
function nextBoundary(ms: number): number {
  const step = PLAN_INTERVAL_S * 1000;
  return Math.floor(ms / step) * step + step;
}

async function fetchGold(): Promise<GoldPrice> {
  const url = `https://metalapi.com/api/v1/latest?base=${CURRENCY}&currencies=XAU&unit=gram`;
  const res = await fetch(url, { headers: { "X-API-Key": API_KEY }, signal: AbortSignal.timeout(10_000) });
  const body = await res.json();
  if (!body.success) throw new Error(`MetalAPI ${body.error.code}: ${body.error.info}`);
  const now = Date.now();
  return {
    perGram: body.rates[`${CURRENCY}XAU`],
    timestamp: body.timestamp,
    fetchedAt: now,
    expiresAt: nextBoundary(now + MIN_TTL_S * 1000) + 5_000, // 留出几秒余量
  };
}

export function goldPerGram(): Promise<GoldPrice> {
  if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached);
  // 请求合并:并发请求共享同一次 API 调用。
  inflight ??= fetchGold()
    .then((fresh) => (cached = fresh))
    .catch((err) => {
      // 最长 6 小时内继续沿用最后一次成功获取的价格;一分钟后重试。
      if (cached && Date.now() - cached.fetchedAt < 6 * 3600_000) {
        cached.expiresAt = Date.now() + 60_000;
        return cached;
      }
      throw err;
    })
    .finally(() => (inflight = null));
  return inflight;
}

export type MakingCharge =
  | { type: "per_gram"; amount: number }
  | { type: "percent"; rate: number } // 0.08 = 金属价值的 8%
  | { type: "flat"; amount: number };

const money = (n: number) => Math.round(n * 100 + Number.EPSILON) / 100;

export async function quote(grams: number, purity: number, making: MakingCharge, vatRate: number) {
  const gold = await goldPerGram();
  const metal = money(grams * gold.perGram * purity);
  const labour = money(
    making.type === "per_gram" ? grams * making.amount
      : making.type === "percent" ? metal * making.rate
      : making.amount,
  );
  const net = money(metal + labour);
  const vat = money(net * vatRate);
  return {
    currency: CURRENCY,
    grams,
    purity,
    gold_per_gram_24k: gold.perGram,
    price_time: new Date(gold.timestamp * 1000).toISOString(),
    metal,
    making: labour,
    net,
    vat,
    total: money(net + vat),
  };
}

有几行代码值得细看。nextBoundary() 向上取整到套餐间隔的下一个整数倍,因此在 Standard 套餐中,10:03 获取的价格会在 10:10:05(即下一次更新后几秒)过期,而不是在 10:13 过期。在 Lite 套餐中将 MIN_TTL_S 设为 3600 后,下一次刷新会等到至少一小时之后的第一个更新时间点,从而将用量控制在每天 24 次请求以内。.catch 分支实现的就是“出错时沿用旧价格”规则:它返回最后一次成功获取的价格,并将下一次尝试安排在一分钟之后。这样一来,故障期间每分钟最多只有一次失败请求,而不是每位访客都触发一次。

通过一个小型 HTTP 端点对外提供服务。请严格校验输入:只接受您所销售的纯度和合理的重量范围。加工费和增值税率属于服务器端设置,切勿从客户端接收。

server.ts
// server.ts — 先 npm install express,然后运行:METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { quote } from "./gold-quote";

// 与您饰品上的印记一致的纯度系数(参见上文的 K 数表)。
const PURITY: Record<string, number> = { "24": 0.999, "22": 0.916, "21": 0.875, "18": 0.75, "14": 0.585, "9": 0.375 };
const app = express();

app.get("/api/jewelry-quote", async (req, res) => {
  const grams = Number(req.query.grams);
  const purity = PURITY[String(req.query.karat)];
  if (!purity || !(grams > 0 && grams <= 2000)) {
    res.status(400).json({ error: "Use karat 24, 22, 21, 18, 14 or 9 and grams between 0 and 2000." });
    return;
  }
  try {
    // 加工费和增值税率是服务器端设置,绝不来自客户端输入。
    res.json(await quote(grams, purity, { type: "per_gram", amount: 12 }, 0.2));
  } catch (err) {
    console.error(err);
    res.status(503).json({ error: "Gold price temporarily unavailable." });
  }
});

app.listen(3000, () => console.log("Try http://localhost:3000/api/jewelry-quote?karat=18&grams=5.2"));

请求 /api/jewelry-quote?karat=18&grams=5.2 会返回类似下面的明细(金价为示例):

json
{
  "currency": "EUR",
  "grams": 5.2,
  "purity": 0.75,
  "gold_per_gram_24k": 110,
  "price_time": "2026-10-24T09:41:32.000Z",
  "metal": 429,
  "making": 62.4,
  "net": 491.4,
  "vat": 98.28,
  "total": 589.68
}

Python 实现

Python 版本采用相同的设计。它使用 Decimal 处理金额,避免产生二进制浮点误差;并使用一把锁,让多线程服务器中的并发请求共享同一次 API 调用。为保持简洁,它只支持按克计的加工费:

gold_quote.py
# gold_quote.py — Python 3.9+,pip install requests
import math
import os
import threading
import time
from decimal import ROUND_HALF_UP, Decimal

import requests

API_KEY = os.environ["METALAPI_KEY"]
CURRENCY = "EUR"
PLAN_INTERVAL_S = 600  # Standard 套餐:价格每 10 分钟更新一次
MIN_TTL_S = 0          # Lite 套餐:设为 3600,以保持在每月 1,000 次请求以内

_lock = threading.Lock()
_cache = {"per_gram": None, "timestamp": 0, "fetched": 0.0, "expires": 0.0}


def _next_boundary(t: float) -> float:
    return (math.floor(t / PLAN_INTERVAL_S) + 1) * PLAN_INTERVAL_S


def gold_per_gram() -> tuple[Decimal, int]:
    """纯金(24K)每克的 CURRENCY 价格,以及该价格的 Unix 时间。"""
    with _lock:  # 同一时间只发出一次 API 调用;等待的线程复用其结果
        now = time.time()
        if _cache["per_gram"] is not None and now < _cache["expires"]:
            return _cache["per_gram"], _cache["timestamp"]
        try:
            r = requests.get(
                "https://metalapi.com/api/v1/latest",
                params={"base": CURRENCY, "currencies": "XAU", "unit": "gram"},
                headers={"X-API-Key": API_KEY},
                timeout=10,
            )
            body = r.json()
            if not body.get("success"):
                raise RuntimeError(f"MetalAPI {body['error']['code']}: {body['error']['info']}")
        except Exception:
            if _cache["per_gram"] is not None and now - _cache["fetched"] < 6 * 3600:
                _cache["expires"] = now + 60  # 沿用最后一次成功获取的价格,一分钟后重试
                return _cache["per_gram"], _cache["timestamp"]
            raise
        _cache.update(
            per_gram=Decimal(str(body["rates"][f"{CURRENCY}XAU"])),
            timestamp=body["timestamp"],
            fetched=now,
            expires=_next_boundary(now + MIN_TTL_S) + 5,
        )
        return _cache["per_gram"], _cache["timestamp"]


def money(x: Decimal) -> Decimal:
    return x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def quote(grams: Decimal, purity: Decimal, making_per_gram: Decimal, vat_rate: Decimal) -> dict:
    per_gram, ts = gold_per_gram()
    metal = money(grams * per_gram * purity)
    making = money(grams * making_per_gram)
    net = metal + making
    vat = money(net * vat_rate)
    return {
        "currency": CURRENCY,
        "gold_per_gram_24k": str(per_gram),
        "price_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)),
        "metal": str(metal),
        "making": str(making),
        "net": str(net),
        "vat": str(vat),
        "total": str(net + vat),
    }

任何 Web 框架都可以对外提供这项服务。在 FastAPI 中,普通的 def 端点运行在线程池中,这正是需要那把锁的原因:

app.py
# app.py — 先 pip install fastapi uvicorn requests,然后运行:METALAPI_KEY=mk_live_... uvicorn app:app
from decimal import Decimal

from fastapi import FastAPI, HTTPException, Query

from gold_quote import quote

# 与您饰品上的印记一致的纯度系数。
PURITY = {24: Decimal("0.999"), 22: Decimal("0.916"), 21: Decimal("0.875"),
          18: Decimal("0.750"), 14: Decimal("0.585"), 9: Decimal("0.375")}
app = FastAPI()


@app.get("/api/jewelry-quote")
def jewelry_quote(karat: int, grams: Decimal = Query(gt=0, le=2000)):
    if karat not in PURITY:
        raise HTTPException(400, "Use karat 24, 22, 21, 18, 14 or 9.")
    try:
        return quote(grams, PURITY[karat], making_per_gram=Decimal("12.00"), vat_rate=Decimal("0.20"))
    except Exception:
        raise HTTPException(503, "Gold price temporarily unavailable.")

Python 版本以字符串形式返回金额,TypeScript 版本则返回数字。如果报价要写入订单系统,请将金额存储为十进制字符串或以分为单位的整数,而不是浮点数。

计算示例

下面逐步计算这枚 18K 戒指的价格,示例金价为每克 €110.00,加工费为每克 €12,增值税率为 20%:

步骤计算金额
黄金,24K 每克rates.EURXAU€110.00
纯度18K,印记 7500.75
金属价值5.2 g × €110.00 × 0.75€429.00
加工费5.2 g × €12.00€62.40
净价€429.00 + €62.40€491.40
增值税€491.40 × 20%€98.28
总价€589.68

再看一条 10 克的 22K 项链,金价相同,加工费按 8% 收取。按纯度 22/24 计算,金属价值为 €1,008.33,加工费为其 8%(€80.67),净价为 €1,089.00,含增值税的总价为 €1,306.80。如果改用印记 916 作为系数,金属价值则为 €1,007.60。

只有金属价值随金价变动,因此总价的变动幅度小于金价本身。如果金价上涨 1% 至每克 €111.10,这枚 18K 戒指的金属价值将变为 €433.29,总价变为 €594.83,上涨 €5.15,即 0.87%。如果采用按比例收取的加工费,总价会更紧密地跟随金价变动。

上线检查清单

  • 将 API 密钥保存在服务器的环境变量中。计算器端点只返回价格,绝不返回密钥。
  • 在每份报价中显示 price_time,让客户和员工了解金价有多新。
  • 下单时锁定价格:将金价及其时间戳、纯度和每一行金额随订单一同保存,而不是事后重新计算。
  • 周末黄金市场休市,价格保持不变属于正常现象,并不是缓存出了问题。
  • 使用 usage 端点跟踪用量(该端点不计入配额),并在当月配额用尽之前提醒自己。
  • 遇到错误 105(每月配额已用完)时,继续提供缓存的价格并通知您的团队。配额于每月 1 日 00:00 UTC 重置,升级套餐则立即生效。
  • 如需对价格的大幅波动做出反应(例如重新为商品目录定价),请使用价格提醒与 Webhook,而不是提高轮询频率。

在定价页面比较各套餐及其配额,或创建免费账户获取密钥,用真实价格试一试这个计算器。

用您自己的数据试一试

创建免费账户,两分钟内即可发起第一次请求。每月 100 次请求,无需信用卡。

继续阅读