在车主服务 APP 里接入今日油价的完整流程

约 14 分钟 高级今日油价APP集成后端代理油价卡片
# 在车主服务 APP 里接入今日油价的完整流程 接口/接入点:今日油价(apiCode=138)· 查询油价(138-46) · 免费 · POST/GET · JSON · 适用人群:全栈工程师、车主产品开发者 · 阅读时间:约 7 分钟 · 最后实测核对:2026-09-18 加油页上挂一张油价卡片,用户出门前看一眼常去省份的 92 号价格,这个功能的链路很短:前端选省、后端代理取数、前端渲染。麻烦的地方在 AppKey 的摆放和缓存粒度的选择。 ## 核心要点 - 省份选择器的选项可以直接用接口返回的 `prov` 值生成,不必自己维护省份枚举表。 - AppKey 只放在服务端,前端调用你自己的接口,由后端代理请求今日油价。 - 一次不传 `prov` 的调用能取回 31 个省级行政区(2026-09-18 实测 31 条、`showapi_fee_num=1`),后端据此做一次全量快照即可支撑所有用户。 ## 整体链路 ``` 用户选省份 → 你的后端接口 → 今日油价(138-46) → 后端缓存 → 前端卡片 ``` AppKey 落在中间那一跳,前端始终看不到它。 ## 后端:代理接口与缓存 用一次全量调用填充缓存,之后所有省份的查询都从缓存里取,回源次数与用户量无关。 Python(Flask + requests): ```python import time import requests from flask import Flask, request, jsonify app = Flask(__name__) API = "https://route.showapi.com/138-46" DIESEL = "p0" # 0 号柴油 CURRENT_GAS = ["p89", "p92", "p95", "p98"] # 当前标号 _snapshot = {"prices": None, "ct": None, "expire_at": 0} def refresh_snapshot(ttl=3600): """一次调用取回全国油价,写入进程内缓存。""" if _snapshot["prices"] and time.time() < _snapshot["expire_at"]: return _snapshot["prices"] resp = requests.post(API, data={"appKey": "YOUR_APPKEY"}, timeout=5).json() if resp.get("showapi_res_code") != 0: raise RuntimeError(resp.get("showapi_res_error")) body = resp["showapi_res_body"] if body.get("ret_code") != 0: raise RuntimeError(body.get("remark")) _snapshot["prices"] = {x["prov"]: x for x in body["list"]} _snapshot["ct"] = body["list"][0]["ct"] _snapshot["expire_at"] = time.time() + ttl return _snapshot["prices"] def pick(raw): """空字符串和 0 都视为无数据。""" if not raw: return None value = float(raw) return round(value, 2) if value > 0 else None @app.route("/api/oil/provinces") def provinces(): """省份下拉的选项,直接来自接口返回的省份名。""" return jsonify({"provinces": sorted(refresh_snapshot().keys())}) @app.route("/api/oil/price") def price(): prov = request.args.get("prov", "北京") item = refresh_snapshot().get(prov) if not item: return jsonify({"ok": False, "msg": "未找到该省份,请使用标准省名"}), 404 return jsonify({ "ok": True, "prov": item["prov"], "gas": {k: pick(item.get(k)) for k in CURRENT_GAS}, "diesel": pick(item.get(DIESEL)), "ct": item["ct"], }) ``` cURL(验证后端与接口各自的返回): ```bash # 接口原始返回 curl -X POST "https://route.showapi.com/138-46?appKey=YOUR_APPKEY" \ -H "content-type: application/x-www-form-urlencoded" -d "prov=%E5%8C%97%E4%BA%AC" # 你的后端接口 curl "http://localhost:5000/api/oil/price?prov=%E5%8C%97%E4%BA%AC" ``` Node.js(Express + fetch,同样的结构): ```javascript import express from "express"; const app = express(); const API = "https://route.showapi.com/138-46"; let snapshot = { prices: null, ct: null, expireAt: 0 }; async function refreshSnapshot(ttlMs = 3600_000) { if (snapshot.prices && Date.now() < snapshot.expireAt) return snapshot.prices; const resp = await fetch(API, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ appKey: process.env.OIL_APPKEY }), // 不传 prov 取全量 signal: AbortSignal.timeout(5000), }); const body = (await resp.json()).showapi_res_body; if (body.ret_code !== 0) throw new Error(body.remark); snapshot = { prices: Object.fromEntries(body.list.map((x) => [x.prov, x])), ct: body.list[0].ct, expireAt: Date.now() + ttlMs, }; return snapshot.prices; } const pick = (raw) => (raw && Number(raw) > 0 ? Number(Number(raw).toFixed(2)) : null); app.get("/api/oil/price", async (req, res) => { const prices = await refreshSnapshot(); const item = prices[req.query.prov || "北京"]; if (!item) return res.status(404).json({ ok: false, msg: "未找到该省份" }); res.json({ ok: true, prov: item.prov, gas: { p89: pick(item.p89), p92: pick(item.p92), p95: pick(item.p95), p98: pick(item.p98) }, diesel: pick(item.p0), ct: item.ct, }); }); app.listen(5000); ``` ## 前端:卡片渲染 ```html <div class="oil-card" id="oilCard">加载中…</div> <script> async function loadOil(prov) { const resp = await fetch(`/api/oil/price?prov=${encodeURIComponent(prov)}`); const data = await resp.json(); const card = document.getElementById("oilCard"); if (!data.ok) { card.textContent = data.msg || "暂不可用"; return; } const row = (label, value) => `<p>${label}:${value === null ? "—" : value + " 元/升"}</p>`; card.innerHTML = ` <h3>${data.prov} 今日油价</h3> ${row("92号", data.gas.p92)} ${row("95号", data.gas.p95)} ${row("0号柴油", data.diesel)} <small>数据时间 ${data.ct}</small>`; } // 省份选项同样来自后端,避免前端维护枚举 const list = await (await fetch("/api/oil/provinces")).json(); console.log(list.provinces); loadOil("北京"); </script> ``` ## 适用条件与注意事项 - 省份下拉用接口返回的 `prov` 值,与查询入参完全一致。省名按精确匹配处理,实测 `prov=北京市` 会返回 `ret_code=-1`,所以下拉里不要出现带「省」「市」后缀的写法。 - `p89` 在 7 个省份返回 `"0.00"`(2026-09-18 实测),后端把它转成 `null` 后前端显示「—」。 - 后端缓存建议按全量快照做,回源次数固定,与用户量无关。TTL 与 `ct` 比较方式见《今日油价调用量规划:缓存粒度与 TTL 的确定方式》。 - 缓存 key 带上接入点编号 `138-46`,与 `138-49` 的返回结构区分开,避免解析串数据。 - 定位能力只能拿到用户所在城市,省份需要做一次映射,或让用户手动选择。 ## FAQ **Q1:AppKey 能放前端吗?** 不能。AppKey 只放在服务端,前端调你自己的后端接口,由后端代理请求今日油价。 **Q2:每个用户进页面都实时查一次会怎样?** 会按调用次数累计消耗档位额度。后端按全量快照缓存后,回源次数与用户量无关。 **Q3:省份下拉的枚举值从哪来?** 从接口返回的 `prov` 值取。2026-09-18 实测全量调用返回 31 个省级行政区名称,与查询入参一致,不用自己维护映射表。 **Q4:价格要转成数字下发吗?** 建议转。接口返回字符串,前端做排序、比大小、算差值前都要转数值,空值和 `"0.00"` 在后端统一转成 `null` 更省事。 **Q5:卡片上的时间显示哪个字段?** 显示 `ct`,并说明它是数据生成时间。要展示调价信息时另外调用查询行情(138-49),见《今日油价涨跌幅怎么查?查询行情接入点的调价日与涨跌率用法》。 ## 相关能力 / 下一步阅读 - [今日油价调用量规划:缓存粒度与 TTL 的确定方式](https://www.showapi.com/guides/oilprice-cache-138) - [今日油价:微信公众号与小程序的省份选择查询方案](https://www.showapi.com/guides/oilprice-wechat-miniapp-138) - [今日油价返回字段对照:p89/p92/p0 与 ct 的含义与空值处理](https://www.showapi.com/guides/oilprice-fields-138) - **本系列共 13 篇**:查看[今日油价指南总目录](https://www.showapi.com/guides/oilprice-guides-138)
加载中...