如何設定行動裝置的自動化推薦追蹤? 設定自動化推薦追蹤需要整合歸因 SDK、將不透明的推薦權杖(shareCode)附加至網頁分享連結、在到達網頁(landing page)上進行捕捉,並利用平台支援的延遲深度連結(deferred deep linking)在首次開啟 App 時嘗試復原參數。
自動化推薦追蹤是一種工程架構,能夠以程式化方式將行動應用程式安裝歸因於現有使用者的分享動作。透過在網頁到達網頁中傳遞不透明的邀請者識別碼(shareCode),並嘗試在首次啟動應用程式時解析此上下文,自動化推薦追蹤能減少手動輸入促銷代碼的需求,並支援在應用程式內嵌瀏覽器容器中的歸因。
| 名詞 | 定義 | 相關實體 | 搜尋意圖角色 |
|---|---|---|---|
| 推薦追蹤 | 將下游使用者獲取以程式化方式歸因於特定現有使用者分享工作階段。 | App 參數安裝 | 資訊型 / 商業型 |
| 推薦追蹤軟體 | 旨在自動化邀請者與被邀請者綁定的 SDK 與伺服器基礎架構。 | 延遲深度連結 | 商業型 |
| App 推薦計畫 | 鼓勵現有使用者邀請新同儕安裝應用程式的成長計畫。 | 管道統計與追蹤 | 商業型 |
| 推薦系統 | 管理連結產生、歸因與獎勵發放的端到端技術架構。 | 網頁 JS SDK | 技術型 |
架構概覽:自動化推薦追蹤如何將社群分享與 App 安裝串聯起來
傳統推薦系統的機制拆解:為什麼手動促銷代碼會降低轉換漏斗速率
傳統的行動 App 推薦計畫依賴手動的英數字促銷代碼。在此舊有的工作流程中,現有使用者會產生邀請碼(例如 INVITE2026)、手動複製文字,並透過簡訊或社群訊息發送給同儕。被邀請的使用者接著必須從應用程式商店下載應用程式、完成註冊、找到專屬的促銷代碼輸入欄位,並準確轉錄該字串。
這個手動過程在整個獲取漏斗中引入了實質的摩擦力:
- 認知與操作流失:使用者經常忘記複製優惠代碼、在深層引導選單中找不到兌換輸入介面,或在代碼因大小寫或轉錄錯誤而失效時放棄流程。
- 管道外洩與代碼抓取:英數字代碼很容易被擷取並發布到公開的優惠券彙整網站或論壇版面上。這切斷了真實的同儕關係,允許未被推薦的自然流量使用者領取獎勵,同時稀釋了活動單位經濟效益。
- 衡量盲點:如果沒有部署獨立的分析儀器,僅依賴手動促銷代碼會讓中間漏斗的里程碑(例如連結曝光、社群網頁檢視點擊以及商店分發率)無法被衡量。
程式化分享管線:邀請者動作、網頁暫存、商店交接與第 0 天解析

自動化推薦追蹤以持續的程式化參數傳遞管線取代了手動轉錄。應用程式不會分發未帶參數的商店連結與不相關的文字優惠券,而是將邀請者的身分直接打包到互動式網頁分享 URL 中。
技術生命週期運作於四個獨立階段:
[現有使用者邀請好友] ──> 呼叫 shareTelemetry.reportIntent(shareCode, Platform)
│
▼
[產生動態網頁連結] ───> 將 ?shareCode=usr_9f82a1b4 附加至目標 URL
│
▼
[在微信/Line內部分享] ──> 好友點擊沙盒式網頁檢視中的連結
│
▼
[網頁 JS SDK 暫存與交接] ─> 快取上下文並執行 App 啟動 / 商店下載
│
▼
[裝置上首次啟動 App] ───> 原生 SDK 查詢參數復原
│
▼
[應用程式後端綁定] ───> 後端驗證權杖並建立待處理綁定
│
▼
[已驗證的啟用里程碑] ─> 後端於達成里程碑時發放獎勵
當被邀請者開啟連結時,代管的網頁到達網頁會執行一個網頁 JS SDK,該 SDK 會擷取邀請者的權杖(shareCode)並暫存推薦上下文。當使用者繼續安裝並開啟原生應用程式時,客戶端 SDK 會在首次啟動期間擷取暫存的酬載,讓應用程式後端能夠自動綁定推薦關係,而無需任何參與者手動輸入資料。
商店安裝後首次啟動復原:平台特定限制
在使用者的應用程式商店安裝應用程式之後,推薦參數復原取決於平台以及應用程式或歸因 SDK 所實作的機制。透過網頁 URL 傳遞 shareCode 本身並不能保證該參數會對新安裝的應用程式可用。
- Android / Google Play: Google Play 安裝推薦工具(Install Referrer)可以提供與 Play 商店安裝相關的受支援推薦後設資料。應用程式必須設定並測試實際的推薦來源酬載,並驗證預期的參數是否能在重新導向和安裝流程中存留。第三方歸因 SDK 可能會使用必須另行記錄的額外機制。
- iOS / App Store: App Store 不會自動將任意的到達網頁查詢參數轉移到已安裝的應用程式中。通用連結(Universal Links)在應用程式已安裝且相關網域設定有效時支援直接路由;它們本身並不能保證在新的安裝之後進行延遲參數復原。
- 歸因 SDK: 僅使用針對所部署的確切 SDK 版本和平台記錄的復原機制。驗證實作、資料收集與隱私權隱含意義,而不是假設 SDK 可以跨所有瀏覽器和安裝路徑復原每個推薦參數。
如果無法透過支援的機制復原推薦上下文,請允許使用者完成註冊並將安裝記錄為未歸因。切勿從未經驗證的配對推斷或製造推薦關係。

iOS 歸因與隱私權界線
iOS 推薦實作絕不能依賴被禁止的裝置指紋識別來跨應用程式或網網站識別使用者或裝置。Apple 的隱私權與資料使用要求禁止為此目的從裝置、瀏覽器、設定、位置或網路訊號衍生唯一識別碼。App 追蹤透明度(ATT)授權並不會使原本被禁止的指紋識別變得合法。
當應用程式已安裝時,請使用通用連結進行經過驗證的直接 App 路由。對於安裝後的推薦復原,請使用已針對所部署的 SDK 版本和適用 Apple 要求檢閱過其行為的記錄機制。在適用的情況下檢閱 SDK 的隱私權揭露與必要原因 API 宣告,但切勿將隱私權資訊清單(Privacy Manifest)的存在視為歸因實作合規的證明。
如果所選的 SDK 依賴無法被驗證為允許的配對方法,請勿建議將該方法用於 iOS 部署。取得 SDK 提供者的釐清並選擇合規的替代方案。在沒有允許的機制可以可靠地建立推薦關係的情況下,請保留正常的引導流程並將安裝標記為未歸因。
參考資料:Apple 使用者隱私權與資料使用。
參見:推薦追蹤 ──> App 參數安裝
步驟 1:部署客戶端分享回報 API
在 Android 與 iOS 入口點初始化原生歸因 SDK
在應用程式能夠回報分享活動或擷取進來的推薦上下文之前,原生行動 SDK 必須在應用程式生命周期的早期進行初始化。
在 Android 上,初始化屬於自訂 Application 子類別的 onCreate() 方法,確保在前景 UI 元件掛載之前建立歸因監聽器與核心記憶體結構。在 iOS 上,初始化則發生在 application(_:didFinishLaunchingWithOptions:) 執行委派中。
開發人員可以從 OpoInstall SDK 下載中心直接取得官方行動 SDK 套件。
在觸發原生作業系統分享面板之前叫用分享回報方法
應用程式應在開啟平台分享介面之前記錄分享意圖。透過應用程式擁有的遙測服務或針對所使用之確切 SDK 版本驗證過的歸因 SDK 方法來實作此功能。以下範例使用應用程式擁有的 shareTelemetry.reportIntent(...) 抽象化;它並非官方的 OpoInstall API。配接器必須為所部署的應用程式實作實際的網路請求、錯誤處理與回呼行為。
為了維持乾淨的遙測資料,工程團隊必須區分分享生命週期中的五個不同事件:
- 分享意圖已初始化(
share_intent_initiated):客戶端應用程式 UI 記錄使用者點擊了邀請動作。 - 分享已分發(
share_dispatched):客戶端應用程式將酬載傳遞至已識別的目標平台(例如啟動明確的社群意圖)或開啟系統分享選擇器。 - 分享已完成(
share_completed):第三方社群 SDK 傳回已驗證的完成回呼,確認訊息已發布或分發(僅在使用支援回呼的專屬第三方平台 SDK 時適用)。 - 推薦已點擊(
referral_click):收件者開啟網頁到達網頁,觸發網頁端進場記錄。 - 推薦已綁定(
referral_bound):收件者完成帳戶註冊,後端驗證候選的shareCode並將推薦關係寫入資料庫。
記錄分享遙測資料可擷取使用者分享的意圖。這允許分析管線將邀請者的 shareCode 對應至指定的平台管道,讓系統準備好關聯後續的網頁到達網頁訪問。
指定平台識別碼:對應社群管道
以下識別碼僅為說明範例。請使用所部署之 SDK 版本所支援的確切管道列舉,並針對其官方 API 參考驗證每個識別碼:
- 封閉式訊息網路:
WechatSession(微信直接聊天)、WechatTimeline(微信朋友圈)、Line、WhatsApp、FacebookMessenger。 - 社群動態與公開微部落格:
Twitter、Facebook、Instagram、Weibo、Douyin、TikTok。 - 直接作業系統傳輸:
Sms、Email、Copy(剪貼簿連結複製)以及Other(系統分享面板)。
當應用程式啟動無法事先判定使用者最終目的地選擇的通用系統分享選擇器時,事件應歸類為 Other,而不是寫死特定的社群網路。
建構邀請者身分權杖:shareCode 格式化的最佳做法
shareCode 作為應用程式層級的商業權杖,將新安裝連接回推薦帳戶。雖然開發人員可以使用任意的英數字字串,但遵守結構上的最佳做法可防止參數損壞:
- 不透明的伺服器發行權杖:
shareCode應為不透明的伺服器發行推薦權杖(例如網址安全權杖或 UUID,如usr_9f82a1b4),而不是可列舉的原始資料庫自動遞增 ID(1024)。應用程式後端將此權杖解析為邀請者帳戶,執行過期與速率限制。 - 長度限制:保持權杖精簡(建議在 64 字元以下;嚴格限制在
\le 128 字元內),以避免膨脹 URL 長度或在訊息應用程式中觸發字串截斷。 - 安全字元集:將字元限制為網址安全的英數字值、連字號與底線(
[A-Za-z0-9_-])。避免使用會改變 URI 解析的原始分隔符號,例如&、=、?或空格。
在 Android 11(API 級別 30)及更新版本中,查詢是否已安裝第三方社群套件需要在 AndroidManifest.xml 中宣告對應的 <queries> 元素,依據 Google Android 開發人員指南關於 Android 11 中的套件能見度。以下實作模式說明如何宣告套件能見度、處理非阻塞的分享回報逾時、管理生命週期安全的狀態重設,以及透過剪貼簿備用方案分發明確的社群意圖:
<!-- Android 設定與客戶端分享實作 -->
<!-- 第 1 部分:AndroidManifest.xml 套件能見度與應用程式宣告 -->
<!-- 檔案:app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- Android 11+ (API 30+) 所需的明確套件能見度宣告 -->
<queries>
<package android:name="com.tencent.mm" />
<package android:name="jp.naver.line.android" />
</queries>
<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".InviteActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
// Android 應用程式生命週期入口
// 第 2 部分:CustomApplication.kt (SDK 生命週期初始化)
// 檔案:app/src/main/java/com/example/app/CustomApplication.kt
package com.example.app
import android.app.Application
import com.opoinstall.api.OpoInstall
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
// 在活動掛載之前於主執行緒上初始化 OpoInstall 核心客戶端
OpoInstall.initialize(this)
}
}
// Android 分享活動實作
// 第 3 部分:InviteActivity.kt (應用程式遙測與具備剪貼簿備用方案的目標分發)
// 檔案:app/src/main/java/com/example/app/InviteActivity.kt
package com.example.app
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.util.concurrent.atomic.AtomicBoolean
class InviteActivity : AppCompatActivity() {
companion object {
private const val TAG = "ReferralShareTracking"
// 範例應用程式擁有的重新導向端點;請替換為已部署的 HTTPS 路由
private const val BASE_INVITE_HOST = "promo.example.com"
private const val BASE_INVITE_PATH = "invite.html"
// 用於明確意圖路由的目標平台套件
private const val PACKAGE_WECHAT = "com.tencent.mm"
private const val PACKAGE_LINE = "jp.naver.line.android"
// 應用程式定義的範例逾時;請針對實際的遙測服務進行調整與測試
private const val SHARE_TIMEOUT_MS = 800L
}
// 用於防止快速連續分享意圖叫用的防彈跳 (debounce) 保護機制
private val isSharingInProgress = AtomicBoolean(false)
// 應用程式擁有的遙測配接器抽象化
private val shareTelemetry = ShareTelemetryService()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_invite)
val btnShareWeChat = findViewById<Button>(R.id.btn_share_wechat)
val btnShareLine = findViewById<Button>(R.id.btn_share_line)
val btnShareGeneric = findViewById<Button>(R.id.btn_share_generic)
// 說明性模擬權杖:在生產環境中,請從已驗證的工作階段中擷取伺服器發行的權杖
val currentInviterToken = "usr_9f82a1b4"
btnShareWeChat.setOnClickListener {
initiateShareFlow(currentInviterToken, "WechatSession", targetPackage = PACKAGE_WECHAT)
}
btnShareLine.setOnClickListener {
initiateShareFlow(currentInviterToken, "Line", targetPackage = PACKAGE_LINE)
}
btnShareGeneric.setOnClickListener {
// 未區段化的系統分享面板;平台分類為 Other
initiateShareFlow(currentInviterToken, "Other", targetPackage = null)
}
}
/**
* 用於在回報特定管道遙測之前驗證目標社群 App 是否已安裝的輔助方法。
* 在 Android 11+ (API 30+) 上需要 AndroidManifest.xml 中的 <queries> 宣告。
*/
private fun isAppInstalled(packageName: String?): Boolean {
if (packageName.isNullOrEmpty()) return false
return try {
packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
true
} catch (e: PackageManager.NameNotFoundException) {
false
} catch (e: Exception) {
Log.w(TAG, "$packageName 的套件檢查例外狀況:${e.message}")
false
}
}
/**
* 將完整的邀請 URL 複製到系統剪貼簿作為最後的備用方案。
*/
private fun copyInviteLinkToClipboard(shareUri: String): Boolean {
return try {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("推薦邀請連結", shareUri))
true
} catch (e: Exception) {
Log.e(TAG, "複製邀請連結失敗:${e.message}", e)
false
}
}
private fun showShareFailureFallback(shareUri: String) {
val copied = copyInviteLinkToClipboard(shareUri)
val message = if (copied) {
"無法開啟分享應用程式。邀請連結已複製到剪貼簿。"
} else {
"無法開啟分享應用程式或複製邀請連結。"
}
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
/**
* 透過應用程式擁有的遙測記錄分享意圖,並具備非阻塞逾時備用方案。
*
* @param shareCode 不透明的邀請者權杖 (嚴格限制 <= 128 字元)
* @param platformIdentifier 符合回報分類法的目標社群管道
* @param targetPackage 特定的應用程式套件名稱,或針對通用系統選擇器傳入 null
*/
private fun initiateShareFlow(shareCode: String, platformIdentifier: String, targetPackage: String?) {
// 強制執行防彈跳檢查
if (!isSharingInProgress.compareAndSet(false, true)) {
Log.d(TAG, "分享叫用已在進行中。忽略重複點擊。")
return
}
val isTargetInstalled = isAppInstalled(targetPackage)
val effectivePlatform = if (isTargetInstalled) platformIdentifier else "Other"
val effectivePackage = if (isTargetInstalled) targetPackage else null
val isDispatched = AtomicBoolean(false)
val mainHandler = Handler(Looper.getMainLooper())
// 非阻塞逾時執行緒:確保即使網路握手延遲,UI 分享面板仍會開啟
val timeoutRunnable = Runnable {
if (isDispatched.compareAndSet(false, true)) {
Log.w(TAG, "預先分享回報在 ${SHARE_TIMEOUT_MS}ms 後逾時。繼續進行分發。")
dispatchShareSafely(shareCode, effectivePlatform, effectivePackage)
}
}
mainHandler.postDelayed(timeoutRunnable, SHARE_TIMEOUT_MS)
// 透過應用程式擁有的遙測配接器非同步回報分享意圖
shareTelemetry.reportIntent(shareCode, effectivePlatform) { result ->
mainHandler.removeCallbacks(timeoutRunnable)
if (isDispatched.compareAndSet(false, true)) {
if (result.isFailure) {
Log.w(TAG, "分享遙測失敗:${result.exceptionOrNull()?.message}")
} else {
Log.d(TAG, "已記錄 $effectivePlatform 的分享意圖")
}
mainHandler.post {
dispatchShareSafely(shareCode, effectivePlatform, effectivePackage)
}
}
}
}
/**
* 執行意圖分發,具備生命週期檢查並在 finally 區塊中保證狀態重設。
*/
private fun dispatchShareSafely(shareCode: String, platform: String, targetPackage: String?) {
try {
if (isFinishing || isDestroyed) {
Log.d(TAG, "活動已結束或銷毀;跳過分享分發。")
return
}
dispatchShareAction(shareCode, platform, targetPackage)
} catch (e: Exception) {
Log.e(TAG, "分享分發遇到未處理的例外狀況:${e.message}", e)
} finally {
isSharingInProgress.set(false)
}
}
private fun dispatchShareAction(shareCode: String, platform: String, targetPackage: String?) {
// 使用 Uri.Builder 安全地建構目標 URL,以強制執行 RFC 3986 編碼
val shareUri = Uri.Builder()
.scheme("https")
.authority(BASE_INVITE_HOST)
.appendEncodedPath(BASE_INVITE_PATH)
.appendQueryParameter("shareCode", shareCode)
.appendQueryParameter("platform", platform)
.build()
.toString()
val shareMessage = "加入我的 App 並獲得迎新優惠券!在此下載:$shareUri"
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, shareMessage)
type = "text/plain"
}
if (targetPackage != null) {
sendIntent.setPackage(targetPackage)
try {
startActivity(sendIntent)
return
} catch (e: Exception) {
Log.w(TAG, "目標 App 啟動失敗:${e.message}", e)
}
}
// 當目標 App 不可用或啟動目標分享失敗時,使用系統分享選擇器
try {
sendIntent.setPackage(null)
val fallbackChooser = Intent.createChooser(sendIntent, "透過以下方式邀請好友:")
startActivity(fallbackChooser)
} catch (e: Exception) {
Log.e(TAG, "系統分享選擇器失敗:${e.message}", e)
showShareFailureFallback(shareUri)
}
}
}
// 說明性應用程式擁有的遙測服務介面
class ShareTelemetryService {
fun reportIntent(shareCode: String, platform: String, callback: (Result<Unit>) -> Unit) {
// 實作:將網路請求分發至應用程式遙測後端
callback(Result.success(Unit))
}
}
若要註冊應用程式設定檔並擷取分享追蹤所需的組態金鑰,團隊可以存取 OpoInstall 開發人員主控台。
步驟 2:設定網頁 JS SDK 以擷取動態分享參數
透過版本驗證的配接器整合網頁 SDK
以下的到達網頁實作是一個說明性的整合模式,並非經過驗證的即插即用 OpoInstall SDK 整合。在部署之前,請根據所部署 SDK 版本的官方說明文件,確認確切的 JavaScript 組合包 URL、初始化簽章、就緒回呼、喚醒或安裝方法以及參數結構描述。
將 SDK 特定的行為保持在小型應用程式配接器後方。配接器應向到達網頁公開記錄的初始化結果以及單一喚醒或安裝操作。只有在驗證對應的方法名稱、回呼行為與組態要求之後,才將這些操作對應至實際的廠商 API。
到達網頁應繼續驗證進來的 shareCode、在核准的備用路由上保留推薦參數,以及防止重複導航。如果 SDK 初始化失敗或其記錄的入口點不可用,請使用已設定的備用路由。除非後端與平台整合已經過測試以支援此功能,否則切勿聲明備用方案可以復原延遲歸因。

透過瀏覽器 URLSearchParams 自動化 shareCode 查詢字串擷取
當建構分享 URL 時,應用程式會將邀請者的權杖附加為查詢參數(例如 https://promo.example.com/invite.html?shareCode=usr_9f82a1b4&platform=WechatSession)。
在到達網頁上,客戶端腳本使用諸如 URLSearchParams 等標準瀏覽器介面來擷取此值。為了防範惡意輸入或跨站腳本攻擊(XSS),腳本應在初始化 SDK 之前針對允許清單與格式模式驗證權杖:
const query = new URLSearchParams(window.location.search);
const rawShareCode = query.get("shareCode");
const safePattern = /^[A-Za-z0-9_-]{1,128}$/;
const sanitizedShareCode = (rawShareCode && safePattern.test(rawShareCode)) ? rawShareCode : "";
將推薦上下文暫存至短期記憶體與系統緩衝區
當網頁 JS SDK 初始化時,它會攝取已淨化的 shareCode 以及任何輔助行銷活動識別碼(channelCode)。當使用者點擊主要的行動呼籲按鈕時,SDK 會協調上下文暫存:
- 歸因伺服器註冊:腳本傳送非同步網路請求,將目前的工作階段與客戶端上下文連結至
shareCode。 - 系統剪貼簿暫存:剪貼簿存取受限於瀏覽器權限、使用者互動要求以及實作特定的安全性政策。網頁實作應將剪貼簿暫存視為選用增強功能,並在存取遭到拒絕或不可用時提供備用方案。
- 備用協調:如果剪貼簿存取受到限制、被使用者提示拒絕,或被後續使用者動作清除,歸因架構將退回至伺服器端工作階段配對或平台支援的安裝推薦來源管道。
將下載 CTA 按鈕綁定以執行延遲參數註冊
到達網頁將其主要的下載按鈕直接綁定到 SDK 的執行方法(wakeupOrInstall())。
網頁 JS SDK 在內部管理重新導向生命週期:它透過已設定的方案或通用連結發起 App 喚醒嘗試,如果 App 尚未安裝,則執行至已設定應用程式商店的延遲重新導向。使用者端程式碼在正常執行期間應依賴 SDK 內部導航管理,並保留直接備用導航給 SDK 腳本載入失敗、丟棄同步例外狀況或被瀏覽器安全性控制封鎖的情境。
以下的實作模式展示如何整合網頁 JS SDK 以擷取進來的 shareCode 查詢參數、偵測站內社群容器、管理點擊佇列狀態,以及跨備用路由保留參數而不會產生重複金鑰或格式錯誤的 URI 片段:
<!-- 網頁 JS SDK 動態分享參數攝取與路由 -->
<!-- 檔案:/var/www/html/invite.html -->
<!--
整合備註:
此範例需要針對所部署 SDK 版本進行驗證的配接器。
請勿假設說明性程式碼片段中顯示的建構子、CDN 路徑、回呼或方法名稱
與目前的廠商 SDK 相符。
-->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>接受您的推薦邀請</title>
<!-- 從官方 CDN 引入經過驗證的 OpoInstall 網頁 JS SDK -->
<script src="https://web.cdn.opoinstallcloud.com/opoinstall.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 20px;
text-align: center;
background-color: #f8fafc;
}
.card {
background: #ffffff;
border-radius: 12px;
padding: 32px 24px;
max-width: 420px;
margin: 40px auto;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
}
button {
background-color: #2563eb;
color: #ffffff;
border: none;
padding: 14px 28px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
width: 100%;
margin-top: 20px;
}
button:disabled {
background-color: #94a3b8;
cursor: not-allowed;
}
/* 針對受限制的站內網頁檢視容器之視覺指引覆蓋層 */
.webview-guide-mask {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
color: #ffffff;
z-index: 9999;
padding-top: 40px;
box-sizing: border-box;
}
.guide-arrow {
font-size: 32px;
text-align: right;
padding-right: 30px;
}
.guide-text {
font-size: 18px;
line-height: 1.6;
margin-top: 20px;
padding: 0 30px;
}
</style>
</head>
<body>
<main class="card">
<h1>歡迎使用 App</h1>
<p id="invite-desc">您的好友已邀請您加入。點擊下方以安裝並領取您的迎新額度。</p>
<!-- 主要轉換按鈕 -->
<button id="btn-install" type="button">下載並領取獎金</button>
<!-- 範例應用程式擁有的重新導向端點;請替換為已部署的 HTTPS 路由 -->
<p style="margin-top: 16px; font-size: 14px;">
<a id="fallback-store-link" href="https://promo.example.com/app-redirect">直接商店連結</a>
</p>
</main>
<!-- 在受限制的訊息網頁檢視(微信、Line)內部顯示的覆蓋提示 -->
<div id="webview-guide" class="webview-guide-mask">
<div class="guide-arrow">↗</div>
<div class="guide-text">
點擊右上角的選單 (•••) 並選擇 <strong>「在瀏覽器中開啟」</strong>(Safari / Chrome)以下載 App。
</div>
</div>
<script>
/**
* 從 window.location 擷取並淨化 shareCode 查詢參數。
* 根據規格強制執行最多 128 字元的網址安全英數字範圍。
*/
function getSanitizedShareCode() {
try {
const query = new URLSearchParams(window.location.search);
const rawCode = query.get("shareCode");
const pattern = /^[A-Za-z0-9_-]{1,128}$/;
return (rawCode && pattern.test(rawCode)) ? rawCode : "";
} catch (e) {
console.error("解析搜尋參數失敗:", e);
return "";
}
}
/**
* 擷取並淨化平台查詢參數。
*/
function getSanitizedPlatform() {
try {
const query = new URLSearchParams(window.location.search);
const rawPlatform = query.get("platform");
const pattern = /^[A-Za-z0-9_]{1,32}$/;
return (rawPlatform && pattern.test(rawPlatform)) ? rawPlatform : "";
} catch (e) {
return "";
}
}
/**
* 偵測受限制的站內瀏覽器容器(微信、Line、WhatsApp、Facebook)。
*/
function detectInAppBrowser() {
const ua = navigator.userAgent.toLowerCase();
return {
isWeChat: ua.indexOf("micromessenger") !== -1,
isLine: ua.indexOf("line/") !== -1,
isWhatsApp: ua.indexOf("whatsapp") !== -1,
isFacebook: ua.indexOf("fbav") !== -1 || ua.indexOf("instagram") !== -1
};
}
/**
* 安全地解碼 URI 元件,在格式錯誤的百分比編碼時退回至原始字串。
*/
function safeDecode(str) {
if (!str) return "";
try {
return decodeURIComponent(str.replace(/\+/g, " "));
} catch (e) {
return str;
}
}
/**
* 執行備用導航,在 URL 解析與字串備用路徑之間取代或更新 shareCode 參數,
* 且不會損壞 URL 片段。
*/
function executeFallbackNavigation(fallbackLink, shareCode, platform) {
if (!fallbackLink || !fallbackLink.href) {
console.error("備用目標連結未定義。");
return;
}
let destinationUrl = fallbackLink.href;
try {
const url = new URL(destinationUrl, window.location.origin);
if (shareCode) {
url.searchParams.set("shareCode", shareCode);
}
if (platform) {
url.searchParams.set("platform", platform);
}
destinationUrl = url.toString();
} catch (err) {
console.warn("URL API 解析失敗,套用強固的字串備用方案:", err);
const hashIndex = destinationUrl.indexOf("#");
const fragment = hashIndex !== -1 ? destinationUrl.substring(hashIndex) : "";
const rawBase = hashIndex !== -1 ? destinationUrl.substring(0, hashIndex) : destinationUrl;
const qIndex = rawBase.indexOf("?");
const pathname = qIndex !== -1 ? rawBase.substring(0, qIndex) : rawBase;
const queryString = qIndex !== -1 ? rawBase.substring(qIndex + 1) : "";
const params = {};
if (queryString) {
const pairs = queryString.split("&");
for (let i = 0; i < pairs.length; i++) {
if (!pairs[i]) continue;
const parts = pairs[i].split("=");
const key = safeDecode(parts[0]);
const value = parts.length > 1 ? safeDecode(parts.slice(1).join("=")) : "";
if (key) {
params[key] = value;
}
}
}
if (shareCode) params["shareCode"] = shareCode;
if (platform) params["platform"] = platform;
const serializedPairs = [];
for (const k in params) {
if (Object.prototype.hasOwnProperty.call(params, k)) {
serializedPairs.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
}
}
const newQuery = serializedPairs.length > 0 ? "?" + serializedPairs.join("&") : "";
destinationUrl = pathname + newQuery + fragment;
}
window.location.href = destinationUrl;
}
document.addEventListener("DOMContentLoaded", function() {
const shareCode = getSanitizedShareCode();
const platform = getSanitizedPlatform();
const inApp = detectInAppBrowser();
const installBtn = document.getElementById("btn-install");
const guideOverlay = document.getElementById("webview-guide");
const fallbackLink = document.getElementById("fallback-store-link");
// 若在會攔截下載的受限制容器中開啟,則顯示視覺指引覆蓋層
if (inApp.isWeChat || inApp.isLine) {
if (guideOverlay) guideOverlay.style.display = "block";
}
// 在 DOM 載入時立即更新靜態備用連結的 href,以便直接點擊能保留參數
if (fallbackLink && fallbackLink.href) {
try {
const initialUrl = new URL(fallbackLink.href, window.location.origin);
if (shareCode) initialUrl.searchParams.set("shareCode", shareCode);
if (platform) initialUrl.searchParams.set("platform", platform);
fallbackLink.href = initialUrl.toString();
} catch (e) {
// 備用解析在點擊時處理
}
}
// 封裝動態參數以供 OpoInstall 攝取
const dynamicData = {
shareCode: shareCode,
platform: platform
};
// SDK 就緒與單次動作狀態
let isSdkReady = false;
let isSdkBlocked = false;
let isClickQueued = false;
let actionStarted = false;
let installerInstance = null;
let clickTimeoutId = null;
// 應用程式定義的初始化備用閾值,並非經過驗證的廠商預設值
const SDK_CLICK_TIMEOUT_MS = 1500;
function clearClickTimeout() {
if (clickTimeoutId !== null) {
clearTimeout(clickTimeoutId);
clickTimeoutId = null;
}
}
function restoreInstallButton() {
if (!installBtn) return;
installBtn.disabled = false;
installBtn.textContent = "下載並領取獎金";
}
function triggerSdkWakeup() {
try {
if (installerInstance && typeof installerInstance.wakeupOrInstall === "function") {
installerInstance.wakeupOrInstall({
data: dynamicData,
channelCode: "viral_social_share"
});
} else {
executeFallbackNavigation(fallbackLink, shareCode, platform);
}
} catch (err) {
console.error("SDK 喚醒失敗:", err);
executeFallbackNavigation(fallbackLink, shareCode, platform);
}
}
// 所有導航路徑都必須透過此單次保護機制
function runOnce(action) {
if (actionStarted) return;
actionStarted = true;
isClickQueued = false;
clearClickTimeout();
if (installBtn) {
installBtn.disabled = true;
installBtn.textContent = "開啟中...";
}
action();
}
function handleInstallClick() {
if (actionStarted) return;
if (isSdkReady) {
runOnce(triggerSdkWakeup);
return;
}
if (isSdkBlocked) {
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
return;
}
// 在 SDK 初始化時將點擊加入佇列
isClickQueued = true;
if (installBtn) {
installBtn.disabled = true;
installBtn.textContent = "正在連線至應用程式商店...";
}
clearClickTimeout();
clickTimeoutId = setTimeout(function() {
if (actionStarted || isSdkReady || !isClickQueued) {
return;
}
isSdkBlocked = true;
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
}, SDK_CLICK_TIMEOUT_MS);
}
if (installBtn) {
installBtn.addEventListener("click", handleInstallClick);
}
// 初始化網頁 JS SDK
if (typeof OpoInstall === "function") {
try {
// 請替換為從已設定的廠商主控台取得的應用程式金鑰;
// 切勿將生產環境密鑰提交至公開的原始碼控制中。
installerInstance = new OpoInstall({
appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
onready: function() {
if (this && typeof this.wakeupOrInstall === "function") {
installerInstance = this;
}
isSdkReady = true;
clearClickTimeout();
// 遲來的回呼絕不能重複已完成的動作
if (actionStarted) return;
if (isClickQueued) {
runOnce(triggerSdkWakeup);
} else {
restoreInstallButton();
}
}
});
} catch (err) {
console.error("OpoInstall 初始化失敗:", err);
isSdkBlocked = true;
clearClickTimeout();
if (isClickQueued) {
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
} else {
restoreInstallButton();
}
}
} else {
console.warn("OpoInstall SDK 不可用;已啟用備用導航。");
isSdkBlocked = true;
restoreInstallButton();
}
});
</script>
</body>
</html>
步驟 3:復原推薦參數與綁定帳戶
在首次 App 啟動期間擷取推薦上下文
安裝後,應用程式應嘗試使用平台和所部署歸因 SDK 支援的機制來擷取推薦上下文。
切勿假設存在於網頁到達網頁 URL 中的參數會自動在 App 商店安裝後存留。
在 Android 上,當安裝路徑與已設定的推薦來源酬載支援時,應用程式可以使用 Google Play 安裝推薦工具。如果第三方歸因 SDK 提供額外的擷取 API,請針對所使用的確切 SDK 版本驗證其方法簽章、回呼行為與酬載結構描述。
在 iOS 上,直接通用連結處理與安裝後推薦復原必須被視為獨立的流程。應用程式必須使用經過記錄、平台允許的機制來進行延遲復原。如果無法驗證該機制或無法復原推薦上下文,請繼續進行引導,而不要將安裝歸因於邀請者。
非阻塞式擷取與重試行為
推薦擷取不得阻塞主要 UI 執行緒或阻止使用者完成註冊。將擷取操作視為非同步,套用適合實際 SDK 或應用程式服務的逾時時間,並針對成功復原、空結果、逾時與失敗記錄不同的結果。
根據記錄的 SDK 行為與應用程式的引導要求來設定逾時時間。此範例中顯示的任何逾時值皆為應用程式層級的組態,除非相關 SDK 說明文件明確確認,否則並非廠商預設值。
當記錄的 API 支援且應用程式具有定義好的重試政策時,可以嘗試重試。防止重複的帳戶綁定請求,並避免在註冊完成後才到達延遲回呼時重複發放獎勵。
伺服器端推薦驗證
將所有客戶端回報的推薦值視為不受信任的輸入。應用程式後端必須驗證候選邀請者權杖、驗證被邀請者的資格,並在建立綁定之前強制執行推薦計畫的商業規則。
使用資料庫層級的唯一性限制或同等的事務控制,來防止同一個被邀請者有多個邀請者綁定。將推薦綁定與獎勵發放分開,並且只有在伺服器上驗證所需的啟用里程碑後才釋放獎勵。

管理微信、Line 與 WhatsApp 中的站內瀏覽器限制
容器沙盒障礙:訊息應用程式如何攔截自訂方案與直接 APK 下載
絕大多數的病毒式推薦分享都發生在行動訊息應用程式內。然而,諸如微信、Line 與 WhatsApp 等平台會在沙盒式站內網頁檢視中呈現外部網頁連結,這些檢視會根據作業系統版本與容器安全性政策,頻繁地攔截、限制或修改直接二進位下載與深度連結方案。
當被邀請者在站內瀏覽器中點擊推薦連結時:
- 直接 APK 下載遭到攔截:在 Android 上,直接從網頁到達網頁下載
.apk二進位檔案的嘗試經常被網頁檢視容器攔截或封鎖。 - 自訂 URL 方案遭到封鎖:私密 URL 方案的叫用(
myapp://invite?code=123)經常執行失敗,導致已經安裝 App 的使用者無法直接啟動原生 App。 - 通用連結可能被抑止:某些訊息應用程式會攔截外向導航,阻止標準的 iOS 通用連結向外解析至作業系統。
偵測站內瀏覽器使用者代理程式(User-Agent)標頭
為了妥善處理這些容器限制,分享到達網頁必須在初始 DOM 載入期間檢查進來的瀏覽器 User-Agent 字串:
- 微信偵測:檢查
navigator.userAgent中的MicroMessenger。 - Line 偵測:檢查
navigator.userAgent中的Line/。 - WhatsApp 偵測:檢查
navigator.userAgent中的WhatsApp。 - Facebook / Messenger / Instagram:檢查
navigator.userAgent中的FBAV或Instagram。
評估重新導向策略:直接 App 交接與系統瀏覽器轉移
作業系統與社群應用程式展現出不同的導航行為:
- 微信與 Line 環境:這些容器會限制直接二進位下載與外部方案執行。嘗試自動重新導向通常會觸發平台警告畫面。到達網頁會顯示明確的視覺指引,提示使用者在裝置的預設系統瀏覽器(iOS 上的 Safari,Android 上的 Chrome)中開啟連結。
- WhatsApp 環境:在現代的 iOS 與 Android 實作中,WhatsApp 通常會在系統提供的站內視埠(例如 iOS 上的 SFSafariViewController 或 Android 上的 Chrome Custom Tabs)中開啟網頁連結。這些環境允許標準的網頁導航並支援轉移至平台應用程式商店,儘管自訂 URL 方案可能仍需要使用者明確點擊。
實作優雅的使用者指引:當直接重新導向失敗時覆蓋原生選單提示
當站內網頁檢視阻止程式化外部交接時,到達網頁會部署視覺化使用者指引:
- 條件式遮罩覆蓋層:當偵測到
MicroMessenger或Line/時,顯示指向螢幕右上角的高對比視覺覆蓋層。 - 明確動作文案:提示指示使用者:「點擊右上角的選單 (•••) 並選擇『在瀏覽器中開啟』(Safari / Chrome)以下載 App。」
- 保留參數狀態:如果使用者透過載入相同的 URL 轉移至外部瀏覽器,只要中間的重新導向沒有剝除它們,網址列中的查詢參數(
?shareCode=...)就會保持完整。當到達網頁在 Safari 或 Chrome 中重新載入時,網頁 JS SDK 會乾淨俐落地執行並暫存推薦上下文。
疑難排解跨社群漏斗的推薦參數遺失
診斷跨侵入式站內連結包裝代理伺服器的參數剝除
某些社群平台會透過中間的連結包裝或包裝網域(例如 Facebook 的 l.facebook.com 或 Twitter 的 t.co)來路由分享的外部連結。在某些情況下,這些重新導向跳躍會剝除非標準的查詢參數或附加衝突的內部點擊識別碼(fbclid)。
為了防止跨多跳社群重新導向的參數遺失:
- 標準參數命名:根據 IETF RFC 3986 堅持使用標準的英數字查詢金鑰,例如
shareCode和channelCode。 - 預先飛行連結驗證:在目標訊息網路上的即時聊天工作 sessions 中測試連結解析,檢查最終到達網頁上的
window.location.search,以驗證shareCode參數在所有中間重新導向中存留。
處理桌面網頁分享:當使用者在電腦瀏覽器上開啟行動推薦連結時
雖然推薦計畫專為行動裝置而設計,但使用者經常會在桌上型電腦(例如 WhatsApp 網頁版、Mac/Windows 版微信或 Slack)上開啟聊天連結。如果桌面使用者存取行動下載到達網頁,行動商店重新導向將會失敗。
企業到達網頁透過響應式降級來處理桌面流量:
- 桌面環境偵測:網頁 JS SDK 偵測桌面作業系統(
Windows、macOS、Linux)。 - 動態 QR Code 產生:響應式範本不會顯示無法點擊的行動商店按鈕,而是可以在桌面視埠上有條件地顯示 QR Code 容器。
- 跨裝置掃描:使用者使用其行動裝置掃描桌面螢幕 QR Code。QR Code 將帶有原始
shareCode的目標 URL 編碼,將推薦工作階段從桌面轉移至行動裝置。
減輕作業系統剪貼簿存取限制
作業系統隱私權控制管轄剪貼簿存取:
- Android 前景焦點規則:在 Android 10(API 級別 30)及更新版本中,背景應用程式無法透過 Android ClipboardManager 讀取剪貼簿資料。原生 SDK 必須延遲剪貼簿檢查,直到應用程式的主要 Activity 獲得主動視窗焦點為止。
- iOS 剪貼簿提示:在 iOS 16 及更新版本中,對 Apple UIPasteboard 的程式化存取可能會觸發系統通知或提示,要求使用者允許從來源瀏覽器貼上。
- 網路備用協調:歸因架構透過將本地剪貼簿緩衝區視為機會性增強功能來考量這些限制,當剪貼簿讀取遭到拒絕或不可用時,退回至伺服器端工作階段配對。
跨實體裝置之社群推薦漏斗的預先飛行驗證協定
在發起客戶推薦計畫之前,成長工程團隊應執行以下系統化驗證檢查清單:
- Android 目標分享備用方案:確認當微信或 Line 未安裝時,App 會優雅地退回到系統分享選擇器。
- Android 剪貼簿連結複製備用方案:確認如果系統分享選擇器失敗或丟棄例外狀況,完整的推薦連結會被複製到剪貼簿並顯示 Toast 通知。
- H5 SDK 延遲與單次導航:模擬超過 1500ms 的 SDK 初始化延遲,以驗證備用導航能夠乾淨俐落地執行,且遲來的
onready事件不會觸發重複導航。 - H5 參數與片段保留:使用包含現有查詢參數、重複金鑰與
#fragments的 URL 測試到達網頁備用導航,以驗證乾淨的參數取代。 - iOS 通用連結與商店交接:測試現有使用者的直接 App 開啟,並針對 Apple 的反指紋識別規則評估安裝後參數復原。
- 即時社群訊息分發:將連結分發至活躍的微信與 Line 聊天工作階段中,以驗證站內瀏覽器偵測覆蓋層引導使用者前往 Safari 或 Chrome 而不會丟棄
shareCode。 - 後端綁定與獎勵等冪性:驗證應用程式後端接受候選權杖、拒絕重複綁定,並將獎勵保留在待處理狀態,直到完成已驗證的里程碑。
推薦追蹤架構之比較分析
比較推薦追蹤架構
| 架構構面 | 手動促銷代碼 | 裝置指紋識別 | 具備平台支援復原的明確推薦參數 |
|---|---|---|---|
| 使用者引導摩擦力 | 需要手動輸入 | 當配對成功時可能較低 | 當推薦上下文成功復原時較低 |
| 歸因基礎 | 由後端驗證的使用者輸入代碼 | 基於實作特定訊號的機率性配對 | 明確的推薦上下文結合支援的復原機制 |
| 歸因可靠性 | 取決於準確的代碼輸入與驗證 | 可能受訊號變更、平台限制與隱私權要求影響 | 取決於參數連續性、平台功能與選定的復原機制 |
| 隱私權與合規性 | 需要安全的權杖處理與反濫用控制 | 必須針對適用的平台政策進行評估;不得使用被禁止的指紋識別 | 需要檢閱 SDK 實作與適用的平台規則 |
| 營運複雜度 | 初始實作相對簡單;手動兌換會產生額外的摩擦力 | 需要配對邏輯、監控與政策檢閱 | 需要連結路由、平台特定整合、後端驗證與備用處理 |
實作複雜度、轉換摩擦力與歸因準確性之間的取捨
手動促銷代碼相對簡單實作,但它們要求使用者輸入並兌換代碼。這可能會引入摩擦力,並為預期推薦關係之外的代碼分享創造機會。
裝置指紋識別似乎可以減少使用者互動,但其可靠性取決於配對實作與可用的訊號。在與適用的隱私權要求(包括 Apple 對禁止指紋識別的限制)衝突的情況下,絕對不得使用它。
明確的推薦參數提供了預期邀請者的清晰商業級表示。然而,在網頁 URL 中攜帶權杖並不等於在 App 商店安裝後復原該權杖。整體解決方案必須結合參數連續性、平台支援的復原機制以及伺服器端驗證。當復原失敗時,安裝應維持未歸因狀態,而不是指派給未經驗證的邀請者。
常見問題 (FAQ)
自動化推薦追蹤如何在沒有促銷代碼的情況下透過 App Store 傳遞參數?
為什麼在微信或 Line 內部分享連結時,推薦參數會被丟棄?
應用程式後端如何防止詐欺性推薦迴圈?
總結與決策框架
自動化的行動推薦追蹤可以透過網頁分享流程攜帶明確的邀請者權杖,並在應用程式後端驗證產生的推薦關係,從而減少手動輸入推薦代碼的情況。
可靠的實作需要的不仅仅是新增 shareCode 查詢參數。到達網頁必須透過支援的重新導向保留權杖,應用程式必須針對相關平台使用經過驗證的復原機制,且後端必須驗證帳戶資格並強制執行等冪綁定與獎勵發放。
Android 與 iOS 應各別進行評估。Google Play 安裝推薦工具可以在 Android 上提供支援的安裝後設資料。在 iOS 上,通用連結在安裝應用程式時支援直接路由,但它們不會自動保證 App Store 安裝後的延遲參數復原。任何額外的復原機制都必須針對適用的平台隱私權要求進行記錄、測試與檢閱。
為了進行生產環境部署,請針對正在整合的確切版本驗證所有的 SDK 方法名稱、組態值、管道識別碼與回呼結構描述。在乾淨的裝置上以及目標訊息應用程式中測試完整的旅程。當無法透過支援的機制復原推薦時,請允許引導程序繼續並將結果記錄為未歸因,而不是建立未經驗證的推薦關係。
OpoInstall 的 SDK 與開發人員說明文件可能會提供用於實作此架構的元件。在生產環境推薦計畫中依賴它們之前,請確認所部署版本所支援的特定功能與平台行為。
如需整合詳細資料,請參閱 OpoInstall 開發人員說明文件、OpoInstall SDK 下載中心以及 OpoInstall 開發人員主控台。請使用對應的官方 Android 與 Apple 平台說明文件來驗證與應用程式相關的安裝與隱私權行為。
相關資料
-
概念:自動化推薦追蹤、邀請者-被邀請者綁定、延遲深度連結、站內網頁檢視沙盒化
-
技術:行動歸因 SDK、網頁 JS SDK、通用連結、Android App 連結、S2S Postback
-
整合介面:應用程式擁有的分享意圖遙測、所部署 SDK 版本的記錄安裝推薦來源或歸因 API、平台特定深度連結路由,以及伺服器端推薦驗證。
-
官方說明文件與參考資料:
Share this article



