用一个二维码实现多平台智能跳转

二维码只能编码一个 URL,但不同设备、不同应用环境需要访问不同的目标。无论是将用户导向 App Store、Google Play,还是在微信与支付宝之间选择支付入口,都可以采用同一种技术思路:二维码固定指向可控的短链,由服务端根据请求环境动态决定下一步操作。

核心原理

二维码指向统一入口,例如 https://go.example.com/app。服务端接收到请求后,根据 HTTP User-Agent 判断客户端类型:iOS 跳转 App Store,Android 跳转 Google Play;在微信或支付宝环境中,则进入对应的支付流程。无法识别的请求统一进入官网或支付方式选择页。

这里需要注意,User-Agent 只是辅助判断依据,并非可靠的身份认证机制。爬虫也可能携带移动设备标识,因此应优先识别常见爬虫,再执行设备路由。对于无法准确识别的环境,采用通用页面兜底,比错误跳转更稳妥。

关键工程设计

  1. 使用 302 临时重定向。 301 可能被浏览器或 CDN 长期缓存,而 302 更适合目标地址可能变化的短链。
  2. 控制缓存策略。 同一 URL 根据 User-Agent 返回不同结果时,应设置 Vary: User-Agent,并合理配置 CDN 缓存规则,避免不同客户端复用错误的跳转结果。
  3. 处理查询参数。 跳转官网时,可以保留来源追踪参数;跳转应用商店或支付接口时,则应按照目标平台的参数规范重新构造 URL,避免参数冲突。
  4. 隔离路由域名。 独立使用短链域名,可以避免主站配置的 Universal Links 或 Android App Links 抢先拦截请求。
  5. 严格匹配路径。 对不支持的路径直接返回 404,只容忍明确允许的尾部斜杠,避免错误链接被静默处理。

总结

这种模式的核心不是二维码本身,而是将静态入口与动态路由分离。通过服务端识别请求环境、执行临时重定向并设置合理的缓存策略,同一张二维码就能适配多种设备和应用环境,同时保留后续调整目标地址的能力。

#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
import re

HOST = "0.0.0.0"
PORT = 8080

# 替换为实际目标地址
IOS_STORE = "https://apps.apple.com/"
ANDROID_STORE = "https://play.google.com/store"
WEBSITE = "https://example.com/"

# 演示用支付入口。生产环境应由服务端创建支付订单。
WECHAT_PAY = "https://pay.example.com/wechat"
ALIPAY_PAY = "https://pay.example.com/alipay"
PAYMENT_PAGE = "https://pay.example.com/"

BOT_RE = re.compile(
    r"bot|crawler|spider|slurp|googlebot|bingbot|"
    r"baiduspider|yandex|duckduckbot",
    re.IGNORECASE,
)


def detect_target(path: str, ua: str) -> str | None:
    """根据固定路径和客户端特征决定跳转目标。"""

    # 仅允许指定路径,忽略末尾斜杠。
    path = path.rstrip("/") or "/"

    if path not in ("/app", "/pay"):
        return None

    # 优先处理爬虫,避免 Googlebot 等被误判为手机用户。
    if BOT_RE.search(ua):
        return WEBSITE

    if path == "/app":
        if "iPhone" in ua or "iPod" in ua:
            return IOS_STORE

        if "Android" in ua:
            return ANDROID_STORE

        # iPadOS Safari 可能使用桌面版 UA,无法可靠识别时回官网。
        return WEBSITE

    # 国内支付客户端识别:仅作为路由线索。
    if "MicroMessenger" in ua:
        return WECHAT_PAY

    if "AlipayClient" in ua or "Alipay" in ua:
        return ALIPAY_PAY

    # 普通浏览器、桌面端及未知客户端。
    return PAYMENT_PAGE


class RedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        request = urlsplit(self.path)
        path = request.path.rstrip("/") or "/"

        target = detect_target(path, self.headers.get("User-Agent", ""))

        if target is None:
            self.send_error(404, "Not Found")
            return

        # 只对官网回退保留查询参数,避免污染商店或支付链接。
        if target == WEBSITE and request.query:
            target = f"{target}?{request.query}"

        self.send_response(302)
        self.send_header("Location", target)
        self.send_header("Vary", "User-Agent")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", "0")
        self.end_headers()

    def log_message(self, fmt, *args):
        print(f"{self.client_address[0]} - {fmt % args}")


if __name__ == "__main__":
    server = ThreadingHTTPServer((HOST, PORT), RedirectHandler)
    print(f"Redirect server listening on {HOST}:{PORT}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
-> % vim /tmp/server.py
-> % chmod +x /tmp/server.py
-> % /tmp/server.py
Redirect server listening on 0.0.0.0:8080
# iPhone
curl -Sis -A 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)' http://127.0.0.1:8080/app

# Android
curl -Sis -A 'Mozilla/5.0 (Linux; Android 15)' http://127.0.0.1:8080/app

# Googlebot
curl -Sis -A 'Mozilla/5.0 (Linux; Android 15) Googlebot/2.1' http://127.0.0.1:8080/app

# Unknown path
curl -Sis http://127.0.0.1:8080/apps
  1. 需要补充 HEAD 方法。
  2. 支付的话,实际还需要订单创建,生成平台需要的参数,还有签名验证之类的流程,这里只演示路由。
  3. 实际业务前面会有 Nginx 之类的反向代理,需要重点确认一下响应中的 Location、Vary 和 Cache-Control 三个响应头。
如果你有魔法,你可以看到一个评论框~