From ad93fcac51bebf9ff6204e3cd2c3cba5cc5d6a6f Mon Sep 17 00:00:00 2001
From: 13404089107 <puwei@sinata.cn>
Date: 星期四, 30 四月 2026 13:54:39 +0800
Subject: [PATCH] Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/tcm-training
---
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java | 44 ++++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java | 76 +++++++
traditionalApp/pages/register/register.vue | 3
traditionalApp/utils/openinstall.js | 98 +++++++++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java | 18 +
tcm-training-admin-backend/src/main/resources/application.yml | 6
traditionalApp/utils/utils.js | 84 +++++++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java | 2
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java | 163 ++++++++++++++++
traditionalApp/App.vue | 49 +++-
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java | 11 +
traditionalApp/manifest.json | 35 +++
12 files changed, 566 insertions(+), 23 deletions(-)
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
index 618af18..9bca475 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
@@ -70,11 +70,22 @@
private boolean isPublicAdminApi(String requestPath) {
return "/api/auth/login".equals(requestPath)
|| "/api/auth/captcha".equals(requestPath)
+ || isJpushManualPushApi(requestPath)
|| requestPath.startsWith("/api/open/")
|| requestPath.startsWith("/api/v1/open/");
}
/**
+ * 判断当前后台路由是否属于极光手动推送联调接口。
+ */
+ private boolean isJpushManualPushApi(String requestPath) {
+ return "/api/jpush/manual-push".equals(requestPath)
+ || "/api/v1/jpush/manual-push".equals(requestPath)
+ || requestPath.startsWith("/api/jpush/manual-push/")
+ || requestPath.startsWith("/api/v1/jpush/manual-push/");
+ }
+
+ /**
* 将本地上传目录映射为静态资源,可通过 /uploads/** 访问。
*/
@Override
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java
new file mode 100644
index 0000000..6d1e468
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java
@@ -0,0 +1,76 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.system.dto.JpushManualPushRequest;
+import com.tcm.training.admin.system.service.JpushManualPushAppService;
+import com.tcm.training.admin.system.vo.JpushManualPushVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+/**
+ * 极光手动推送联调接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class JpushManualPushController {
+
+ private final JpushManualPushAppService jpushManualPushAppService;
+
+ public JpushManualPushController(JpushManualPushAppService jpushManualPushAppService) {
+ this.jpushManualPushAppService = jpushManualPushAppService;
+ }
+
+ /**
+ * 按APP用户ID推送固定联调消息。
+ */
+ @PostMapping("/jpush/manual-push")
+ public ApiResponse<JpushManualPushVO> push(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.push(request));
+ }
+
+ /**
+ * 生成会员到期提醒测试通知并推送。
+ */
+ @PostMapping("/jpush/manual-push/member-expire-remind")
+ public ApiResponse<JpushManualPushVO> pushMemberExpireRemind(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.pushMemberExpireRemind(request));
+ }
+
+ /**
+ * 生成直播提醒测试通知并推送。
+ */
+ @PostMapping("/jpush/manual-push/live-remind")
+ public ApiResponse<JpushManualPushVO> pushLiveRemind(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.pushLiveRemind(request));
+ }
+
+ /**
+ * 生成活动通知测试通知并推送。
+ */
+ @PostMapping("/jpush/manual-push/activity-notice")
+ public ApiResponse<JpushManualPushVO> pushActivityNotice(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.pushActivityNotice(request));
+ }
+
+ /**
+ * 生成订单状态更新测试通知并推送。
+ */
+ @PostMapping("/jpush/manual-push/order-status-update")
+ public ApiResponse<JpushManualPushVO> pushOrderStatusUpdate(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.pushOrderStatusUpdate(request));
+ }
+
+ /**
+ * 生成课程更新测试通知并推送。
+ */
+ @PostMapping("/jpush/manual-push/course-update-notice")
+ public ApiResponse<JpushManualPushVO> pushCourseUpdateNotice(@Valid @RequestBody JpushManualPushRequest request) {
+ return ApiResponse.ok(jpushManualPushAppService.pushCourseUpdateNotice(request));
+ }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java
new file mode 100644
index 0000000..1a7b5f0
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java
@@ -0,0 +1,18 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 极光手动推送联调请求。
+ */
+@Getter
+@Setter
+public class JpushManualPushRequest {
+
+ /** 推送目标APP用户ID。 */
+ @NotNull(message = "请选择推送用户")
+ private Long appUserId;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java
new file mode 100644
index 0000000..c549b1d
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java
@@ -0,0 +1,163 @@
+package com.tcm.training.admin.system.service;
+
+import com.tcm.training.admin.app.notice.entity.AppSystemNotice;
+import com.tcm.training.admin.app.notice.service.AppSystemNoticeDispatchCommand;
+import com.tcm.training.admin.app.notice.service.AppSystemNoticeDispatchService;
+import com.tcm.training.admin.app.notice.service.AppSystemNoticeType;
+import com.tcm.training.admin.system.dto.JpushManualPushRequest;
+import com.tcm.training.admin.system.vo.JpushManualPushVO;
+import com.tcm.training.admin.utils.JGPushUtil;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 极光手动推送联调应用服务。
+ */
+@Service
+public class JpushManualPushAppService {
+
+ private static final String TEST_TITLE = "极光推送联调";
+ private static final String TEST_CONTENT = "这是一条极光推送联调测试消息";
+ private static final String TEST_SCENE = "JPUSH_MANUAL_TEST";
+ private static final String MANUAL_NOTICE_TEST_SCENE = "MANUAL_NOTICE_TEST";
+ private static final String PUSH_STATUS_SUCCESS = "SUCCESS";
+
+ private final JGPushUtil jgPushUtil;
+ private final AppSystemNoticeDispatchService appSystemNoticeDispatchService;
+
+ public JpushManualPushAppService(JGPushUtil jgPushUtil,
+ AppSystemNoticeDispatchService appSystemNoticeDispatchService) {
+ this.jgPushUtil = jgPushUtil;
+ this.appSystemNoticeDispatchService = appSystemNoticeDispatchService;
+ }
+
+ /**
+ * 按APP用户ID发起固定内容的极光推送联调。
+ */
+ public JpushManualPushVO push(JpushManualPushRequest request) {
+ String alias = String.valueOf(request.getAppUserId());
+ Map<String, String> payload = new LinkedHashMap<String, String>();
+ payload.put("scene", TEST_SCENE);
+ payload.put("appUserId", alias);
+
+ // 极光APP端alias约定为APP用户ID字符串,用于验证设备绑定与推送链路。
+ jgPushUtil.push(TEST_CONTENT, TEST_TITLE, payload, alias);
+
+ JpushManualPushVO result = new JpushManualPushVO();
+ result.setAppUserId(request.getAppUserId());
+ result.setAlias(alias);
+ result.setTitle(TEST_TITLE);
+ result.setContent(TEST_CONTENT);
+ result.setPushed(true);
+ return result;
+ }
+
+ /**
+ * 生成会员到期提醒测试通知并尝试极光推送。
+ */
+ public JpushManualPushVO pushMemberExpireRemind(JpushManualPushRequest request) {
+ return pushNoticeType(request,
+ AppSystemNoticeType.MEMBER_EXPIRE_REMIND,
+ "会员到期提醒",
+ "您的会员有效期将于测试日期到期",
+ false);
+ }
+
+ /**
+ * 生成直播提醒测试通知并尝试极光推送。
+ */
+ public JpushManualPushVO pushLiveRemind(JpushManualPushRequest request) {
+ return pushNoticeType(request,
+ AppSystemNoticeType.LIVE_REMIND,
+ "直播提醒",
+ "测试直播课程 即将开播",
+ true);
+ }
+
+ /**
+ * 生成活动通知测试通知并尝试极光推送。
+ */
+ public JpushManualPushVO pushActivityNotice(JpushManualPushRequest request) {
+ return pushNoticeType(request,
+ AppSystemNoticeType.ACTIVITY_NOTICE,
+ "活动通知",
+ "这是一条活动通知联调测试消息",
+ true);
+ }
+
+ /**
+ * 生成订单状态更新测试通知并尝试极光推送。
+ */
+ public JpushManualPushVO pushOrderStatusUpdate(JpushManualPushRequest request) {
+ return pushNoticeType(request,
+ AppSystemNoticeType.ORDER_STATUS_UPDATE,
+ "订单状态更新",
+ "您的礼品兑换订单状态已更新",
+ true);
+ }
+
+ /**
+ * 生成课程更新测试通知并尝试极光推送。
+ */
+ public JpushManualPushVO pushCourseUpdateNotice(JpushManualPushRequest request) {
+ return pushNoticeType(request,
+ AppSystemNoticeType.COURSE_UPDATE_NOTICE,
+ "课程更新通知",
+ "测试课程 已更新新章节",
+ true);
+ }
+
+ /**
+ * 按指定通知类型生成系统通知,并复用现有通知推送开关和极光推送链路。
+ */
+ private JpushManualPushVO pushNoticeType(JpushManualPushRequest request,
+ String noticeTypeCode,
+ String title,
+ String content,
+ boolean requireUserSetting) {
+ Long referenceId = System.currentTimeMillis();
+ AppSystemNoticeDispatchCommand command = new AppSystemNoticeDispatchCommand();
+ command.setAppUserId(request.getAppUserId());
+ command.setNoticeTypeCode(noticeTypeCode);
+ command.setTitle(title);
+ command.setContent(content);
+ command.setReferenceType("MANUAL_TEST_" + noticeTypeCode);
+ command.setReferenceId(referenceId);
+ command.setRequireUserSetting(requireUserSetting);
+ command.setPayload(buildManualNoticePayload(request.getAppUserId(), noticeTypeCode, referenceId));
+ AppSystemNotice notice = appSystemNoticeDispatchService.createAndPush(command);
+ return toNoticePushVO(notice);
+ }
+
+ /**
+ * 构造手动通知测试透传参数,便于APP端区分联调消息。
+ */
+ private Map<String, String> buildManualNoticePayload(Long appUserId, String noticeTypeCode, Long referenceId) {
+ Map<String, String> payload = new LinkedHashMap<String, String>();
+ payload.put("scene", MANUAL_NOTICE_TEST_SCENE);
+ payload.put("noticeTypeCode", noticeTypeCode);
+ payload.put("appUserId", String.valueOf(appUserId));
+ payload.put("referenceId", String.valueOf(referenceId));
+ return payload;
+ }
+
+ /**
+ * 转换系统通知推送结果为联调接口响应。
+ */
+ private JpushManualPushVO toNoticePushVO(AppSystemNotice notice) {
+ JpushManualPushVO result = new JpushManualPushVO();
+ result.setNoticeId(notice.getId());
+ result.setAppUserId(notice.getAppUserId());
+ result.setNoticeTypeCode(notice.getNoticeTypeCode());
+ result.setAlias(String.valueOf(notice.getAppUserId()));
+ result.setTitle(notice.getTitle());
+ result.setContent(notice.getContent());
+ result.setPushStatus(notice.getPushStatus());
+ result.setPushErrorMessage(notice.getPushErrorMessage());
+ result.setPushedAt(notice.getPushedAt());
+ result.setPushed(PUSH_STATUS_SUCCESS.equals(notice.getPushStatus()));
+ return result;
+ }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java
new file mode 100644
index 0000000..eb17905
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java
@@ -0,0 +1,44 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * 极光手动推送联调结果。
+ */
+@Getter
+@Setter
+public class JpushManualPushVO {
+
+ /** 系统通知ID。 */
+ private Long noticeId;
+
+ /** 推送目标APP用户ID。 */
+ private Long appUserId;
+
+ /** 通知类型编码。 */
+ private String noticeTypeCode;
+
+ /** 极光推送别名。 */
+ private String alias;
+
+ /** 推送标题。 */
+ private String title;
+
+ /** 推送内容。 */
+ private String content;
+
+ /** 推送状态。 */
+ private String pushStatus;
+
+ /** 推送错误信息。 */
+ private String pushErrorMessage;
+
+ /** 推送完成时间。 */
+ private LocalDateTime pushedAt;
+
+ /** 是否推送成功。 */
+ private Boolean pushed;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java
index b172b21..2f8708b 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java
@@ -50,7 +50,7 @@
.setPlatform(Platform.all())//设置推送平台为全部平台
.setAudience(Audience.alias(tags))//通过别名的方式发送(user_id)
.setNotification(Notification.android(content, title, map))
- .setNotification(Notification.ios(content, map))
+ // .setNotification(Notification.ios(content, map))
// .setNotification(Notification.winphone(content, map))
.build();
}
diff --git a/tcm-training-admin-backend/src/main/resources/application.yml b/tcm-training-admin-backend/src/main/resources/application.yml
index 623eac6..29724ec 100644
--- a/tcm-training-admin-backend/src/main/resources/application.yml
+++ b/tcm-training-admin-backend/src/main/resources/application.yml
@@ -122,10 +122,10 @@
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: d2c5362f73af203f25704b8d
- masterSecret: c6996db56be1a93ae16fc1b0
+ appKey: b5aed7d076638a0d835786bd
+ masterSecret: e17305459be4f0011ca15c9e
logging:
level:
diff --git a/traditionalApp/App.vue b/traditionalApp/App.vue
index b53ec9b..782cb6e 100644
--- a/traditionalApp/App.vue
+++ b/traditionalApp/App.vue
@@ -8,40 +8,70 @@
extractInviteCode
} from '@/utils/utils.js'
import { initJPushLifecycle } from '@/utils/push.js'
+ import {
+ getOpeninstallInstallData,
+ initOpeninstall,
+ registerOpeninstallWakeUp
+ } from '@/utils/openinstall.js'
- let lastHandledBindTicket = ''
+ let lastHandledInvitePayload = ''
/**
- * 应用启动时保留钩子,当前不再依赖系统剪贴板承接推广关系。
+ * 应用启动时初始化极光与 openinstall,承接推广关系参数。
*/
onLaunch(() => {
// #ifdef APP-PLUS
initJPushLifecycle()
+ initOpeninstall()
+ registerOpeninstallWakeUp(handleOpeninstallPayload)
+ getOpeninstallInstallData(handleOpeninstallPayload)
// #endif
})
/**
- * App 被外部 Scheme 拉起时,解析 bindTicket 并跳转注册页。
+ * App 被外部 Scheme 拉起时,解析 bindTicket 或 inviteCode 并跳转注册页。
*/
onShow(() => {
handleOpenByScheme()
})
/**
- * 读取原生启动参数中的绑定票据,并在命中时跳转注册页。
+ * 读取原生启动参数中的绑定票据或邀请码,并在命中时跳转注册页。
*/
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)}`)
}
@@ -55,7 +85,6 @@
uni.navigateTo({
url: targetUrl
})
- // #endif
}
</script>
diff --git a/traditionalApp/manifest.json b/traditionalApp/manifest.json
index 81fab2c..a3df16a 100644
--- a/traditionalApp/manifest.json
+++ b/traditionalApp/manifest.json
@@ -53,6 +53,11 @@
/* ios打包配置 */
"ios" : {
"dSYMs" : false,
+ "privacyDescription": {
+ "NSCameraUsageDescription": "需要使用相机拍摄照片 / 视频,用于扫码自动填充邀请码、拍摄照片作为头像",
+ "NSPhotoLibraryUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码",
+ "NSPhotoLibraryAddUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码"
+ },
"capabilities" : {
"entitlements" : {
"com.apple.developer.associated-domains" : [
@@ -60,7 +65,8 @@
]
}
},
- "idfa" : false
+ "idfa" : false,
+ "urltypes": "ag8spw"
},
/* SDK配置 */
"sdkConfigs" : {
@@ -124,6 +130,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