From 1c1769a9e97d3dc68fe35c56af13e9e5a7fc209a Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 30 四月 2026 11:25:21 +0800
Subject: [PATCH] feat:openinstall接入

---
 traditionalApp/pages/register/register.vue                    |    3 
 traditionalApp/utils/openinstall.js                           |   98 ++++++++++++++++
 tcm-training-admin-backend/src/main/resources/application.yml |    2 
 traditionalApp/utils/utils.js                                 |   84 ++++++++++++-
 traditionalApp/App.vue                                        |   93 ++++++++++-----
 traditionalApp/manifest.json                                  |   36 +++++
 6 files changed, 272 insertions(+), 44 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/resources/application.yml b/tcm-training-admin-backend/src/main/resources/application.yml
index 97ed108..29724ec 100644
--- a/tcm-training-admin-backend/src/main/resources/application.yml
+++ b/tcm-training-admin-backend/src/main/resources/application.yml
@@ -122,7 +122,7 @@
       jwks-url: https://appleid.apple.com/auth/keys
   share:
     # 邀请二维码默认指向线上下载页,开发环境可在 application-local.yml 覆盖。
-    landing-base-url: https://haizhentong.com/app-download/
+    landing-base-url: https://ag8spw.oplinking.com/
 jpush:
   appKey: b5aed7d076638a0d835786bd
   masterSecret: e17305459be4f0011ca15c9e
diff --git a/traditionalApp/App.vue b/traditionalApp/App.vue
index 2fd1ad1..57d9944 100644
--- a/traditionalApp/App.vue
+++ b/traditionalApp/App.vue
@@ -7,60 +7,90 @@
 		extractBindTicket,
 		extractInviteCode
 	} from '@/utils/utils.js'
+	import {
+		getOpeninstallInstallData,
+		initOpeninstall,
+		registerOpeninstallWakeUp
+	} from '@/utils/openinstall.js'
 
-	let lastHandledBindTicket = ''
+	let lastHandledInvitePayload = ''
 
 	/**
-	 * 应用启动时保留钩子,当前不再依赖系统剪贴板承接推广关系。
+	 * 应用启动时初始化极光与 openinstall,承接推广关系参数。
 	 */
 	onLaunch(() => {
 		// #ifdef APP-PLUS
-		const jpushModule = uni.requireNativePlugin("JG-JPush")
-
-		jpushModule.setLoggerEnable(true);
-		jpushModule.initJPushService();
-		// 监听是否连接成功
-		jpushModule.addConnectEventListener(result => {
-			console.log('qweqweqwe', result);
-			let connectEnable = result.connectEnable;
-			console.log(connectEnable, '极光连接成功');
-			if (uni.getStorageSync('token')) {
-
-			}
-		});
-
-		jpushModule.addNotificationListener(result => {
-			console.log('qeqwewqe', result);
-			// plus.runtime.setBadgeNumber('111111');
-			// jpushModule.setBadge('2222222222222')
-
-
-		});
+		initJPush()
+		initOpeninstall()
+		registerOpeninstallWakeUp(handleOpeninstallPayload)
+		getOpeninstallInstallData(handleOpeninstallPayload)
 		// #endif
 	})
 
 	/**
-	 * App 被外部 Scheme 拉起时,解析 bindTicket 并跳转注册页。
+	 * App 被外部 Scheme 拉起时,解析 bindTicket 或 inviteCode 并跳转注册页。
 	 */
 	onShow(() => {
 		handleOpenByScheme()
 	})
 
 	/**
-	 * 读取原生启动参数中的绑定票据,并在命中时跳转注册页。
+	 * 初始化极光推送服务。
+	 */
+	function initJPush() {
+		// #ifdef APP-PLUS
+		const jpushModule = uni.requireNativePlugin('JG-JPush')
+
+		jpushModule.setLoggerEnable(true)
+		jpushModule.initJPushService()
+		// 监听极光连接状态,便于联调确认服务是否可用。
+		jpushModule.addConnectEventListener(result => {
+			console.log('极光连接状态', result)
+		})
+
+		jpushModule.addNotificationListener(result => {
+			console.log('极光通知回调', result)
+		})
+		// #endif
+	}
+
+	/**
+	 * 读取原生启动参数中的绑定票据或邀请码,并在命中时跳转注册页。
 	 */
 	function handleOpenByScheme() {
 		// #ifdef APP-PLUS
 		const runtimeArguments = typeof plus !== 'undefined' && plus.runtime ? plus.runtime.arguments : ''
-		const bindTicket = extractBindTicket(runtimeArguments)
-		if (!bindTicket || bindTicket === lastHandledBindTicket) {
+		handleInvitePayload(runtimeArguments)
+		// #endif
+	}
+
+	/**
+	 * 处理 openinstall 安装参数或唤醒参数。
+	 */
+	function handleOpeninstallPayload(payload) {
+		handleInvitePayload(payload)
+	}
+
+	/**
+	 * 统一解析外部推广参数,并跳转到注册页回填。 
+	 */
+	function handleInvitePayload(payload) {
+		const bindTicket = extractBindTicket(payload)
+		const inviteCode = extractInviteCode(payload)
+		if (!bindTicket && !inviteCode) {
 			return
 		}
-		lastHandledBindTicket = bindTicket
-		const inviteCode = extractInviteCode(runtimeArguments)
+		const payloadKey = `${bindTicket || ''}_${inviteCode || ''}`
+		if (payloadKey === lastHandledInvitePayload) {
+			return
+		}
+		lastHandledInvitePayload = payloadKey
 		const currentPages = getCurrentPages()
 		const currentRoute = currentPages && currentPages.length > 0 ? currentPages[currentPages.length - 1].route : ''
-		const targetParams = [`bindTicket=${encodeURIComponent(bindTicket)}`]
+		const targetParams = []
+		if (bindTicket) {
+			targetParams.push(`bindTicket=${encodeURIComponent(bindTicket)}`)
+		}
 		if (inviteCode) {
 			targetParams.push(`inviteCode=${encodeURIComponent(inviteCode)}`)
 		}
@@ -74,7 +104,6 @@
 		uni.navigateTo({
 			url: targetUrl
 		})
-		// #endif
 	}
 </script>
 
@@ -82,4 +111,4 @@
 	/*每个页面公共css */
 	@import "@/uni_modules/uview-pro/index.scss";
 	@import "@/static/style/index.scss";
-</style>
\ No newline at end of file
+</style>
diff --git a/traditionalApp/manifest.json b/traditionalApp/manifest.json
index 4995e98..d129b26 100644
--- a/traditionalApp/manifest.json
+++ b/traditionalApp/manifest.json
@@ -48,7 +48,8 @@
 					"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
 				],
 				"minSdkVersion": 28,
-				"targetSdkVersion": 30
+				"targetSdkVersion": 30,
+				"schemes": "ag8spw"
 			},
 			/* ios打包配置 */
 			"ios": {
@@ -61,11 +62,13 @@
 				"capabilities": {
 					"entitlements": {
 						"com.apple.developer.associated-domains": [
-							"applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com"
+							"applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com",
+							"applinks:ag8spw.oplinking.com"
 						]
 					}
 				},
-				"idfa": false
+				"idfa": false,
+				"urltypes": "ag8spw"
 			},
 			/* SDK配置 */
 			"sdkConfigs": {
@@ -129,6 +132,33 @@
 			}
 		},
 		"nativePlugins": {
+			"openinstall-plugin": {
+				"APP_KEY_ANDROID": "ag8spw",
+				"APP_KEY_IOS": "ag8spw",
+				"__plugin_info__": {
+					"name": "openinstall",
+					"description": "openinstall 一键拉起与免填邀请码插件",
+					"platforms": "Android,iOS",
+					"url": "https://ext.dcloud.net.cn/plugin?id=692",
+					"android_package_name": "com.HaiZhenKangDao.hzt",
+					"ios_bundle_id": "com.HaiZhenKangDao.hzt",
+					"isCloud": true,
+					"bought": 1,
+					"pid": "692",
+					"parameters": {
+						"APP_KEY_ANDROID": {
+							"des": "[Android]openinstall 控制台分配的 AppKey",
+							"key": "com.openinstall.APP_KEY",
+							"value": ""
+						},
+						"APP_KEY_IOS": {
+							"des": "[iOS]openinstall 控制台分配的 AppKey",
+							"key": "com.openinstall.APP_KEY",
+							"value": ""
+						}
+					}
+				}
+			},
 			"JG-JCore": {
 				"JPUSH_APPKEY_ANDROID": "b5aed7d076638a0d835786bd",
 				"JPUSH_APPKEY_IOS": "b5aed7d076638a0d835786bd",
diff --git a/traditionalApp/pages/register/register.vue b/traditionalApp/pages/register/register.vue
index 5902300..0d7abf3 100644
--- a/traditionalApp/pages/register/register.vue
+++ b/traditionalApp/pages/register/register.vue
@@ -176,6 +176,7 @@
 	validateInviteBindTicket
 } from './service.js'
 import { extractBindTicket, extractInviteCode, normalizeOptionalText } from '@/utils/utils.js'
+import { reportOpeninstallRegister } from '@/utils/openinstall.js'
 import { confirmCameraForInviteCodeScan, confirmMediaForLicense } from '@/utils/permission.js'
 
 interface RegionItem {
@@ -816,6 +817,8 @@
  * 注册成功后提示并返回登录页。
  */
 const handleRegisterSuccess = async (message: string) => {
+	// 注册成功后通知 openinstall,用于统计推广链路的注册转化。
+	reportOpeninstallRegister()
 	uni.showToast({
 		title: message,
 		icon: 'success'
diff --git a/traditionalApp/utils/openinstall.js b/traditionalApp/utils/openinstall.js
new file mode 100644
index 0000000..7faae0b
--- /dev/null
+++ b/traditionalApp/utils/openinstall.js
@@ -0,0 +1,98 @@
+let openinstallPlugin = null
+let initialized = false
+let wakeUpRegistered = false
+
+/**
+ * 安全获取 openinstall 原生插件实例,非 APP 或插件不存在时返回空。
+ */
+export const getOpeninstallPlugin = () => {
+	// #ifdef APP-PLUS
+	if (openinstallPlugin) {
+		return openinstallPlugin
+	}
+	try {
+		openinstallPlugin = uni.requireNativePlugin('openinstall-plugin')
+	} catch (error) {
+		console.log('openinstall 插件不可用', error)
+		openinstallPlugin = null
+	}
+	return openinstallPlugin
+	// #endif
+	// #ifndef APP-PLUS
+	return null
+	// #endif
+}
+
+/**
+ * 初始化 openinstall 插件,避免多次启动重复初始化。
+ */
+export const initOpeninstall = () => {
+	const plugin = getOpeninstallPlugin()
+	if (!plugin || initialized) {
+		return false
+	}
+	try {
+		plugin.init()
+		initialized = true
+		return true
+	} catch (error) {
+		console.log('openinstall 初始化失败', error)
+		return false
+	}
+}
+
+/**
+ * 注册 openinstall 拉起参数监听,用于已安装 APP 的扫码唤醒场景。
+ */
+export const registerOpeninstallWakeUp = (callback) => {
+	const plugin = getOpeninstallPlugin()
+	if (!plugin || wakeUpRegistered || typeof callback !== 'function') {
+		return false
+	}
+	try {
+		plugin.registerWakeUp((result) => {
+			callback(result)
+		})
+		wakeUpRegistered = true
+		return true
+	} catch (error) {
+		console.log('openinstall 拉起监听注册失败', error)
+		return false
+	}
+}
+
+/**
+ * 读取 openinstall 安装参数,用于未安装 APP 下载后首次打开场景。
+ */
+export const getOpeninstallInstallData = (callback) => {
+	const plugin = getOpeninstallPlugin()
+	if (!plugin || typeof callback !== 'function') {
+		return false
+	}
+	try {
+		plugin.getInstall(8, (result) => {
+			callback(result)
+		})
+		return true
+	} catch (error) {
+		console.log('openinstall 安装参数获取失败', error)
+		return false
+	}
+}
+
+/**
+ * 注册成功后上报 openinstall,便于后台统计注册转化。
+ */
+export const reportOpeninstallRegister = () => {
+	const plugin = getOpeninstallPlugin()
+	if (!plugin) {
+		return false
+	}
+	try {
+		plugin.reportRegister()
+		return true
+	} catch (error) {
+		console.log('openinstall 注册上报失败', error)
+		return false
+	}
+}
diff --git a/traditionalApp/utils/utils.js b/traditionalApp/utils/utils.js
index f129ac8..7032f75 100644
--- a/traditionalApp/utils/utils.js
+++ b/traditionalApp/utils/utils.js
@@ -225,33 +225,101 @@
 }
 
 /**
- * 从 URL、纯文本或扫码内容中提取 6 位邀请码。
+ * 将外部唤醒参数归一化为可解析文本,兼容 openinstall 对象、JSON 和 URL。
+ */
+export const normalizeInvitePayloadText = (value) => {
+	if (!value) {
+		return ''
+	}
+	if (typeof value === 'string') {
+		return value.trim()
+	}
+	if (typeof value !== 'object') {
+		return ''
+	}
+	const candidateKeys = ['bindData', 'data', 'channelData', 'params', 'wakeUpData']
+	for (const key of candidateKeys) {
+		const nestedValue = value[key]
+		if (nestedValue) {
+			const nestedText = normalizeInvitePayloadText(nestedValue)
+			if (nestedText) {
+				return nestedText
+			}
+		}
+	}
+	try {
+		return JSON.stringify(value)
+	} catch {
+		return ''
+	}
+}
+
+/**
+ * 从 URL、纯文本、对象或扫码内容中提取 6 位邀请码。
  */
 export const extractInviteCode = (value) => {
-	const normalizedValue = normalizeOptionalText(value)
+	const normalizedValue = normalizeInvitePayloadText(value)
 	if (!normalizedValue) {
 		return ''
 	}
-	const queryMatchedInviteCode = normalizedValue.match(/[?&]inviteCode=([^&#]+)/i)
+	const objectInviteCode = extractObjectField(normalizedValue, 'inviteCode')
+	if (objectInviteCode) {
+		return normalizeInviteCodeText(objectInviteCode)
+	}
+	const queryMatchedInviteCode = normalizedValue.match(/(?:^|[?&])inviteCode=([^&#]+)/i)
 	if (queryMatchedInviteCode && queryMatchedInviteCode[1]) {
-		const decodedInviteCode = decodeURIComponent(queryMatchedInviteCode[1]).trim().toUpperCase()
-		return /^[A-Z0-9]{6}$/.test(decodedInviteCode) ? decodedInviteCode : ''
+		return normalizeInviteCodeText(decodeURIComponent(queryMatchedInviteCode[1]))
 	}
 	const matchedInviteCode = normalizedValue.toUpperCase().match(/[A-Z0-9]{6}/)
 	return matchedInviteCode ? matchedInviteCode[0] : ''
 }
 
 /**
- * 从 URL 中提取 bindTicket 参数。
+ * 从 URL、对象或 JSON 文本中提取 bindTicket 参数。
  */
 export const extractBindTicket = (value) => {
-	const normalizedValue = normalizeOptionalText(value)
+	const normalizedValue = normalizeInvitePayloadText(value)
 	if (!normalizedValue) {
 		return ''
 	}
-	const queryMatchedBindTicket = normalizedValue.match(/[?&]bindTicket=([^&#]+)/i)
+	const objectBindTicket = extractObjectField(normalizedValue, 'bindTicket')
+	if (objectBindTicket) {
+		return objectBindTicket.trim()
+	}
+	const queryMatchedBindTicket = normalizedValue.match(/(?:^|[?&])bindTicket=([^&#]+)/i)
 	if (!queryMatchedBindTicket || !queryMatchedBindTicket[1]) {
 		return ''
 	}
 	return decodeURIComponent(queryMatchedBindTicket[1]).trim()
 }
+
+/**
+ * 从 JSON 字符串中读取指定字段。
+ */
+const extractObjectField = (value, fieldName) => {
+	try {
+		const parsedValue = JSON.parse(value)
+		if (!parsedValue || typeof parsedValue !== 'object') {
+			return ''
+		}
+		const directValue = parsedValue[fieldName]
+		if (typeof directValue === 'string') {
+			return directValue
+		}
+		const bindDataValue = parsedValue.bindData || parsedValue.data || parsedValue.params
+		if (bindDataValue && bindDataValue !== value) {
+			return extractObjectField(normalizeInvitePayloadText(bindDataValue), fieldName)
+		}
+	} catch {
+		return ''
+	}
+	return ''
+}
+
+/**
+ * 标准化邀请码文本。
+ */
+const normalizeInviteCodeText = (value) => {
+	const inviteCode = normalizeOptionalText(value).toUpperCase()
+	return /^[A-Z0-9]{6}$/.test(inviteCode) ? inviteCode : ''
+}

--
Gitblit v1.7.1