在车主服务 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)





