用实时金价打造首饰价格计算器
用 TypeScript 和 Python 在服务器端构建首饰价格计算器:按克计的实时金价、K 金纯度、加工费、增值税,以及不超出 API 配额的缓存策略,附完整代码。
计算器的功能
首饰的定价通常取决于四项输入:饰品的重量、所用黄金的纯度、当前金价以及首饰商的加工费,最后再加上税费。要为每个商品页面或每次报价请求完成这样的计算,计算器需要一个以店铺所用货币计价的每克金价。而且,获取这个价格必须足够快、不能超出 API 配额,也不能把 API 密钥暴露给浏览器。
本教程分别用 TypeScript(Node.js)和 Python 构建一个小型服务器端服务。浏览器或您的商城后端将重量和 K 数发送到您自己的端点,服务器将其与从 MetalAPI 获取并缓存的金价相结合,返回完整的价格明细。计算公式如下:
金属价值 = 重量(克)× 每克金价(24K)× 纯度
加工费 = 按克计、按金属价值的百分比计,或固定金额
净价 = 金属价值 + 加工费
增值税 = 净价 × 增值税税率
总价 = 净价 + 增值税现货金价是批发参考价格。零售价格还包含设计、宝石、损耗和利润等因素,而回购价格通常低于现货价。请将本计算器视为定价中与金属相关的部分,再在此基础上叠加您自己的业务规则。
以您的货币计价的每克金价
/latest 端点可以用任意基础货币返回价格,金属价格还可以使用任意重量单位。只需一次请求,即可获得 1 克纯金的欧元价格:
curl "https://metalapi.com/api/v1/latest?base=EUR¤cies=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 | 常见印记 |
|---|---|---|
| 24K | 1.0000 | 999 |
| 22K | 0.9167 | 916 |
| 21K | 0.8750 | 875 |
| 18K | 0.7500 | 750 |
| 14K | 0.5833 | 585 |
| 9K | 0.3750 | 375 |
以一枚 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 — 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}¤cies=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 — 先 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 会返回类似下面的明细(金价为示例):
{
"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 — 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 — 先 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,印记 750 | 0.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,而不是提高轮询频率。
继续阅读
黄金价格 API:开发者完全指南
黄金价格 API 使用指南:获取密钥,用 curl、JavaScript 和 Python 发起调用,正确解读汇率格式,并妥善处理历史数据、错误、配额与缓存。
如何在网站上添加实时金价小组件
在 WordPress、Shopify 或纯 HTML 页面中添加实时金价小组件:在 MetalAPI 控制台中创建小组件,选择品种、布局和主题,然后粘贴两行代码即可完成。
K 金价格详解:24K、22K、18K 及其计算方法
24K、22K 和 18K 分别代表什么,916、750 等成色印记如何与 K 数对应,怎样计算每克 K 金价格,以及如何通过金价 API 直接获取各 K 数的金价。