如何部署 QR Code 追蹤以衡量線下 App 安裝量

opoinstall
2026-10-15
5 min read

如何使用 QR Code 追蹤 App 下載?使用 QR Code 追蹤 App 下載需要產生嵌入參數的動態 QR Code,將用戶導向具備適應能力的行動版到達網頁,透過網頁 SDK 記錄符合條件的行銷活動關聯內容,並引導訪客前往適當的應用程式商店,同時在支援的情況下啟用相容的安裝歸因。

行動應用程式的 QR Code 追蹤是一種衡量方法,將獨特的行銷活動識別碼編碼至動態條碼中,藉此搭起實體行銷接觸點與數位 App 安裝之間的橋樑。透過將掃描裝置導向可辨識參數的轉址端點,此架構能夠識別特定的線下來源(例如戶外看板、零售傳單或活動識別證)並衡量端到端的轉換成效。

術語 定義 相關實體 搜尋意圖角色
QR Code 追蹤 對透過實體 QR Code 發起的 App 下載進行系統化衡量。 通路統計與追蹤 資訊型/技術型
動態 QR Code 編碼了受管理的間接導向網址(URL)的 QR Code,其伺服器端目的地與路由規則可在印刷後進行更新。 HTTP 302 重新導向 技術型
App QR Code 將掃描用戶引導至適當行動應用程式商店的統一條碼。 通用連結(Universal Link)/Intent 技術型/商業型

線下盲點:為什麼靜態 QR Code 無法有效衡量 App 安裝

靜態直連商店的 QR Code 將印刷素材永久綁定至固定的目的地網址。

直連商店 QR Code 的技術限制:不可變更的目的地與有限的歸因彈性

對行動成長團隊而言,實體平面媒體一直以來都是一項歸因挑戰。當行銷團隊部署印刷文宣(例如大眾運輸站牌、零售包裝、直郵廣告或會議橫幅)時,他們需要一座實體通往數位的橋樑。最直覺的機制就是符合 ISO/IEC 18004 標準的快速回應碼(QR Code)。

然而,靜態直連商店的 QR Code 具有一項固有的架構限制:其編碼資料負載在印刷後即無法更改。在靜態 QR Code 中,代表目的網路位址的英數字元序列在產生過程中,會直接以圖案形式固定在黑白矩陣模組中。如果公司編碼了一個未經管理的 Apple App Store 直連連結:

https://apps.apple.com/app/id1234567890

該條碼只能將掃描裝置導向該明確的網址。雖然靜態 QR Code 若指向中間網頁,技術上可以編碼靜態網址查詢參數,但直接編碼商店網址意味著一旦實體材料進入流通,目的地將無法在伺服器端修改,且直接商店連結無法執行條件式作業系統路由。直連商店 QR Code 提供有限的印後路由彈性。資產層級的歸因取決於支援的商店歸因機制,以及在印刷前設定的行銷活動參數或識別碼。

多平台難題:為什麼硬編碼單一 App 商店連結會排除半數受眾

行動裝置受眾基本上分為兩個主流作業系統:Apple iOS 與 Google Android。硬編碼至 Apple App Store 連結(apps.apple.com)的靜態 QR Code,當被 Android 裝置掃描時會立即陷入死胡同,因為 Google Play 服務無法解析原生的 iOS 商店通訊協定。

相對地,編碼 Google Play 商店網址(play.google.com)則會強迫 iOS 用戶進入網頁版的 Google Play 介面,該介面無法在 iPhone 上安裝原生軟體。為了繞過這個問題,傳統的印刷行銷活動通常會並排印出兩個獨立的 QR Code:一個標示「掃描下載 iOS 版」,另一個標示「掃描下載 Android 版」。

這種雙代碼方法帶來了顯著的營運與轉換成本:

  • 視覺雜亂與掃描率降低: 印出互相競爭的條碼會消耗寶貴的實體表面積,並引入認知阻力,迫使用戶在掃描前必須先檢查標籤。
  • 用戶操作錯誤與放棄: 行動用戶經常會掃描到錯誤的條碼,遇到不相容的商店頁面或平台錯誤,進而完全放棄引導流程。
  • 有限的資產層級歸因: 直連商店的 QR Code 可能透過符合條件的商店歸因參數支援部分行銷活動層級的衡量,但本身無法提供集中管理的掃描記錄或印後路由彈性。

印後漏洞:當行銷活動目的地在印刷後變更時會發生什麼事

實體文宣的生產需要固定的前置時間。雜誌、產品外盒、銷售點展示架以及戶外(OOH)看板通常在行銷活動推出前數週或數月就已經印好。

如果工程團隊部署了靜態直連商店 QR Code,隨後對數位基礎設施進行的任何變更都會使實體材料失效:

  • App 商店審核延遲: 如果宣傳素材印上了預期的 App Store 網址,但 App 審核要求變更套件識別碼(Bundle ID)或主要 App 識別碼,所有已印刷的文宣將永久過期失效。
  • 行銷活動重新導向需求: 一旦宣傳活動結束,直接指向商店刊登資訊的靜態 QR Code 將無法重新導向至新的季節性到達網頁,導致持續提供過期的優惠或 404 錯誤頁面。
  • 網域棄用與失效端點: 如果未經管理的短連結服務關閉或變更其網址格式,已印刷的資產將無法更新,從而浪費實體廣告預算。

動態 QR Code 路由如何將印刷行銷活動與 App 商店串聯起來

動態 QR Code 能保持印刷網址穩定,同時伺服器端的路由目的地仍可隨時編輯。

解耦架構:作為間接導向層的短網址

動態 QR Code 在實體條碼與數位目的地之間引入了經解耦的間接導向層,藉此解決實體平面媒體的限制。

動態 QR Code 不會在矩陣模組中硬編碼終端 App 商店網址,而是編碼一個輕量且集中化的路由端點(例如範例說明文件端點 https://trk.example.com/c/poster_station_a,切勿直接用於正式環境的印刷資產中)。

QR Code 本身不執行軟體邏輯;相反地,動態功能完全由代管間接導向網址的網路伺服器提供。由於動態 QR Code 中的字元字串可以保持簡短且統一,與編碼帶有多個參數的複雜目的地網址相比,實體條碼通常需要較少的資料模組。這會產生較低的模組密度,讓程式碼能夠以較小的實體尺寸印刷,同時在 ISO/IEC 18004 標準下維持卓越的掃描容忍度與高容錯率。

當智慧型手機相機掃描動態條碼時,裝置會向路由端點發起 HTTP GET 請求。受管理的追蹤端點可能會使用 HTTP 302 重新導向來提供具適應能力的轉送頁面。隨後,範例轉送頁面會檢查客戶端環境指標,並提供由用戶主動觸發的商店導覽連結。

檢查客戶端 User-Agent 標頭以偵測 iOS、Android 與電腦裝置

當掃描裝置查詢動態路由端點時,其瀏覽器網路堆疊會傳輸標準的 HTTP 請求標頭,特別是 User-Agent 字串。

路由引擎會即時使用啟發式模式比對來解析此字串,以隔離裝置作業系統:

  • iOS 偵測: 匹配如 iPhone、iPad 或 iPod 等模式,表示必須將用戶導向 Apple App Store 或相關的通用連結(Universal Link)到達網頁。
  • Android 偵測: 匹配包含 Android 的模式,表示請求必須導向 Google Play 商店或 APK 發布到達網頁。
  • 電腦/工作站偵測: 匹配電腦平台符號,將訪客引導至高解析度的電腦版網頁總覽或次要宣傳內容。

動態分派用戶至 Apple App Store、Google Play 商店或網頁到達網頁

透過在掃描當下評估客戶端裝置的功能,動態路由引擎會執行條件分支邏輯:

  • 行動版 iOS 流量: 重新導向至應用程式獨有的 App Store 產品頁面,或是代管通用連結(Universal Links)的相關到達網頁。
  • 行動版 Android 流量: 直接重新導向至帶有正確編碼安裝推薦連結(Install Referrer)中繼資料的 Google Play 商店客戶端,或提供針對直接下載 APK 所最佳化的行動網頁。
  • 電腦流量: 提供針對電腦最佳化的網頁到達網頁,展示完整功能或提示訪客輸入手機號碼以接收直接的簡訊下載連結。

部署單一統一的 App QR Code 以消除零散的素材成本

部署動態間接導向能夠建立單一且通用的「App QR Code」。行銷與設計團隊不再需要為互相競爭的作業系統徽章分配版面空間。

單一且帶有品牌的 QR Code(標示「掃描下載」)即可同時服務兩個主要的行動生態系。路由後端會根據作業系統功能透明地隔離流量,簡化實體素材設計、將印刷錯誤減到最少,並確保掃描互動能順利到達相容的軟體閘道。

什麼樣的技術架構能實現帶參數的「掃描即安裝」歸因

將通路識別碼與行銷活動中繼資料編碼至查詢字串中

線下 QR 歸因將每個實體擺放位置對應至穩定的行銷活動身份與可衡量的數位事件。

為了實現精細的歸因,動態 QR Code 必須在其重新導向路徑中編碼結構化的追蹤參數。成長行銷不僅需要知道是否發生了安裝,還需要精準掌握是哪一個實體資產帶來了安裝。

歸因平台透過為每個實體部署產生獨特的子通路來實現這一點。每個通路都會被指派一個明確的參數字串:

?channelCode=subway_billboard_04&utm_source=offline&utm_medium=print_ooh&utm_campaign=spring2026

當批量建立動態 QR Code 時,平台會將這些查詢參數與對應到每個實體位置的獨特短網址進行關聯。短網址在歸因資料庫中作為索引鍵,將實體座標直接連結至數位歸因記錄。

跨商店轉換維持內容:結合網頁 JS 擷取與原生 SDK 檢索

當線下用戶掃描動態 QR Code 並重新導向至 App 商店時,標準的網頁查詢字串無法自動穿透市集邊界進入應用程式執行階段。

為了跨越這個邊界,動態轉送架構部署了多階段交接通訊協定:

  1. 階段 1(網頁 JS 載入): 動態 QR Code 將用戶導向代管網頁 JS SDK 的輕量行動版轉送頁面。SDK 會擷取查詢字串參數(channelCode、utm_campaign),根據其部署設定回報這些參數,並準備交接內容。
  2. 階段 2(商店交接): 轉送頁面將用戶重新導向至目的地的 App 商店。對於造訪 Google Play 商店的 Android 用戶,網頁可以將經過清理的參數嵌入官方的 Google Play 安裝推薦連結(Install Referrer)網址中。
  3. 階段 3(原生 SDK 參數檢索): 一旦用戶下載並首次啟動 App,原生行動 SDK 就會初始化並呼叫 getInstallParam()。符合條件的行銷活動參數可能會在安裝後透過支援的平台或 SDK 機制取回。成功的歸因取決於安裝路徑、SDK 設定、可用訊號以及平台的隱私權要求。

分析線下到線上的歸因管線

下方的架構流程圖說明瞭從實體掃描到原生參數還原的資料管線:

[實體海報/展示] ──> 用戶使用相機掃描動態 QR Code
             │
             ▼
[動態轉送端點] ──> 檢查 User-Agent 並擷取 ?channelCode=poster_01
             │
             ├──> [iOS 用戶] ──> 傳遞內容 ──> Apple App Store 商店頁面
             │                                             │
             └──> [Android 用戶] ──> 傳遞推薦連結 ──> Google Play Store 商店頁面
                                                           │
                                                           ▼
[首次 App 啟動] ──> 原生 SDK 嘗試檢索通路 ──> 已匹配通路/其他來源/未知 ──> 分析儀表板

衡量掃描到安裝的轉換漏斗(M4 模型模組)

QR 歸因將可觀察的轉送與首次啟動事件,與不可觀察的商店及安裝流失區隔開來。

線下行銷活動的衡量需要在不同的管線里程碑中定義清晰的數學轉換比例。行銷團隊必須將可觀察的事件與平台受限的轉移區隔開來,評估在相同通路、觀察視窗以及去重複化規則下標準化的指標:

  • 記錄的轉送造訪(SS): 動態轉送伺服器在 QR Code 掃描時記錄的有效、去重複化 HTTP 請求總數,並在定義的轉送造訪觀察視窗內進行標準化。
  • 去重複化的商店交接嘗試(HH): 發起至少一次商店交接動作的有效轉送造訪次數,並在與 SS 相同的觀察視窗與去重複化標準下進行評估。請注意,交接指標(HH)必須透過已確認的重新導向事件或獨立設定的商店連結啟用來收集;單獨建構 href 並不會記錄交接嘗試。
  • 商店交接率(RhandoffR_{\text{handoff}}): 定義為 Rhandoff=HSR_{\text{handoff}} = \frac{H}{S}。较低的比率可能反映了網路故障、客戶端限制、用戶放棄操作或事件收集上的差異。
  • 已歸因首次啟動次數(AcA_c): 在隨後的歸因成熟視窗內,成功匹配至通路 cc 的去重複化首次應用程式啟動次數。
  • 已歸因首次啟動率(Rattributed-first-launchR_{\text{attributed-first-launch}}): 定義為 Rattributed-first-launch=AcS×100%R_{\text{attributed-first-launch}} = \frac{A_c}{S} \times 100\%。此指標用來評估每個實體資產隨時間推移所觀察到的已歸因首次啟動與造訪比例。
  • 不可觀察的管線流失考量: 商店頁面放棄、安裝未完成、首次啟動延遲以及無法匹配的歸因事件,無法單憑重新導向記錄來進行個別量化。除非有支援的商店或 SDK 遙測資料可供使用,否則應將這些階段回報為未觀察狀態。

隱私權界線:平台隱私權規則與供應商資料審查

現代行動歸因架構必須嚴格遵守作業系統的隱私權框架。Apple 的 App 追蹤透明度(ATT)政策與平台安全指南明確禁止裝置指紋識別——定義為收集 IP 位址、User-Agent 字串、螢幕解析度與系統設定的組合,以建立持久性或機率性的裝置特徵。

動態 QR 路由會記錄行銷活動層級的造訪,並可能透過符合條件的平台機制(例如 Google Play Install Referrer API)來支援安裝歸因。請優先使用範圍受限的行銷活動識別碼以及支援的平台歸因通路。切勿假設所選用的第三方 SDK 能夠完全避免裝置指紋識別;在部署前應驗證其實際的匹配方法與資料處理實務。僅靠網址參數無法建立符合隱私規範的 iOS 安裝匹配,開發人員必須獨立稽核所部署 SDK 的匹配機制是否符合 Apple 對禁止裝置指紋識別的規定:

  • 範圍受限的行銷活動識別碼: 使用具決定性的網址查詢參數、官方 Google Play Install Referrer API 以及短暫的伺服器端工作階段權杖來歸因行銷活動來源,而非跨不相關的 App 追蹤與硬體相關的識別碼。
  • 資料最小化: 掃描的參數代表行銷活動中繼資料(例如看板 ID、位置權杖)。然而,開發人員必須認知到網路傳輸本質上會處理 IP 位址與請求標頭,且自訂權杖(如 inviter_id)在隱私權規範下可能代表可識別的用戶帳戶。
  • 客戶端授權隔離: 客戶端還原的參數僅作為歸因提示。下游後端服務會使用經過驗證的用戶帳戶來驗證推薦邏輯與行銷活動資格,避免未經證實的客戶端權限授予。

逐步實作:產生與追蹤動態 App QR Code

在開發人員主控台中設定通路識別碼與到達網頁範本

在產生實體條碼之前,成長團隊必須在歸因儀表板中建立其通路階層:

  1. 註冊應用程式資產: 存取歸因管理主控台,設定您的應用程式識別碼(AppKey、Android 套件名稱、iOS Bundle ID),並上傳測試安裝套件。
  2. 定義主要通路: 在通路統計管理下方,建立主要通路群組(例如 Offline_OOH、Retail_Packaging、Event_Conference)。
  3. 實例化帶參數的子通路: 產生對應至每個實體位置的獨特子通路。系統會為每個擺放位置指派一個相異的 channelCode(例如 ooh_chicago_01、pkg_retail_box_v2)。
  4. 設定轉送到達網頁: 選擇預設的行動版到達網頁範本,或將網頁 JS SDK 整合至您自訂的 H5 頁面中。到達網頁作為掃描事件與 App 商店之間的轉送路由器。

實作網頁端的動態重新導向與參數保留

當代管自訂網頁到達網頁以服務動態 QR Code 掃描時,開發人員必須清理傳入的參數、建構合法的商店目的地網址,並呼叫深層連結(Deep Linking)或商店路由。

[CODE_BLOCK_01] 中的實作範例展示了一個 HTML/JS 轉送頁面,該頁面針對允許清單驗證查詢字串、建構帶有正確編碼 referrer 參數的有效 Google Play 網址,並初始化網頁 JS SDK,同時將原生商店錨點保留為單一導航擁有者。

請參閱 SDK 整合參考資料以取得關於設定參數與參數解析的技術規格。

<!-- HTML / JS 動態 QR Code 重新導向與參數處理 -->
<!-- 檔案:qr_transit_page.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, sans-serif;
            background-color: #fafafa;
            color: #333;
            text-align: center;
            padding: 40px 16px;
            margin: 0;
        }
        .transit-card {
            background: #ffffff;
            border-radius: 14px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.06);
            max-width: 420px;
            margin: 0 auto;
            padding: 32px 24px;
            box-sizing: border-box;
        }
        .app-badge {
            width: 72px;
            height: 72px;
            border-radius: 16px;
            margin-bottom: 16px;
        }
        .btn-action {
            display: inline-block;
            background-color: #007aff;
            color: #ffffff;
            font-size: 15px;
            font-weight: 600;
            padding: 12px 28px;
            border-radius: 22px;
            text-decoration: none;
            margin-top: 20px;
            border: none;
            cursor: pointer;
        }
        .status-msg {
            font-size: 13px;
            color: #777;
            margin-top: 14px;
        }
    </style>
</head>
<body>

    <div class="transit-card">
        <img src="https://cdn.example.com/assets/app-icon.png" alt="App 圖示" class="app-badge">
        <h2 id="card-title">繼續前往您的應用程式商店</h2>
        <p id="card-desc">請點擊下方按鈕以開啟對應的應用程式商店。</p>
        
        <!-- 可存取的主動備用錨點連結 -->
        <!-- 正式環境備註:請將 https://app.example.com/default-store 替換為您已驗證的正式環境到達網址 -->
        <a id="fallback-store-btn" class="btn-action" href="https://app.example.com/default-store">繼續下載</a>
        <p class="status-msg">點擊上方按鈕以繼續前往您的應用程式商店。</p>
    </div>

    <!-- OpoInstall 網頁 JS SDK 引入(請為您的部署正式版本進行驗證) -->
    <script src="https://web.cdn.opoinstallcloud.com/opoinstall.js"></script>
    <script>
    (function() {
        // 步驟 1:解析並清理來自實體 QR 掃描的傳入追蹤參數
        function getSanitizedTrackingParams() {
            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 = getSanitizedTrackingParams();

        // 步驟 2:客戶端 User-Agent 檢查以進行平台路由
        var ua = navigator.userAgent || "";
        var isIOS = /iPhone|iPad|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
        var isAndroid = /Android/.test(ua);

        var fallbackBtn = document.getElementById("fallback-store-btn");

        // 步驟 3:使用清理後的參數建構平台特定的商店網址
        if (fallbackBtn) {
            if (isIOS) {
                // Apple App Store 直接刊登網址(註:iOS App Store 不解析任意查詢字串)
                fallbackBtn.href = "https://apps.apple.com/app/id1234567890";
            } else if (isAndroid) {
                // 建構帶有正確編碼 'referrer' 參數的 Google Play 商店網址
                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());
                    }

                    fallbackBtn.href = playUrl.toString();
                } catch (e) {
                    fallbackBtn.href = "https://play.google.com/store/apps/details?id=com.example.app";
                }
            }
        }

        // 步驟 4:透過單一導航擁有者原則初始化網頁 JS SDK
        // 上方建構的 fallbackBtn.href 仍然是主要的可用目的地。
        // SDK 初始化以擷取並回報參數,而不會覆寫錨點導航。
        if (typeof OpoInstall !== "function") {
            console.error("OpoInstall 網頁 SDK 無法使用;尚未確認歸因擷取。");
        } else {
            try {
                new OpoInstall({
                    appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
                    data: campaignData,
                    onready: function() {
                        // SDK 回報就緒。
                        // 參數持久化與隨後的安裝檢索取決於部署的 SDK 合約與平台匹配支援。
                        // 導航仍由 fallbackBtn.href 錨點單獨擁有。
                    }
                });
            } catch (error) {
                console.error("OpoInstall SDK 初始化錯誤;原生商店 href 保持啟用狀態:", error);
            }
        }
    })();
    </script>
</body>
</html>

透過伺服器端 REST API 自動產生動態 QR Code

對於部署數千個實體 SKU 或在地化宣傳傳單的企業行銷活動而言,透過網頁主控台手動產生條碼是缺乏效率的。

下列示範 Node.js 工作流程展示了通路註冊、追蹤網址驗證以及 QR 影像產生。在執行前,請根據部署的 OpoInstall API 合約來設定 API 端點、驗證機制、請求欄位與回應對應。

[CODE_BLOCK_02] 中的指令碼展示了後端伺服器如何執行已授權的請求工作流程、針對已核准的 API 與追蹤網域允許清單驗證傳回的網址結構,並將經過驗證的 HTTPS 端點傳遞至符合 ISO/IEC 18004 規格的標準 QR Code 產生器,以產生高解析度的 PNG 條碼影像檔案。

// 用於程式化通路建立與動態 QR Code產生的 Node.js 指令碼
// 檔案:generate_channel_qr.js
// 相依性:npm install qrcode

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

/**
 * 設定物件:
 * 示範自動化工作流程,展示通路註冊、追蹤網址驗證以及 QR 影像產生。
 * 在執行前,請根據部署的 OpoInstall API 合約來設定 API 端點、驗證機制、請求欄位與回應對應。
 */
const CONFIG = {
    apiKey: process.env.OPOINSTALL_API_KEY,
    apiHost: process.env.OPOINSTALL_API_HOST,
    channelAddEndpoint: process.env.OPOINSTALL_CHANNEL_ADD_PATH,
    channelGetEndpoint: process.env.OPOINSTALL_CHANNEL_GET_PATH
};

/**
 * 驗證主機名稱是否符合 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;
}

/**
 * 驗證設定存在性、語法以及已授權的 API 主機成員資格
 */
function validateConfig() {
    if (!CONFIG.apiKey || CONFIG.apiKey === "OPOINSTALL_API_KEY_PLACEHOLDER") {
        throw new Error("缺少必要的 OPOINSTALL_API_KEY 環境變數。中止執行。");
    }

    // 強制對 apiHost 進行嚴格的 DNS 標籤驗證
    if (!CONFIG.apiHost || !isValidHostname(CONFIG.apiHost)) {
        throw new Error(`無效或格式錯誤的 OPOINSTALL_API_HOST:"${CONFIG.apiHost}"。必須是有效的 DNS 主機名稱。`);
    }

    // 針對專用已核准的 API 主機允許清單驗證 apiHost
    const approvedApiHosts = getApprovedHosts("APPROVED_API_HOSTS");
    if (!approvedApiHosts.has(CONFIG.apiHost.toLowerCase())) {
        throw new Error(`未授權的 API 主機:"${CONFIG.apiHost}"。主機不存在於 APPROVED_API_HOSTS 中。`);
    }

    // 強制端點路徑結構以斜線開頭
    const pathPattern = /^\/[a-zA-Z0-9/_-]+$/;
    if (!CONFIG.channelAddEndpoint || !pathPattern.test(CONFIG.channelAddEndpoint)) {
        throw new Error(`無效或缺少的 OPOINSTALL_CHANNEL_ADD_PATH:"${CONFIG.channelAddEndpoint}"。必須以 "/" 開頭。`);
    }

    if (!CONFIG.channelGetEndpoint || !pathPattern.test(CONFIG.channelGetEndpoint)) {
        throw new Error(`無效或缺少的 OPOINSTALL_CHANNEL_GET_PATH:"${CONFIG.channelGetEndpoint}"。必須以 "/" 開頭。`);
    }
}

/**
 * 執行 HTTPS GET 請求並傳回解析後的 JSON(具備逾時與大小防護)的輔助函式
 */
function httpsGetJson(url) {
    return new Promise((resolve, reject) => {
        const req = https.get(url, (res) => {
            if (res.statusCode < 200 || res.statusCode >= 300) {
                reject(new Error(`HTTP 請求失敗,狀態碼為:${res.statusCode}`));
                res.resume();
                return;
            }

            let rawData = '';
            let bytes = 0;
            const maxBytes = 1024 * 1024; // 1 MB 負載保護限制

            res.on('data', (chunk) => {
                bytes += chunk.length;
                if (bytes > maxBytes) {
                    req.destroy();
                    reject(new Error("API 回應負載超過 1 MB 限制。"));
                    return;
                }
                rawData += chunk;
            });

            res.on('end', () => {
                try {
                    const parsed = JSON.parse(rawData);
                    resolve(parsed);
                } catch (err) {
                    reject(new Error(`解析 JSON 回應失敗:${err.message}`));
                }
            });

            res.on('error', (err) => {
                reject(new Error(`回應串流錯誤:${err.message}`));
            });
        });

        req.on('error', (err) => {
            reject(new Error(`網路請求錯誤:${err.message}`));
        });

        req.setTimeout(8000, () => {
            req.destroy();
            reject(new Error("網路請求在 8000ms 後逾時"));
        });
    });
}

/**
 * 建立線下追蹤通路並轉譯符合 ISO/IEC 18004 標準的 QR Code 影像。
 * 
 * @param {string} channelCode - 獨特的實體資產識別碼(例如 'poster_station_01')
 * @param {string} channelName - 歸因儀表板報告的描述性標籤
 * @param {string} groupName   - 通路群組類別(例如 'Offline_OOH')
 * @param {string} outputFilePath - 產生的 QR PNG 的目標檔案路徑
 */
async function generateOfflineTrackingQRCode(channelCode, channelName, groupName = "Offline_OOH", outputFilePath = "channel_qr.png") {
    // 在發起網路請求前,先驗證設定存在性、DNS 語法以及已授權的 API 主機
    validateConfig();

    // 步驟 1:透過設定的 channelAddEndpoint 註冊或更新通路中繼資料
    const addParams = new URLSearchParams({
        apiKey: CONFIG.apiKey,
        channelCode: channelCode,
        channelName: channelName,
        groupName: groupName
    });
    const addUrl = `https://${CONFIG.apiHost}${CONFIG.channelAddEndpoint}?${addParams.toString()}`;

    console.log(`[OpoInstall API] 正在註冊通路:${channelCode} (${channelName})...`);
    const addResponse = await httpsGetJson(addUrl);

    if (addResponse.code !== 0) {
        throw new Error(`API 拒絕建立通路。代碼:${addResponse.code},錯誤:${addResponse.error}`);
    }
    console.log(`[OpoInstall API] 通路註冊成功。代碼:${channelCode}`);

    // 步驟 2:如果建立主體中未包含,則透過 channelGetEndpoint 檢索已驗證的通路追蹤連結
    let verifiedRawUrl = addResponse.body?.linkUrl || addResponse.body?.channelUrl;

    if (!verifiedRawUrl) {
        const getParams = new URLSearchParams({
            apiKey: CONFIG.apiKey,
            channelCode: channelCode
        });
        const getUrl = `https://${CONFIG.apiHost}${CONFIG.channelGetEndpoint}?${getParams.toString()}`;
        
        console.log(`[OpoInstall API] 正在查詢以下通路的詳細資料:${channelCode}...`);
        const getResponse = await httpsGetJson(getUrl);

        if (getResponse.code !== 0 || !getResponse.body) {
            throw new Error(`API 無法檢索通路詳細資料。代碼:${getResponse.code},錯誤:${getResponse.error}`);
        }

        verifiedRawUrl = getResponse.body.linkUrl || getResponse.body.channelUrl;
    }

    if (!verifiedRawUrl) {
        throw new Error(`伺服器未傳回通路的追蹤連結網址:${channelCode}`);
    }

    // 步驟 3:針對已核准的 QR 追蹤主機允許清單驗證通訊協定與受信任的網域權威
    const trackingUrl = new URL(verifiedRawUrl);
    if (trackingUrl.protocol !== "https:") {
        throw new Error(`偵測到不安全的通訊協定:${trackingUrl.protocol}。追蹤網址必須是 HTTPS。`);
    }

    const approvedQrHosts = getApprovedHosts("APPROVED_QR_HOSTS");
    if (!approvedQrHosts.has(trackingUrl.hostname.toLowerCase())) {
        throw new Error(`不受信任的主機網域:${trackingUrl.hostname}。主機不存在於 APPROVED_QR_HOSTS 中。`);
    }

    console.log(`[成功] 已驗證的追蹤網址:${trackingUrl.toString()}`);

    // 步驟 4:轉譯符合 ISO/IEC 18004 標準的 QR Code 矩陣
    const qrOptions = {
        errorCorrectionLevel: 'Q', // 針對印刷媒體提供高達 25% 的損壞復原能力
        type: 'image/png',
        margin: 4, // 符合規範的 4 模組留白邊距
        width: 1024 // 用於印刷生產的高解析度輸出
    };

    await QRCode.toFile(outputFilePath, trackingUrl.toString(), qrOptions);
    console.log(`[已產生] 條碼影像成功儲存至:${outputFilePath}`);
    return outputFilePath;
}

// 用於產生在地化印刷宣傳素材資產的範例執行
// 用法:node generate_channel_qr.js
if (require.main === module) {
    generateOfflineTrackingQRCode("ooh_broadway_01", "紐約百老匯看板 #1", "Q3_Outdoor", "broadway_billboard_qr.png")
        .then((file) => console.log(`執行完成:${file}`))
        .catch((err) => {
            console.error(`執行失敗:${err.message}`);
            process.exit(1);
        });
}

module.exports = { generateOfflineTrackingQRCode };

線下 App 追蹤技術比較矩陣

評估傳統印刷條碼、靜態商店連結與動態帶參數 QR Code

選擇最佳的 QR 追蹤方法需要評估印後彈性、平台路由能力、歸因深度以及營運風險。

下方的比較矩陣概述了線下追蹤模型之間的根本技術差異:

衡量策略 平台路由能力 歸因精細度 印後目的地可編輯性 營運風險
靜態直連商店 QR Code 僅限單一 OS(iOS 或 Android) 有限(資產層級衡量取決於商店機制與預先設定的參數) 否(硬編碼目的地網址) 高(連結失效需要重新印製實體素材)
雙直連商店 QR Code(並排顯示) 需要用戶手動選擇 有限(需要分開的硬編碼通路網址) 否(硬編碼目的地網址) 高(浪費印刷空間且容易造成用戶掃描錯誤)
靜態管理網頁 QR Code 基於腳本或手動路由 中等(衡量網頁造訪;App 歸因取決於網頁漏斗) 條件式(伺服器端重新導向可編輯) 中等(用戶在中間網頁流失)
動態帶參數 App QR Code 自動化的 User-Agent 分派 精細(每個實體資產皆有獨特的通路指標) 是(透過管理主控台或 API 更新) 可變(取決於路由網域與間接導向服務可用性)

評估維護成本、分析精細度與印後彈性

直連商店的靜態實作會帶來巨大的營運負擔。如果企業印製了 50,000 個印有靜態 App Store 連結的產品包裝,任何未來的網址變更都會使包裝永久失效。此外,除非為每個行銷活動建立獨立的網頁,否則行銷團隊無法辨別產品包裝究竟帶來了 10 個還是 10,000 個安裝。

動態帶參數 QR Code 緩解了這些限制。受管理的 QR 連結可以引導 iOS 與 Android 訪客前往平台適當的目的地,並將實體擺放識別碼與掃描活動進行關聯。安裝歸因仍然取決於支援的 SDK 或商店機制以及成功的匹配。如果行銷團隊決定將目的地從直接商店刊登資訊切換為互動式的假期贈品到達網頁,他們只需在中央管理主控台更新目的地網址即可——完全不需要重新印製。

排除掃描失敗與線下資料差異的故障排除

管理掃描放棄:當用戶掃描但在商店重新導向之前終止

在線下行銷漏斗中,轉送造訪通常會超過已歸因的首次啟動次數(視事件定義而定)。當實體條碼被智慧型手機相機解析並載入轉送網址的瞬間,就會記錄掃描事件。

掃描指標與 App 安裝之間的落差源自於數種真實世界的用戶行為:

  • 網路延遲下降: 如果實體環境的行動網路訊號不佳(例如地鐵站或擁擠的會議中心),用戶可能會在 HTTP 302 重新導向或網頁 JS SDK 載入完成前關閉瀏覽器。
  • 商店檢視放棄: 用戶經常會檢視 App 商店頁面、評估檔案大小或用戶評論,然後決定不發起下載。
  • 延遲首次啟動: 連線至行動網路的用戶可能會點擊「下載」,但會延後開啟應用程式直到連上 Wi-Fi 為止。由於歸因回呼會在應用程式首次啟動期間執行,因此安裝時間會落後掃描時間數小時或數天。

處理原生商店網址在應用程式內社群掃描器中的封鎖

很大一部分的線下用戶會使用社群通訊應用程式(例如微信、LINE 或 WhatsApp)內建的相機來掃描 QR Code,而不是使用原生系統相機 App。

正如行動容器研究所探討的,社群應用程式內建瀏覽器會強制執行嚴格的沙箱機制:

  • 通訊協定攔截: 當社群掃描器載入試圖叫出自訂通訊協定的網址(market:// 或直接自訂網址)時,應用程式內建容器會封鎖該通訊協定。
  • 下載抑制: 社群網頁檢視器通常會限制 Android 上的直接 APK 二進位檔下載。
  • 解析通訊協定: 轉送頁面可以提供支援的交接選項,或提供在外部瀏覽器中開啟目的地的清晰指示。實際行為取決於主應用程式的導航政策;當無法直接進行 App 或商店交接時,請提供可存取的 HTTPS 替代方案。

診斷非管理型 App 下載工作流程中的參數遺失

如果應用程式在首次啟動時無法擷取參數(getInstallParam 傳回空白資料),工程團隊應稽核以下整合檢查點:

  • SDK 初始化範圍: 根據原生 SDK 平台文件,驗證 SDK 初始化是否在主要的應用程式處理程序中正確設定。
  • 覆寫安裝: 根據 SDK 快取與去重複化規則,在先前已經安裝過該應用程式的裝置上進行測試可能會抑制新的參數回呼。請務必完全解除安裝應用程式,或使用全新的實體測試裝置進行測試。
  • 安裝時間視窗逾時: 如果用戶掃描了實體海報,但延遲了下載並開啟 App 的時間,超過了平台的回溯視窗,符合條件的歸因視窗可能會過期,導致安裝維持未匹配或透過其他方式歸因(取決於報告規則)。

實體驗證通訊協定:尺寸、留白邊距與容錯

線下歸因的可行性高度取決於實體條碼的執行品質:

  • 經驗法則實體尺寸: 可掃描的矩陣應遵循經驗距離與尺寸法則:Estimated Minimum Width≈Scanning Distance10\text{Estimated Minimum Width} \approx \frac{\text{Scanning Distance}}{10}。對於從 1.5 公尺外觀看的海報,QR 矩陣的寬度至少應為 15 公分。尺寸必須透過多種光源條件下的實體測試列印來進行驗證。
  • 留白邊距保留: 條碼必須在所有四個外側邊緣周圍維持至少 4 個模組寬度的未列印「留白邊距(Quiet Zone)」。直接緊鄰模組矩陣列印圖形邊框、文字或藝術紋理會破壞相機邊緣偵測演算法。
  • 容錯率調整: 容易受到實體磨損、低光源或部分遮擋(例如戶外橫幅或紙板包裝)影響的印刷文宣,應根據 ISO/IEC 18004:2024 標準使用M 級(15% 復原)或Q 級(25% 復原)容錯率來產生,這樣即使程式碼遭受輕微的表面擦傷也能成功解碼。

常見問題(FAQ)

App 下載的靜態與動態 QR Code 有什麼區別?
技術上的區別在於目的地是直接編碼還是透過中介伺服器進行管理。靜態直連商店 QR Code 會直接將不可變更的 App 商店連結編碼至條碼矩陣中;一旦列印完成,其目的地就無法更改,且在沒有預先設定網頁路由的情況下,無法在不同的作業系統之間動態分支。動態(或受管理的重新導向)QR Code 則編碼指向間接導向伺服器的穩定 HTTPS 短連結。當被掃描時,伺服器會檢查客戶端環境、記錄造訪並將用戶重新導向至適當的目的地,允許在不改變實體條碼的情況下於印後更新底層的目標網址。
單一個 QR Code 可以同時將 iOS 與 Android 用戶引導至各自的商店嗎?
可以。統一的動態 App QR Code 使用客戶端 User-Agent 偵測或伺服器端 HTTP 標頭檢查。當 iPhone 掃描該程式碼時,路由伺服器會偵測到 iOS 並將用戶轉發至 Apple App Store。當 Android 裝置掃描相同的程式碼時,伺服器會偵測到 Android 並透過正確結構化的 Install Referrer 參數重新導向至 Google Play 商店,從而省去了為每個平台列印獨立 QR Code 的需求。
當海報在 App 發布前就已印好時,動態 QR Code 如何防止資料遺失?
由於動態 QR Code 是透過由歸因平台代管具彈性的間接導向網址進行路由,因此底層的目的地網址隨時可以在管理主控台中更新,而無需修改實體條碼。如果宣傳海報必須在應用程式於 App Store 獲准前數週印製,動態 QR Code 可以最初指向預先註冊網頁,並在獲准後切換為即時的 App 商店刊登資訊。

總結與實作決策框架

部署 QR Code 追蹤將線下廣告從無法衡量的成本中心轉變為可量化的用戶取得通路。依賴直連商店的靜態連結會引入阻力、排除平台,並在素材列印後冒著連結完全失效的風險。

工程與成長團隊應標準化使用動態、帶參數的 App QR Code。透過將集中化的重新導向間接導向與客戶端環境偵測及 SDK 參數還原相結合,平台能夠以可衡量的行銷活動活動與支援的安裝歸因來跨越實體到數位的鴻溝。

若要設定您的實體行銷活動通路並產生動態追蹤代碼,請在 OpoInstall 開發人員主控台中設定您的參數,或檢閱 SDK 整合參考資料。如需客戶端 SDK 整合的詳細資料,請造訪下載 OpoInstall SDK,或在行動歸因實作參考資料中探索更廣泛的多通路分析。

相關素材

Share this article