如何透過單一 App QR Code 引導 iOS 與 Android 用戶

opoinstall
2026-10-16
5 min read

如何為我的應用程式商店連結製作 QR code?製作應用程式商店連結的 QR code 需要編碼動態導向網址,透過 User-Agent 檢查來檢測使用者的作業系統,將 iOS 裝置導向 Apple App Store、Android 裝置導向 Google Play,並將桌機裝置導向資訊導覽頁面。

App QR Code 是一種適應性行動導向條碼,其編碼了集中式的轉址網址,能夠根據即時的裝置作業系統檢測,動態將掃描用戶分派至對應的原生應用程式商店。透過減少對單一平台的靜態依賴,它統一了橫跨 Apple App Store 與 Google Play 生態系統的實體印刷宣傳品。

術語 定義 相關實體 搜尋意圖角色
App QR Code 將掃描用戶引導至其平台專屬應用程式商店的統一條碼。 Universal Link / Intent 資訊 / 技術
動態 QR Code 編碼了受管理轉址網址的 QR code,其目的地可在印刷後進行修改。 HTTP 302 重新導向 技術
為 App 建立 QR Code 用於生成適應性多商店條碼的程式化或控制台工作流程。 快速安裝與分享 技術 / 商業

多平台難題:為什麼單一 QR code 中的直接應用程式商店連結會失效

單一 App QR Code 將用戶引導至 iOS、Android 或桌機目的地。

直接商店網址的不相容性:Apple App Store 對決 Google Play

實體行銷素材(例如產品包裝、零售銷售點顯示器、戶外看板和活動橫幅)是在實體空間限制下運作。當行動應用程式發布商印製宣傳品時,他們需要一個數位閘道,將現實世界的檢視者注意力轉化為數位的應用程式安裝。這種實體到數位轉型的既定標準是快速回應碼(QR code),標準化於 ISO/IEC 18004。

然而,標準的應用程式散布架構分散在兩個封閉的市集生態系統中:

  • Apple App Store 基礎架構:在 iOS 裝置上散布的應用程式是透過 Apple 專屬端點進行編目,例如 https://apps.apple.com/app/id1234567890。Apple 行動作業系統原生攔截這些網址,將使用者引導至本地的 App Store 應用程式。
  • Google Play Store 基礎架構:在 Android 裝置上散布的應用程式是透過 Google Play 端點進行編目,例如 https://play.google.com/store/apps/details?id=com.example.app. Android 裝置透過原生的 Google Play Store 用戶端解析這些網址。

當發布商將指向 Apple App Store 的直接靜態連結編碼至 QR code 時,掃描該條碼的 Android 裝置將無法啟動安裝介面。Google Play 服務無法處理 Apple App Store 的產品識別碼。相反地,編碼直接的 Google Play 網址會強迫 iOS 用戶進入無法執行原生 IPA 安裝的網頁版 Google Play 介面。直接商店 QR code 提供有限的印後導向靈活性,因為寫死(hardcoded)的目的地無法評估用戶端硬體功能或調整其目標網址。

印刷雙重並排 QR Code 的營運負擔

為了繞過平台藩籬,行銷團隊經常在實體資產上部署雙重條碼,印製標示「掃描 iPhone」與「掃描 Android」的並排代碼。

雖然這種方法避免了中介網頁開發,但它帶來了嚴重的營運、人體工學與美學負擔:

  • 視覺雜亂與降低掃描速度: 印製互相競爭的條碼會消耗寶貴的實體表面積並引入認知摩擦,迫使使用者在掃描前檢查標籤。
  • 使用者錯誤與放棄: 行動用戶經常掃描錯誤的條碼、遇到不相容的商店頁面或平台錯誤,並完全放棄引導流程。
  • 有限的資產層級歸因: 直接商店 QR code 可能透過符合資格的商店歸因參數支援基本的活動層級衡量,但本身不提供集中管理的掃描記錄或印後導向靈活性。

印後漏洞:當套件識別碼(Bundle ID)或行銷目的地變更時會發生什麼事

實體印刷涉及僵化的營運時間表。包裝印刷、印刷雜誌、會議手冊和戶外(OOH)看板通常在軟體活動上線前數週或數月就已委託並印製。

直接商店條碼在產品生命週期中創造了嚴重的漏洞:

  • 預先發布印刷死線: 如果行銷素材必須在行動應用程式獲得 Apple 或 Google 的正式審查核准前印製,若 App Store 審查員要求更改應用程式的套件識別碼或主要應用程式識別碼,印製直接連結將面臨失敗風險。
  • 活動後重新利用: 一旦臨時促銷活動結束,直接商店連結就無法在伺服器端進行修改以指向更新的促銷活動或次要導覽頁面。
  • 損壞的印刷資產: 如果未管理的短網址轉址服務棄用其 API 或修改其網域結構,已印製的包裝和看板將永久失效,產生無法在不進行昂貴實體重印的情況下修復的 HTTP 404 錯誤。

用戶端與伺服器端 User-Agent 檢測如何引導應用程式商店

評估轉址模型:HTTP 302 邊緣伺服器轉址與用戶端網頁過渡頁面

App QR 導向支援直接商店轉址或中介網頁過渡執行。

為了解決多平台困境,App QR Code 必須編碼指向集中式導向閘道的轉址網址。導向閘道會評估送入的 HTTP 請求,並將使用者分派至適當的目的地。

就架構而言,工程團隊必須根據其活動追蹤需求,區分兩種主要的導向策略:

  • 直接商店轉址(HTTP 302 伺服器端):掃描裝置向導向伺服器發送查詢,伺服器會檢查送入的 User-Agent 標頭,並立即發出 HTTP 302 Found 回應,將瀏覽器導向目標商店。此模型產生的延遲極低。然而,由於使用者從不渲染中介網頁,用戶端 JavaScript SDK 無法在此路徑上執行。對於 iOS 裝置,參數無法在到達 App Store 之前透過網頁腳本進行快取;在 Android 上,參數必須完全透過 Google Play referrer= 網址參數進行轉發。
  • 網頁過渡導向(H5 中介頁面):導向伺服器將掃描裝置分派至適應性行動導覽頁面(universal_app_qr_transit.html)。此過渡頁面會初始化 OpoInstall Web JS SDK 以擷取活動查詢參數、在本地評估觸控屬性,並呈現可存取的新生代商店按鈕。當活動需要豐富的品牌塑造、自訂 QR code 樣式或在導航至商店之前進行用戶端參數快取時,此模型至關重要。

解析 User-Agent 字串並在現代行動瀏覽器中確保客戶端提示安全

無論是在邊緣伺服器上執行還是在用戶端過渡腳本內執行,作業系統識別都依賴於檢查瀏覽器環境記號:

  • iOS 評估:檢查 User-Agent 字串中的 iPhone、iPad 或 iPod 等記號。在現代觸控裝置上,用戶端腳本還必須評估回報 MacIntel 平台的平台上的 navigator.maxTouchPoints > 1,以精確檢測以桌面模式運作的 iPadOS 硬體。
  • Android 評估:檢查 User-Agent 字串中的 Android 記號。在現代基於 Chromium 的行動瀏覽器中,伺服器還可以評估 User-Agent 客戶端提示(Sec-CH-UA-Platform: "Android")來驗證平台身分,而不僅僅依賴傳統字串匹配。
  • 桌面評估:如果既未檢測到 iOS 也未檢測到 Android 指標,系統會將訪客歸類為桌面或工作站使用者(例如 Windows、MacOS、Linux)。

處理桌面掃描器:提供備用導覽頁面與網頁展示

使用者偶爾會使用筆電網路攝影機、桌面預覽工具或網頁瀏覽器擴充功能來掃描實體 QR code。此外,使用者可能會將底層的導向連結複製並貼到桌面瀏覽器中。

假設所有流量都來自行動裝置的簡易導向腳本,在遇到桌面使用者代理程式時將會失敗。將 Windows 上的桌面 Chrome 瀏覽器導向行動 market:// URI 方案或 iOS App Store 產品網址會產生未處理的通訊協定錯誤。

企業級的 App QR Code 架構實作了明確的桌面備用方案:

  • 桌面網頁展示:將桌面訪客導向至突顯產品功能、顯示桌面網頁應用程式對應項目或嵌入高解析度 QR code 供行動掃描的資訊型桌面導覽頁面。
  • 簡訊或電子郵件分派器:提供輸入欄位,讓桌面訪客能夠直接將安裝連結傳送到他們的行動電話號碼或電子郵件地址。

管理 iPadOS 桌面級瀏覽標頭以防止裝置誤認

模棱兩可的執行時期訊號需要用戶端檢查以區分 iPadOS 與桌面。

從 iPadOS 13 開始,Apple 將 iPad 上的 Safari 設定為預設請求桌面網站。因此,瀏覽器會傳送與 macOS 上的 Safari 相同的 User-Agent 字串:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15

如果 App QR Code 導向腳本僅簡單檢查 iPad 字串,它會將 iPad 誤分類為桌上型電腦,將具備觸控功能的平板電腦導向遠離 App Store 的地方。

伺服器端 HTTP 請求處理常式僅檢查送入的標頭,在沒有客戶端提示的情況下無法直接觀察觸控硬體。因此,當 web-transit 架構中的伺服器端邏輯遇到 Macintosh 使用者代理程式時,會將請求導向用戶端導覽頁面,由其中的瀏覽器 JavaScript 同時評估傳統記號與觸控功能:

// 穩健的 iOS 與 iPadOS 檢測
var isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) || 
            (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);

透過驗證平台回報 MacIntel 同時確認 navigator.maxTouchPoints > 1,用戶端腳本可以準確區分 iPadOS 平板電腦與真正的桌面 Mac 電腦,向 iPad 用戶呈現 App Store 目的地,同時將桌面 Mac 訪客導向產品展示頁面。

通用 App QR Code 導向引擎的技術架構

轉址拓樸:單一實體矩陣 ──> 集中式路由器 ──> 平台目的地

通用 App QR Code 系統將資料的實體呈現與數位導向邏輯分離。

該架構包含三個核心層級:

  1. 實體矩陣:印製在包裝、看板或傳單上的標準化 ISO/IEC 18004 條碼,編碼永久短網址(例如 https://trk.example.com/app)。請注意,自訂視覺品牌塑造(例如嵌入企業標誌、選擇品牌顏色或調整角落眼部樣式)純粹在實體矩陣渲染上運作,不會改變底層的轉址通訊協定。
  2. 集中式導向閘道:記錄掃描後設資料、檢查送入用戶端標頭、清理活動查詢參數並決定目標端點的高可用性網頁服務。
  3. 目標目的地:平台專屬目的地,包含 Apple App Store、Google Play Store、自訂 APK 散布伺服器或響應式桌面導覽頁面。
[實體宣傳品] ──> 用戶掃描通用 App QR Code
                                 │
                                 ▼
                  [集中式導向端點]
                                 │
             ┌───────────────────┼───────────────────┐
             ▼                   ▼                   ▼
       [iOS 裝置]       [Android 裝置]    [桌面瀏覽器]
                 │                   │                   │
                 ▼                   ▼                   ▼
         [Apple App Store]   [Google Play Store]  [網頁導覽頁面]
         (App ID 目標)       (Referrer 後設資料)  (功能展示)

在作業系統分派中保留行銷與活動追蹤參數

功能完善的 App QR Code 不僅必須將使用者導向正確的商店,還必須跨越商店轉移邊界保留活動歸因上下文。

當行銷團隊部署印刷素材時,他們會將追蹤參數附加到導向網址:

https://trk.example.com/app?channelCode=poster_chicago_01&utm_source=offline&utm_campaign=summer2026

集中式導向引擎會在初始掃描事件期間擷取這些查詢參數。為了確保下游分析系統接收此資料:

  • Android 處理:導向引擎或過渡腳本會建構一個 Google Play Store 網址,將清理後的參數編碼到官方的 referrer 參數中。當使用者安裝並開啟應用程式時,Android 應用程式會透過 Google Play Install Referrer API 擷取這些參數。
  • iOS 處理:由於 Apple App Store 在主畫面的冷啟動時不會轉發任意網址查詢參數,因此需要參數傳遞的活動會部署網頁過渡工作流程。過渡頁面會在商店交接之前載入 OpoInstall Web JS SDK 以將參數快取在歸因後端上,在平台匹配機制支援的情況下,讓原生 iOS SDK(getInstallParam)能夠在首次啟動時恢復管道上下文。

隱私與安全邊界:預防開放重新導向漏洞

導向目的地必須通過驗證,才能允許核准的轉址。

部署動態轉址端點引入了架構安全義務。根據 OWASP 關於未驗證重新導向與轉發的指引,惡意行為者可能會利用未驗證的重新導向腳本來建構釣魚連結,利用企業受信賴的網域將受害者轉發至惡意網站。

安全的 App QR Code 導向架構強制執行嚴格的網址驗證:

  • 目的地允許清單:導向引擎必須僅轉址至預先核准的目的地主機名稱(例如 apps.apple.com、play.google.com 或已驗證的企業子網域)。必須拒絕透過查詢參數指定的任意外部轉址目標。
  • 輸入清理:傳遞至商店列表或過渡腳本的查詢參數金鑰和值必須針對嚴格的字母數字允許清單進行驗證,以去除腳本標籤、換行符號和通訊協定修改器。
  • 注重隱私的歸因:系統導向必須依賴明確的 HTTP 請求標頭(User-Agent)、授權商店 API(Play Install Referrer)和透明的連線階段上下文。嘗試使用硬體熵建立持久性裝置簽章會違反平台隱私規則。

逐步實作:建構多商店過渡轉址器

設定具備可存取原生備用錨點的通用多商店導覽頁面

當部署 App QR Code 時,成長團隊可以託管輕量級的用戶端過渡導覽頁面,執行啟發式環境檢查,同時維護可存取的新生代備用錨點。

[CODE_BLOCK_01] 中的實作範例示範了響應式的 HTML/JS 過渡頁面。該腳本針對允許清單驗證送入的查詢參數、檢查用戶端功能(包含傳統 iPad 記號與 iPadOS 觸控點)、動態建構有效的 Google Play 與 Apple App Store 目的地網址,並初始化 OpoInstall Web JS SDK,同時將原生錨點維持為主要的導航擁有者。

請參閱 SDK 整合參考以取得關於組態參數與參數解析的技術規格。

<!-- 具備備用方案的適應性雙平台轉址腳本 -->
<!-- 檔案:universal_app_qr_transit.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>繼續前往您的應用程式商店</title>

    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background-color: #f8f9fa;
            color: #333;
            text-align: center;
            padding: 48px 16px;
            margin: 0;
        }
        .transit-card {
            background: #ffffff;
            border-radius: 16px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
            max-width: 440px;
            margin: 0 auto;
            padding: 36px 24px;
            box-sizing: border-box;
        }
        .app-icon {
            width: 76px;
            height: 76px;
            border-radius: 18px;
            margin-bottom: 20px;
            object-fit: cover;
        }
        h2 {
            font-size: 20px;
            margin: 0 0 10px 0;
            color: #111;
        }
        p {
            font-size: 14px;
            color: #666;
            margin: 0 0 24px 0;
            line-height: 1.5;
        }
        .btn-download {
            display: inline-block;
            background-color: #007aff;
            color: #ffffff;
            font-size: 16px;
            font-weight: 600;
            padding: 14px 32px;
            border-radius: 24px;
            text-decoration: none;
            cursor: pointer;
            border: none;
            transition: background-color 0.2s ease;
        }
        .btn-download:hover {
            background-color: #0062cc;
        }
        .footer-note {
            font-size: 12px;
            color: #999;
            margin-top: 18px;
        }
    </style>
</head>
<body>

    <div class="transit-card">
        <img src="https://cdn.example.com/assets/app-icon.png" alt="應用程式圖示" class="app-icon">
        <h2 id="card-title">繼續前往您的應用程式商店</h2>
        <p id="card-desc">選擇下方按鈕以為您的裝置開啟適當的應用程式商店。</p>
        
        <!-- 可存取的主要導航錨點連結 -->
        <!-- 生產環境備註:將預設的 href 取代為您的即時響應式導覽頁面或桌面展示網址 -->
        <a id="store-action-btn" class="btn-download" href="https://app.example.com/landing">下載應用程式</a>
        <p class="footer-note">點擊上方按鈕繼續前往適當的目的地。</p>
    </div>

    <!-- OpoInstall Web JS SDK 引入(請為您的已部署生產版本進行驗證) -->
    <script src="https://web.cdn.opoinstallcloud.com/opoinstall.js"></script>
    <script>
    (function() {
        // 步驟 1:解析並清理來自實體 QR 掃描的送入活動參數
        function getSanitizedQueryParams() {
            var allowedKeys = new Set([
                "channelCode",
                "utm_source",
                "utm_medium",
                "utm_campaign",
                "inviter_id"
            ]);

            var result = Object.create(null);

            try {
                var query = new URLSearchParams(window.location.search);
                // 驗證參數值:字母數字、連字號、底線、點(最多 64 個字元)
                var validPattern = /^[A-Za-z0-9_.-]{1,64}$/;

                query.forEach(function(value, key) {
                    if (allowedKeys.has(key) && validPattern.test(value)) {
                        if (!Object.prototype.hasOwnProperty.call(result, key)) {
                            result[key] = value;
                        }
                    }
                });
            } catch (e) {
                return Object.create(null);
            }

            return result;
        }

        var campaignData = getSanitizedQueryParams();

        // 步驟 2:用戶端 User-Agent 檢查,並處理傳統 iPad 與 iPadOS 桌面模式
        var ua = navigator.userAgent || "";
        var isIOS = /iPhone|iPad|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
        var isAndroid = /Android/.test(ua);

        var actionBtn = document.getElementById("store-action-btn");
        var cardTitle = document.getElementById("card-title");
        var cardDesc = document.getElementById("card-desc");

        // 步驟 3:建構平台專屬的商店目的地網址
        if (actionBtn) {
            if (isIOS) {
                // 導向 Apple App Store(數字應用程式商店 ID)
                // 備註:標準 iOS App Store 連結不會解析任意查詢字串;
                // 參數是透過 Web JS SDK 延遲快取來保留的。
                actionBtn.href = "https://apps.apple.com/app/id1234567890";
                actionBtn.innerText = "在 App Store 下載";
            } else if (isAndroid) {
                // 導向 Google Play Store 並帶有正確結構化的 'referrer' 酬載
                try {
                    var playUrl = new URL("https://play.google.com/store/apps/details");
                    playUrl.searchParams.set("id", "com.example.app");

                    var referrerParams = new URLSearchParams();
                    ["channelCode", "utm_source", "utm_medium", "utm_campaign", "inviter_id"].forEach(function(paramKey) {
                        if (campaignData[paramKey]) {
                            referrerParams.set(paramKey, campaignData[paramKey]);
                        }
                    });

                    if (referrerParams.toString()) {
                        playUrl.searchParams.set("referrer", referrerParams.toString());
                    }

                    actionBtn.href = playUrl.toString();
                    actionBtn.innerText = "在 Google Play 取得";
                } catch (e) {
                    actionBtn.href = "https://play.google.com/store/apps/details?id=com.example.app";
                }
            } else {
                // 非行動桌面備用方案:為桌面訪客更新卡片 UI 標題
                actionBtn.href = "https://app.example.com/desktop-showcase";
                actionBtn.innerText = "檢視產品總覽";
                if (cardTitle) cardTitle.innerText = "探索桌面體驗";
                if (cardDesc) cardDesc.innerText = "瀏覽我們的網頁產品展示或透過行動裝置掃描。";
            }
        }

        // 步驟 4:以單一擁有者導航原則初始化 Web JS SDK
        // 上方建構的 actionBtn.href 仍然是主要的可存取目的地。
        // SDK 初始化以擷取與快取參數,而不覆寫錨點導航。
        if (typeof OpoInstall !== "function") {
            console.warn("OpoInstall Web SDK 不可用;繼續使用直接商店導航錨點。");
        } else {
            try {
                new OpoInstall({
                    appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
                    data: campaignData,
                    onready: function() {
                        // SDK 回報就緒。
                        // 參數持久性與後續安裝擷取取決於已部署的 SDK 合約與平台匹配支援。
                        // 導航仍由 actionBtn.href 單獨擁有。
                    }
                });
            } catch (error) {
                console.error("OpoInstall SDK 初始化錯誤;原生商店 href 保持有效:", error);
            }
        }
    })();
    </script>
</body>
</html>

程式化 App QR Code 生成:產生高解析度 ISO/IEC 18004 矩陣資產

對於管理數百種印刷變體的企業行銷活動,條碼必須以程式化方式生成,而不是從設計軟體中手動匯出。

[CODE_BLOCK_02] 中的 Node.js 實作示範了通用 App QR Code 系統的兩個元件:可執行伺服器端 HTTP 302 重新導向處理常式,其透過嚴格的 ROUTING_MODE 驗證(direct-store 對決 web-transit)即時評估送入的請求標頭;以及自動化公用程式,其使用符合 ISO/IEC 18004 規格、具備 Q 級錯誤修正的標準 QR code 產生器來驗證過渡網址並渲染可供印刷的 PNG 條碼影像檔案。

請注意,生成 1024 像素的 PNG 可提供高解析度數位主檔;實際的實體印刷尺寸必須根據實體掃描距離計算,並透過印刷樣張進行驗證。

// 伺服器端 HTTP 302 重新導向與 QR Code 生成腳本
// 檔案:server_qr_router.js
// 相依性:npm install qrcode

const http = require('http');
const QRCode = require('qrcode'); // 符合 ISO/IEC 18004 規格的標準 QR 產生器

/**
 * 伺服器組態物件:
 * 在生產環境中,請為連接埠和即時端點位址設定環境變數。
 * routingMode: 'direct-store'(立即 302 至商店)或 'web-transit'(導向 H5 過渡頁面以進行 SDK 快取)
 */
const SERVER_CONFIG = {
    port: process.env.PORT || 3000,
    routingMode: process.env.ROUTING_MODE || "web-transit",
    iosStoreUrl: "https://apps.apple.com/app/id1234567890",
    androidPackageId: "com.example.app",
    webTransitUrl: process.env.WEB_TRANSIT_URL || "https://app.example.com/universal_app_qr_transit.html",
    desktopFallbackUrl: "https://app.example.com/desktop-overview"
};

/**
 * 驗證主機名稱是否符合 RFC 1035 / RFC 1123 DNS 規格,
 * 且不包含連續的點或無效的標籤格式。
 */
function isValidHostname(host) {
    if (typeof host !== "string" || host.length > 253) {
        return false;
    }
    const labels = host.split(".");
    if (labels.length < 2) {
        return false;
    }
    return labels.every(label =>
        label.length >= 1 &&
        label.length <= 63 &&
        /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label)
    );
}

/**
 * 嚴格從環境組態中擷取核准的主機允許清單。
 */
function getApprovedHosts(envVarName) {
    const raw = process.env[envVarName];
    if (!raw) {
        throw new Error(`${envVarName} 環境變數必須明確設定。`);
    }

    const hosts = new Set(
        raw.split(",")
            .map(host => host.trim().toLowerCase())
            .filter(Boolean)
    );

    if (hosts.size === 0) {
        throw new Error(`${envVarName} 允許清單不得為空。`);
    }

    return hosts;
}

/**
 * 在伺服器啟動前驗證組態結構描述與嚴格的 routingMode 列舉值
 */
function validateConfig() {
    // 對 ROUTING_MODE 強制執行嚴格的列舉驗證
    const allowedModes = new Set(["direct-store", "web-transit"]);
    if (!allowedModes.has(SERVER_CONFIG.routingMode)) {
        throw new Error(`無效的 ROUTING_MODE:「${SERVER_CONFIG.routingMode}」。必須嚴格為「direct-store」或「web-transit」。`);
    }

    const portNum = Number(SERVER_CONFIG.port);
    if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
        throw new Error(`無效的 PORT 組態:「${SERVER_CONFIG.port}」。必須是有效的 TCP 連接埠號碼。`);
    }

    // 驗證路由器主機允許清單環境變數是否已明確設定
    getApprovedHosts("APPROVED_API_HOSTS");

    if (SERVER_CONFIG.routingMode === "web-transit") {
        const transitUrl = new URL(SERVER_CONFIG.webTransitUrl);
        if (transitUrl.protocol !== "https:") {
            throw new Error("WEB_TRANSIT_URL 組態必須使用 HTTPS 通訊協定。");
        }
    }
}

/**
 * 針對明確的允許清單清理送入的網址參數
 */
function sanitizeQueryParams(searchParams) {
    const allowedKeys = ["channelCode", "utm_source", "utm_medium", "utm_campaign", "inviter_id"];
    const validPattern = /^[A-Za-z0-9_.-]{1,64}$/;
    const sanitized = new URLSearchParams();

    allowedKeys.forEach(key => {
        const val = searchParams.get(key);
        if (val && validPattern.test(val)) {
            sanitized.set(key, val);
        }
    });

    return sanitized;
}

/**
 * 評估送入的請求標頭,並根據導向模式傳回目的地網址
 */
function resolveStoreDestination(userAgentString, searchParams) {
    const ua = userAgentString || "";
    const sanitizedParams = sanitizeQueryParams(searchParams);

    // 主動模式在伺服器啟動期間會經過嚴格驗證
    const activeMode = SERVER_CONFIG.routingMode;

    // 模式:web-transit(將行動與模棱兩可的桌面級 UA 導向 H5 過渡頁面)
    if (activeMode === "web-transit") {
        const isMobileOrTablet = /iPhone|iPad|iPod|Android/.test(ua) || /Macintosh/.test(ua);
        if (isMobileOrTablet) {
            const transitUrl = new URL(SERVER_CONFIG.webTransitUrl);
            sanitizedParams.forEach((value, key) => {
                transitUrl.searchParams.set(key, value);
            });
            return transitUrl.toString();
        }
        return SERVER_CONFIG.desktopFallbackUrl;
    }

    // 模式:direct-store(立即 HTTP 302 至應用程式商店列表)
    const isIOS = /iPhone|iPad|iPod/.test(ua);
    const isAndroid = /Android/.test(ua);

    if (isIOS) {
        return SERVER_CONFIG.iosStoreUrl;
    } else if (isAndroid) {
        const playUrl = new URL("https://play.google.com/store/apps/details");
        playUrl.searchParams.set("id", SERVER_CONFIG.androidPackageId);

        // 將清理後的參數編碼至 Google Play 的官方 referrer 參數中
        if (sanitizedParams.toString()) {
            playUrl.searchParams.set("referrer", sanitizedParams.toString());
        }

        return playUrl.toString();
    }

    return SERVER_CONFIG.desktopFallbackUrl;
}

/**
 * 輕量級 HTTP 302 重新導向伺服器
 * 備註:在生產環境中,請在終端機 HTTPS 的驗證反向代理(例如 Nginx、Cloudflare)後方執行。
 * 從受信賴的上游反向代理檢查直接主機標頭。
 */
function startRoutingServer() {
    // 在開啟網路通訊端前強制執行嚴格的組態驗證
    validateConfig();

    const server = http.createServer((req, res) => {
        try {
            // 將重新導向端點的 HTTP 方法限制為 GET 與 HEAD
            if (req.method !== "GET" && req.method !== "HEAD") {
                res.writeHead(405, { "Content-Type": "text/plain", "Allow": "GET, HEAD" });
                res.end("Method Not Allowed");
                return;
            }

            // 從受信賴的反向代理讀取直接主機標頭
            // 備註:.split(":")[0] 會去除選用的連接埠號碼以進行標準主機允許清單評估
            const rawHost = req.headers.host || "";
            const hostHeader = rawHost.split(":")[0];
            const approvedRouterHosts = getApprovedHosts("APPROVED_API_HOSTS");

            if (!isValidHostname(hostHeader) || !approvedRouterHosts.has(hostHeader.toLowerCase())) {
                res.writeHead(403, { "Content-Type": "text/plain" });
                res.end("Forbidden: Access via unauthorized host header.");
                return;
            }

            const requestUrl = new URL(req.url, `https://${hostHeader}`);

            // 將重新導向處理限制為授權的路由端點
            if (requestUrl.pathname !== "/app") {
                res.writeHead(404, { "Content-Type": "text/plain" });
                res.end("Not Found");
                return;
            }

            const destination = resolveStoreDestination(req.headers["user-agent"], requestUrl.searchParams);

            // 執行 HTTP 302 Found 轉址
            res.writeHead(302, {
                "Location": destination,
                "Cache-Control": "no-cache, no-store, must-revalidate"
            });
            res.end();
        } catch (err) {
            res.writeHead(500, { "Content-Type": "text/plain" });
            res.end("Internal Routing Error");
        }
    });

    server.listen(SERVER_CONFIG.port, () => {
        console.log(`[路由器啟動] HTTP 302 伺服器正在連接埠 ${SERVER_CONFIG.port} 以 ${SERVER_CONFIG.routingMode} 模式聆聽`);
    });

    return server;
}

/**
 * 生成符合 ISO/IEC 18004 標準的高解析度 PNG 條碼影像,連結至已驗證的通用過渡網址。
 */
async function generateUniversalAppQRCode(transitUrlString, outputFilePath = "universal_app_qr.png") {
    let parsedUrl;
    try {
        parsedUrl = new URL(transitUrlString);
    } catch (err) {
        throw new Error(`提供的網址字串無效:「${transitUrlString}」。必須是有效的 URI。`);
    }

    if (parsedUrl.protocol !== "https:") {
        throw new Error(`偵測到不安全的通訊協定:${parsedUrl.protocol}。App QR Code 目的地必須使用 HTTPS。`);
    }

    if (!isValidHostname(parsedUrl.hostname)) {
        throw new Error(`偵測到格式錯誤的 DNS 主機名稱:「${parsedUrl.hostname}」。`);
    }

    const approvedQrHosts = getApprovedHosts("APPROVED_QR_HOSTS");
    if (!approvedQrHosts.has(parsedUrl.hostname.toLowerCase())) {
        throw new Error(`未授權的主機網域:「${parsedUrl.hostname}」。主機不存在於 APPROVED_QR_HOSTS 中。`);
    }

    console.log(`[驗證通過] 正在為以下網址生成通用 App QR Code:${parsedUrl.toString()}`);

    const qrOptions = {
        errorCorrectionLevel: 'Q', // Q 級(25% 損壞復原)最適合印刷素材
        type: 'image/png',
        margin: 4,                 // 符合標準的 4 模組留白邊距
        width: 1024                // 適合縮放至印刷生產的高解析度數位主檔
    };

    try {
        await QRCode.toFile(outputFilePath, parsedUrl.toString(), qrOptions);
        console.log(`[成功] 高解析度條碼影像已寫入至:${outputFilePath}`);
        return outputFilePath;
    } catch (err) {
        throw new Error(`無法生成 QR Code 影像資產:${err.message}`);
    }
}

// 獨立執行進入點
if (require.main === module) {
    if (process.argv.includes("--serve")) {
        startRoutingServer();
    } else {
        const sampleUrl = process.env.TARGET_TRANSIT_URL || "https://trk.example.com/app?channelCode=poster_01";
        generateUniversalAppQRCode(sampleUrl, "app_store_qr.png")
            .then((file) => console.log(`建置完成:${file}`))
            .catch((err) => {
                console.error(`建置失敗:${err.message}`);
                process.exit(1);
            });
    }
}

module.exports = { startRoutingServer, generateUniversalAppQRCode, resolveStoreDestination };

多商店 App QR Code 策略之比較分析

比較單一直接連結、雙重列印代碼、靜態網頁轉址器與動態 App QR Code

選擇最佳的 QR 部署策略需要平衡使用者摩擦、技術複雜度、跨平台觸及率與印後維護負擔。

下方的比較矩陣概述了常見實作模型的營運權衡:

部署方法 跨平台支援 印後可編輯性 使用者掃描摩擦 維護風險
直接應用程式商店連結 僅限單一平台 否(固定網址) 對排除的作業系統偏高 高(若網址變更,印刷素材將過時)
並排雙重代碼 手動使用者選擇 否(固定網址) 高(掃描錯誤與視覺雜亂) 高(浪費印刷空間與連結失效風險)
通用靜態 HTML 頁面 相依於腳本 部分(僅限網域層級) 中等(需要中介頁面點擊) 中等(手動網站託管開銷)
動態通用 App QR Code 自動化作業系統分派 是(控制台/API 控制) 極低(自動化商店導向) 變動(受限於導向網域與轉址服務可用性)

評估維護開銷、掃描摩擦與跨平台相容性

直接市集連結代表實體媒體的過時作法。將 App Store 連結寫死可能會防止在不支援的平台上安裝,而雙重並排代碼則會降低實體宣傳品的美感並造成使用者掃描錯誤。

部署動態通用 App QR Code 解決了這些限制。受管理的 QR 連結可以引導 iOS 與 Android 訪客前往平台適當的目的地,並將實體放置識別碼與掃描活動關聯。安裝歸因仍然取決於支援的 SDK 或商店機制以及成功的匹配。如果行銷團隊決定將目的地從直接商店列表切換至互動式假期贈品導覽頁面,他們只需在集中管理控制台中更新目的地網址,完全無需重新印刷。

如何對跨平台掃描與轉址失敗進行疑難排解

解決應用程式內社交瀏覽器對商店網址的限制

很大一部分離線使用者會使用社交通訊應用程式(例如微信、Line 或 WhatsApp)內建的相機來掃描 QR code,而不是原生的行動相機應用程式。

正如行動容器研究中所確立的,社交網頁檢視器會強制執行嚴格的沙箱:

  • 通訊協定抑制:應用程式內網頁檢視器經常會攔截並封鎖自訂通訊協定方案(market://、itms-apps://)並抑制直接套件下載。
  • 過渡交接執行:為了防止死路,過渡頁面必須透過 User-Agent 檢查來偵測社交容器,並提供可存取的 HTTPS 備用按鈕或清晰的視覺指示,引導使用者在系統瀏覽器中開啟目的地。

診斷跨 App Store 與 Play Store 交接的參數剝離

如果應用程式在安裝後啟動時沒有帶上其關聯的活動參數(channelCode 傳回空值),工程團隊應稽核以下交接檢查點:

  • Google Play Referrer 格式化:驗證 Android 商店網址是否正確將參數編碼在單一 referrer= 查詢參數內。直接將鬆散的參數附加到 Play Store 網址而不將其嵌入 referrer 酬載中,會阻止 Google Play 記錄後設資料。
  • iOS 延遲快取:確保 Web JS SDK 在發生商店轉址之前成功將參數酬載傳輸至歸因後端。由於 Apple App Store 會剝離外部查詢字串,原生參數恢復完全依賴後端上下文匹配。
  • 回溯視窗限制:如果使用者在設定的歸因視窗之外安裝或啟動應用程式,則可能無法再匹配符合資格的活動上下文。針對已部署的 SDK 與平台組態驗證適用的視窗。

處理 iPad 桌面模式:區分 Mac 桌機與具備觸控功能的平板電腦

如果測試顯示掃描您的 App QR Code 的 iPad 用戶被重新導向至桌面導覽頁面而不是 App Store,請稽核您的 User-Agent 評估邏輯。

驗證您的導向腳本是否未完全依賴 iPad 字串。確保用戶端邏輯在回報 MacIntel 的平台上評估 navigator.maxTouchPoints > 1,以精確檢測處於桌面級瀏覽模式的 iPadOS 硬體。

實體印刷檢查點:經驗距離尺寸比與留白邊距驗證

許多掃描失敗源自實體圖形生產,而不是軟體導向邏輯:

  • 經驗距離尺寸比:為了保證可靠的相機對焦,請遵循經驗尺寸試探法:Minimum Matrix Width≈Scanning Distance10\text{Minimum Matrix Width} \approx \frac{\text{Scanning Distance}}{10}。設計為從 2 公尺外掃描的海報需要至少 20 公分的 QR 矩陣。在授權印刷生產前,務必在真實世界的燈光條件下進行實體測試掃描。
  • 保留留白邊距:條碼必須在所有四個外側邊緣維持至少 4 個模組寬度的未印刷邊框(「留白邊距」)。重疊此邊距的圖形邊框、字體或背景紋理會阻止相機邊緣檢測演算法識別矩陣邊界。
  • 錯誤修正調整:承受戶外氣候、環境磨損或彎曲包裝表面的印刷素材,應根據 ISO/IEC 18004 標準使用 M 級(15% 復原)或 Q 級(25% 復原)錯誤修正來生成。

真實世界多平台驗證矩陣(M2 模組模型)

在清除 App QR Code 部署以進行實體印刷生產之前,工程團隊必須跨目標測試環境執行驗證,並考量所選的導向模式:

測試裝置 / 容器 導向模式:direct-store 導向模式:web-transit 關鍵檢查檢查點
iPhone Safari 立即 302 至 App Store 載入 H5 過渡頁面 ──> 商店錨點 確認數字 app-id 解析
iPad Safari(傳統 UA) 立即 302 至 App Store 載入 H5 過渡頁面 ──> 商店錨點 確認 iPad 記號評估
iPadOS(桌面模式) 302 至桌面備用方案(UA 限制) 評估觸控 ──> 導向至 App Store 確認 MacIntel + maxTouchPoints > 1 分支
macOS Desktop Safari 302 至桌面備用方案 載入 H5 ──> 評估 0 觸控 ──> 桌面 CTA 乾淨呈現桌面展示
Android Chrome 302 至 Play Store(附加 referrer=) 載入 H5 過渡頁面 ──> Play 錨點 驗證 referrer= 編碼與套件 id
桌面工作站 302 至桌面網頁展示 載入桌面版面配置 / 網頁展示 預防未處理的行動通訊協定錯誤
社交 WebView(微信/Line) 相依於容器(可能封鎖 302) 載入帶有指引的 H5 過渡頁面 驗證手動瀏覽器交接提示
實體印刷樣張 相機掃描時立即重新導向 相機掃描時解析過渡網址 驗證留白邊距與 Q 級復原

常見問題 (FAQ)

單一 QR code 可以自動檢測 iOS 與 Android 裝置嗎?
可以。單一 QR code 透過編碼中介重新導向網址而不是直接市集連結,來實現跨平台導向。當裝置掃描代碼時,導向伺服器或過渡腳本會檢查送入的 User-Agent 字串。如果它偵測到 iOS 裝置,就會將使用者引導至 Apple App Store;如果偵測到 Android 裝置,則會將他們引導至 Google Play Store。
當桌面使用者掃描或點擊 App QR Code 時會發生什麼事?
當桌面電腦(例如工作站相機掃描、預覽點擊或桌面瀏覽器測試)存取 App QR Code 時,導向引擎會偵測到非行動作業系統(macOS、Windows 或 Linux)。引擎不會在行動應用程式商店通訊協定上失敗,而是會將使用者導向響應式桌面導覽頁面,該頁面會突顯產品功能或提供簡訊下載提示。
如何透過 App QR Code 傳遞行銷活動追蹤參數?
若要傳遞行銷活動參數,請將標準查詢金鑰(例如 `channelCode`、`utm_source` 或 `inviter_id`)附加到動態 QR code 的過渡網址。掃描時,過渡引擎會擷取並清理這些參數,透過 Install Referrer API 將其轉發至 Google Play(針對 Android 裝置),或透過 OpoInstall Web JS SDK 將其快取在歸因伺服器上(針對 iOS 裝置),讓原生應用程式能夠在首次啟動時恢復上下文。

總結與實作決策框架

將行動應用程式下載統合至單一通用的 App QR Code 中,解決了實體印刷媒體根本的多平台限制。將直接市集連結寫死會使宣傳品破碎、增加印刷成本,並保證被排除作業系統上的使用者轉換失敗。

工程與行銷團隊應實作由響應式過渡導向支援的集中式動態 App QR code。透過結合自動化 User-Agent 檢測、可存取的新生代備用錨點以及透過 OpoInstall 進行的參數保留,團隊可確保實體宣傳資產支援平台適當的導向,並在所有行動平台上減少摩擦。

若要建立通用多商店 QR code 並設定自動化行動轉址,請在 OpoInstall 開發者控制台中設定您的應用程式端點,或檢閱 SDK 整合參考。如需用戶端 SDK 下載,請造訪下載 OpoInstall SDK,或在行動歸因參考中探索更廣泛的行動導向機制。

相關素材

Share this article