From 0c98579fff979ae80fc30224b87c9c6eb626f49e Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 30 四月 2026 17:29:37 +0800
Subject: [PATCH] feat:openinstall落地页接入websdk

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java        |   14 ++
 tcm-training-admin-backend/src/main/resources/application.yml                                                    |    6 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java           |   15 -
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java    |  104 +++++++++++++----
 traditionalApp/App.vue                                                                                           |    4 
 traditionalApp/pages/clearCache/clearCache.vue                                                                   |    6 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java                     |   16 ++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java         |   19 +++
 traditionalApp/config/index.js                                                                                   |    4 
 tcm-training-admin-backend/src/main/resources/static/app-download/index.html                                     |   90 ++++++++++++++-
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java |    8 +
 traditionalApp/pages/register/register.vue                                                                       |    9 +
 traditionalApp/utils/invite-cache.js                                                                             |   34 +++++
 13 files changed, 280 insertions(+), 49 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
index c8de56a..a9913eb 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
@@ -4,9 +4,11 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.tcm.training.admin.app.auth.config.AppWechatAuthProperties;
 import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest;
+import java.nio.charset.StandardCharsets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.converter.StringHttpMessageConverter;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
@@ -29,7 +31,7 @@
      */
     @Autowired
     public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) {
-        this(properties, objectMapper, new RestTemplate());
+        this(properties, objectMapper, createUtf8RestTemplate());
     }
 
     /**
@@ -200,6 +202,16 @@
     }
 
     /**
+     * 创建默认按 UTF-8 解码字符串响应的 HTTP 客户端,避免微信中文昵称乱码。
+     */
+    private static RestTemplate createUtf8RestTemplate() {
+        RestTemplate restTemplate = new RestTemplate();
+        restTemplate.getMessageConverters().removeIf(StringHttpMessageConverter.class::isInstance);
+        restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
+        return restTemplate;
+    }
+
+    /**
      * 保存微信换票结果,避免在多个私有方法之间散落参数。
      */
     private static class WechatAccessTokenSnapshot {
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java
index 925572c..8657513 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java
@@ -16,6 +16,11 @@
     private String landingBaseUrl = "https://haizhentong.com/app-download/";
 
     /**
+     * 二维码图片公开接口基础地址,需包含协议与域名。
+     */
+    private String qrcodeImageBaseUrl = "https://haizhentong.com/api/open/app-share/qrcode-image";
+
+    /**
      * 安卓 App 自定义 Scheme 基础地址。
      */
     private String androidAppSchemeUrl = "haizhentong://register";
@@ -35,6 +40,20 @@
     }
 
     /**
+     * 获取二维码图片公开接口基础地址。
+     */
+    public String getQrcodeImageBaseUrl() {
+        return qrcodeImageBaseUrl;
+    }
+
+    /**
+     * 设置二维码图片公开接口基础地址。
+     */
+    public void setQrcodeImageBaseUrl(String qrcodeImageBaseUrl) {
+        this.qrcodeImageBaseUrl = qrcodeImageBaseUrl;
+    }
+
+    /**
      * 获取安卓 App 自定义 Scheme 基础地址。
      */
     public String getAndroidAppSchemeUrl() {
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java
index 7386587..6a0ae31 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java
@@ -301,21 +301,16 @@
      * 构建二维码图片公开访问地址。
      */
     private String buildQrCodeImageUrl(String inviteCode) {
-        String landingBaseUrl = trimToNull(appShareProperties.getLandingBaseUrl());
-        if (landingBaseUrl == null) {
-            throw new IllegalArgumentException("邀请下载页地址暂未配置");
+        String qrcodeImageBaseUrl = trimToNull(appShareProperties.getQrcodeImageBaseUrl());
+        if (qrcodeImageBaseUrl == null) {
+            throw new IllegalArgumentException("分享二维码图片地址暂未配置");
         }
-        int schemeIndex = landingBaseUrl.indexOf("://");
-        if (schemeIndex < 0) {
-            throw new IllegalArgumentException("邀请下载页地址格式不正确");
-        }
-        int hostEndIndex = landingBaseUrl.indexOf('/', schemeIndex + 3);
-        String origin = hostEndIndex >= 0 ? landingBaseUrl.substring(0, hostEndIndex) : landingBaseUrl;
         String normalizedInviteCode = trimToNull(inviteCode);
         if (!StringUtils.hasText(normalizedInviteCode)) {
             throw new IllegalArgumentException("邀请码不能为空");
         }
-        return origin + "/api/open/app-share/qrcode-image?inviteCode=" + normalizedInviteCode;
+        String queryJoiner = qrcodeImageBaseUrl.contains("?") ? "&" : "?";
+        return qrcodeImageBaseUrl + queryJoiner + "inviteCode=" + normalizedInviteCode;
     }
 
     /**
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 9bca475..927f494 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
@@ -6,22 +6,27 @@
 import com.tcm.training.admin.common.xss.XssParamInterceptor;
 import com.tcm.training.admin.upload.LocalUploadProperties;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 import java.io.File;
+import java.util.Arrays;
 
 @Configuration
 public class SaTokenWebConfig implements WebMvcConfigurer {
 
     private final XssParamInterceptor xssParamInterceptor;
     private final LocalUploadProperties localUploadProperties;
+    private final Environment environment;
 
     public SaTokenWebConfig(XssParamInterceptor xssParamInterceptor,
-                            LocalUploadProperties localUploadProperties) {
+                            LocalUploadProperties localUploadProperties,
+                            Environment environment) {
         this.xssParamInterceptor = xssParamInterceptor;
         this.localUploadProperties = localUploadProperties;
+        this.environment = environment;
     }
 
     /**
@@ -70,7 +75,7 @@
     private boolean isPublicAdminApi(String requestPath) {
         return "/api/auth/login".equals(requestPath)
                 || "/api/auth/captcha".equals(requestPath)
-                || isJpushManualPushApi(requestPath)
+                || (!isProdProfile() && isJpushManualPushApi(requestPath))
                 || requestPath.startsWith("/api/open/")
                 || requestPath.startsWith("/api/v1/open/");
     }
@@ -86,6 +91,13 @@
     }
 
     /**
+     * 判断当前是否为生产环境,生产环境手动推送接口必须校验后台登录态。
+     */
+    private boolean isProdProfile() {
+        return Arrays.asList(environment.getActiveProfiles()).contains("prod");
+    }
+
+    /**
      * 将本地上传目录映射为静态资源,可通过 /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
index 6d1e468..2c90ed7 100644
--- 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
@@ -67,6 +67,14 @@
     }
 
     /**
+     * 生成礼品发货测试通知并推送。
+     */
+    @PostMapping("/jpush/manual-push/order-shipped")
+    public ApiResponse<JpushManualPushVO> pushOrderShipped(@Valid @RequestBody JpushManualPushRequest request) {
+        return ApiResponse.ok(jpushManualPushAppService.pushOrderShipped(request));
+    }
+
+    /**
      * 生成课程更新测试通知并推送。
      */
     @PostMapping("/jpush/manual-push/course-update-notice")
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
index c549b1d..646f802 100644
--- 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
@@ -9,6 +9,8 @@
 import com.tcm.training.admin.utils.JGPushUtil;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -21,7 +23,6 @@
     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;
@@ -58,55 +59,116 @@
      * 生成会员到期提醒测试通知并尝试极光推送。
      */
     public JpushManualPushVO pushMemberExpireRemind(JpushManualPushRequest request) {
+        LocalDate expireDate = LocalDate.now().plusDays(3L);
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("expireAt", expireDate.atTime(23, 59, 59).toString());
+        payload.put("remainingDays", "3");
         return pushNoticeType(request,
                 AppSystemNoticeType.MEMBER_EXPIRE_REMIND,
                 "会员到期提醒",
-                "您的会员有效期将于测试日期到期",
-                false);
+                "您的会员有效期将于" + expireDate + "到期",
+                "MEMBER_EXPIRE_" + expireDate,
+                request.getAppUserId(),
+                false,
+                payload);
     }
 
     /**
      * 生成直播提醒测试通知并尝试极光推送。
      */
     public JpushManualPushVO pushLiveRemind(JpushManualPushRequest request) {
+        Long sessionId = System.currentTimeMillis();
+        LocalDateTime startTime = LocalDateTime.now().plusHours(1L);
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("courseId", "100001");
+        payload.put("sessionId", String.valueOf(sessionId));
+        payload.put("startTime", String.valueOf(startTime));
+        payload.put("courseTitle", "测试直播课程");
         return pushNoticeType(request,
                 AppSystemNoticeType.LIVE_REMIND,
                 "直播提醒",
                 "测试直播课程 即将开播",
-                true);
+                "LIVE_SESSION",
+                sessionId,
+                true,
+                payload);
     }
 
     /**
      * 生成活动通知测试通知并尝试极光推送。
      */
     public JpushManualPushVO pushActivityNotice(JpushManualPushRequest request) {
+        Long activityNoticeId = System.currentTimeMillis();
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("activityNoticeId", String.valueOf(activityNoticeId));
+        payload.put("title", "活动通知");
         return pushNoticeType(request,
                 AppSystemNoticeType.ACTIVITY_NOTICE,
                 "活动通知",
-                "这是一条活动通知联调测试消息",
-                true);
+                "活动通知",
+                "ACTIVITY_NOTICE",
+                activityNoticeId,
+                true,
+                payload);
     }
 
     /**
-     * 生成订单状态更新测试通知并尝试极光推送。
+     * 生成礼品订单创建测试通知并尝试极光推送。
      */
     public JpushManualPushVO pushOrderStatusUpdate(JpushManualPushRequest request) {
+        Long orderId = System.currentTimeMillis();
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("orderId", String.valueOf(orderId));
+        payload.put("orderNo", "TEST" + orderId);
+        payload.put("giftName", "测试礼品");
+        payload.put("itemCount", "1");
         return pushNoticeType(request,
                 AppSystemNoticeType.ORDER_STATUS_UPDATE,
                 "订单状态更新",
-                "您的礼品兑换订单状态已更新",
-                true);
+                "您有新的礼品兑换订单待处理",
+                "POINTS_EXCHANGE_ORDER",
+                orderId,
+                true,
+                payload);
+    }
+
+    /**
+     * 生成礼品发货测试通知并尝试极光推送。
+     */
+    public JpushManualPushVO pushOrderShipped(JpushManualPushRequest request) {
+        Long orderId = System.currentTimeMillis();
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("orderId", String.valueOf(orderId));
+        payload.put("orderNo", "TEST" + orderId);
+        payload.put("trackingNo", "SF" + orderId);
+        return pushNoticeType(request,
+                AppSystemNoticeType.ORDER_STATUS_UPDATE,
+                "订单状态更新",
+                "您的礼品兑换订单已发货",
+                "POINTS_EXCHANGE_ORDER_SHIPPED",
+                orderId,
+                true,
+                payload);
     }
 
     /**
      * 生成课程更新测试通知并尝试极光推送。
      */
     public JpushManualPushVO pushCourseUpdateNotice(JpushManualPushRequest request) {
+        Long videoId = System.currentTimeMillis();
+        Map<String, String> payload = new LinkedHashMap<String, String>();
+        payload.put("courseId", "100001");
+        payload.put("courseTitle", "测试课程");
+        payload.put("videoId", String.valueOf(videoId));
+        payload.put("chapterName", "测试章节");
         return pushNoticeType(request,
                 AppSystemNoticeType.COURSE_UPDATE_NOTICE,
                 "课程更新通知",
                 "测试课程 已更新新章节",
-                true);
+                "HEALTH_COURSE_VIDEO",
+                videoId,
+                true,
+                payload);
     }
 
     /**
@@ -116,31 +178,21 @@
                                              String noticeTypeCode,
                                              String title,
                                              String content,
-                                             boolean requireUserSetting) {
-        Long referenceId = System.currentTimeMillis();
+                                             String referenceType,
+                                             Long referenceId,
+                                             boolean requireUserSetting,
+                                             Map<String, String> payload) {
         AppSystemNoticeDispatchCommand command = new AppSystemNoticeDispatchCommand();
         command.setAppUserId(request.getAppUserId());
         command.setNoticeTypeCode(noticeTypeCode);
         command.setTitle(title);
         command.setContent(content);
-        command.setReferenceType("MANUAL_TEST_" + noticeTypeCode);
+        command.setReferenceType(referenceType);
         command.setReferenceId(referenceId);
         command.setRequireUserSetting(requireUserSetting);
-        command.setPayload(buildManualNoticePayload(request.getAppUserId(), noticeTypeCode, referenceId));
+        command.setPayload(payload);
         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;
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/main/resources/application.yml b/tcm-training-admin-backend/src/main/resources/application.yml
index 29724ec..1fbb6f2 100644
--- a/tcm-training-admin-backend/src/main/resources/application.yml
+++ b/tcm-training-admin-backend/src/main/resources/application.yml
@@ -121,8 +121,10 @@
       # 苹果 JWKS 公钥地址
       jwks-url: https://appleid.apple.com/auth/keys
   share:
-    # 邀请二维码默认指向线上下载页,开发环境可在 application-local.yml 覆盖。
-    landing-base-url: https://ag8spw.oplinking.com/
+    # 邀请二维码默认指向 openinstall 下载页,开发环境可在 application-local.yml 覆盖。
+    landing-base-url: https://haizhentong.com/app-download/
+    # 二维码图片仍由后端公开接口生成,不能使用 openinstall 域名拼接。
+    qrcode-image-base-url: https://haizhentong.com/api/open/app-share/qrcode-image
 jpush:
   appKey: b5aed7d076638a0d835786bd
   masterSecret: e17305459be4f0011ca15c9e
diff --git a/tcm-training-admin-backend/src/main/resources/static/app-download/index.html b/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
index 678d1fa..3bc0852 100644
--- a/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
+++ b/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
@@ -245,8 +245,8 @@
         <div class="section">
             <div class="section-title">安装说明</div>
             <ol class="notice">
-                <li>点击“打开 APP 继续注册”,页面会优先尝试打开 APP。</li>
-                <li>若尚未安装,将自动下载对应平台安装包;安装完成后返回本页会继续打开 APP。</li>
+                <li>点击“打开 APP 继续注册”,页面会通过 openinstall 自动打开或引导安装 APP。</li>
+                <li>若尚未安装,安装后首次打开 APP 会自动带上当前邀请码。</li>
                 <li>普通用户注册成功后,将自动关联邀请医生所属诊所。</li>
             </ol>
         </div>
@@ -257,6 +257,7 @@
         </div>
     </div>
 </div>
+<script src="https://res.opstatistics.com/openinstall-ag8spw.js"></script>
 <script>
     const state = {
         inviteCode: '',
@@ -270,7 +271,10 @@
         resumeWatchTimer: 0,
         openConfirmTimer: 0,
         openingApp: false,
-        pendingResumeOpen: false
+        pendingResumeOpen: false,
+        openinstallInstance: null,
+        openinstallReady: false,
+        openinstallInitializing: false
     }
 
     const PENDING_OPEN_STORAGE_KEY = 'appInvitePendingOpen'
@@ -293,6 +297,7 @@
         state.inviteCode = inviteCode
         renderInviteCode(inviteCode)
         await loadInviteLanding(inviteCode)
+        initOpeninstall()
     }
 
     /**
@@ -380,6 +385,9 @@
             showStatus('当前二维码未包含邀请码,请复制邀请码后手动注册')
             return
         }
+        if (tryOpeninstallWakeupOrInstall()) {
+            return
+        }
         const openAppUrl = await ensureOpenAppUrl()
         if (!openAppUrl) {
             return
@@ -394,6 +402,74 @@
             return
         }
         scheduleDownloadFallback()
+    }
+
+    /**
+     * 初始化 openinstall Web SDK,并把邀请码作为安装和唤醒参数传递。
+     */
+    function initOpeninstall() {
+        if (!state.inviteCode || state.openinstallInstance || state.openinstallInitializing) {
+            return
+        }
+        if (typeof OpenInstall === 'undefined') {
+            showStatus('openinstall 组件加载失败,已启用普通下载方式')
+            return
+        }
+        state.openinstallInitializing = true
+        const wakeupData = buildOpeninstallWakeupData()
+        try {
+            state.openinstallInstance = new OpenInstall({
+                appKey: 'ag8spw',
+                fastInstall: false,
+                onready: function () {
+                    state.openinstallReady = true
+                    state.openinstallInitializing = false
+                    updateButtonState()
+                    this.schemeWakeup()
+                }
+            }, wakeupData)
+        } catch (error) {
+            state.openinstallInstance = null
+            state.openinstallReady = false
+            state.openinstallInitializing = false
+            showStatus('openinstall 初始化失败,已启用普通下载方式')
+        }
+    }
+
+    /**
+     * 使用 openinstall 打开或安装 APP,失败时由旧逻辑兜底。
+     */
+    function tryOpeninstallWakeupOrInstall() {
+        if (!state.openinstallInstance || !state.openinstallReady) {
+            initOpeninstall()
+            return false
+        }
+        try {
+            persistPendingOpen({
+                downloadStartedAt: Date.now(),
+                resumeAttempted: true
+            })
+            document.getElementById('flowTip').textContent = '正在通过 openinstall 打开或安装 APP,并保留当前邀请码。'
+            showStatus('正在打开或安装 APP,请稍候...')
+            state.openinstallInstance.wakeupOrInstall()
+            scheduleOpenFallbackConfirm()
+            return true
+        } catch (error) {
+            showStatus('openinstall 调用失败,已启用普通下载方式')
+            return false
+        }
+    }
+
+    /**
+     * 构造 openinstall 传参对象,字段需与 APP 端解析逻辑保持一致。
+     */
+    function buildOpeninstallWakeupData() {
+        const parsedParams = typeof OpenInstall !== 'undefined' && OpenInstall.parseUrlParams
+            ? OpenInstall.parseUrlParams()
+            : {}
+        return Object.assign({}, parsedParams || {}, {
+            inviteCode: state.inviteCode
+        })
     }
 
     /**
@@ -689,14 +765,16 @@
      */
     function updateButtonState() {
         const primaryActionBtn = document.getElementById('primaryActionBtn')
-        primaryActionBtn.disabled = state.bindTicketLoading || (!state.androidDownloadUrl && !state.iosDownloadUrl)
+        primaryActionBtn.disabled = state.bindTicketLoading || state.openinstallInitializing || (!state.openinstallReady && !state.androidDownloadUrl && !state.iosDownloadUrl)
         const pendingOpen = readPendingOpen()
         if (pendingOpen && pendingOpen.inviteCode === state.inviteCode && pendingOpen.resumeAttempted) {
             primaryActionBtn.textContent = '已安装,点此打开 APP'
             return
         }
-        document.getElementById('flowTip').textContent = '点击下方按钮,已安装将直接打开 APP;未安装会自动下载。'
-        primaryActionBtn.textContent = state.bindTicketLoading ? '正在准备注册信息...' : '打开 APP 继续注册'
+        document.getElementById('flowTip').textContent = state.openinstallReady
+            ? '点击下方按钮,openinstall 将自动打开或安装 APP,并保留邀请码。'
+            : '点击下方按钮,已安装将直接打开 APP;未安装会自动下载。'
+        primaryActionBtn.textContent = state.bindTicketLoading || state.openinstallInitializing ? '正在准备注册信息...' : '打开 APP 继续注册'
     }
 
     /**
diff --git a/traditionalApp/App.vue b/traditionalApp/App.vue
index 782cb6e..9baa077 100644
--- a/traditionalApp/App.vue
+++ b/traditionalApp/App.vue
@@ -7,6 +7,7 @@
 		extractBindTicket,
 		extractInviteCode
 	} from '@/utils/utils.js'
+	import { cacheInviteCode } from '@/utils/invite-cache.js'
 	import { initJPushLifecycle } from '@/utils/push.js'
 	import {
 		getOpeninstallInstallData,
@@ -61,6 +62,9 @@
 		if (!bindTicket && !inviteCode) {
 			return
 		}
+		if (inviteCode) {
+			cacheInviteCode(inviteCode)
+		}
 		const payloadKey = `${bindTicket || ''}_${inviteCode || ''}`
 		if (payloadKey === lastHandledInvitePayload) {
 			return
diff --git a/traditionalApp/config/index.js b/traditionalApp/config/index.js
index e889a40..0ba8136 100644
--- a/traditionalApp/config/index.js
+++ b/traditionalApp/config/index.js
@@ -1,6 +1,6 @@
 export default {
-	// BASE_URL: 'http://192.168.110.188:48080',
-	BASE_URL: 'https://haizhentong.com',
+	BASE_URL: 'http://192.168.110.188:48080',
+	// BASE_URL: 'https://haizhentong.com',
 	uploadUrl: "https://haizhentong.com/api/open/file/upload",
 	imageUrl: 'http://221.182.45.100:11224/images/'
 };
\ No newline at end of file
diff --git a/traditionalApp/pages/clearCache/clearCache.vue b/traditionalApp/pages/clearCache/clearCache.vue
index 67cd744..b7e05af 100644
--- a/traditionalApp/pages/clearCache/clearCache.vue
+++ b/traditionalApp/pages/clearCache/clearCache.vue
@@ -27,6 +27,7 @@
 
 <script setup lang="ts">
 import { onMounted, ref } from 'vue'
+import { getInviteCodeCacheKey } from '@/utils/invite-cache.js'
 
 const statusBarHeight = ref(20)
 const cacheSize = ref('0M')
@@ -100,6 +101,8 @@
 		const appUserInfo = uni.getStorageSync('appUserInfo')
 		const loginRedirectMeta = uni.getStorageSync('loginRedirectMeta')
 		const showLogoutModal = uni.getStorageSync('showLogoutModal')
+		const inviteCodeCacheKey = getInviteCodeCacheKey()
+		const cachedInviteCode = uni.getStorageSync(inviteCodeCacheKey)
 		uni.clearStorageSync()
 		if (token) {
 			uni.setStorageSync('token', token)
@@ -113,6 +116,9 @@
 		if (showLogoutModal !== '') {
 			uni.setStorageSync('showLogoutModal', showLogoutModal)
 		}
+		if (cachedInviteCode) {
+			uni.setStorageSync(inviteCodeCacheKey, cachedInviteCode)
+		}
 		// #ifdef APP-PLUS
 		const plusRef = typeof plus !== 'undefined' ? plus : null
 		if (plusRef && plusRef.cache && typeof plusRef.cache.clear === 'function') {
diff --git a/traditionalApp/pages/register/register.vue b/traditionalApp/pages/register/register.vue
index 0d7abf3..1934fa5 100644
--- a/traditionalApp/pages/register/register.vue
+++ b/traditionalApp/pages/register/register.vue
@@ -177,6 +177,7 @@
 } from './service.js'
 import { extractBindTicket, extractInviteCode, normalizeOptionalText } from '@/utils/utils.js'
 import { reportOpeninstallRegister } from '@/utils/openinstall.js'
+import { cacheInviteCode, clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
 import { confirmCameraForInviteCodeScan, confirmMediaForLicense } from '@/utils/permission.js'
 
 interface RegionItem {
@@ -360,6 +361,7 @@
 				return
 			}
 			form.inviteCode = inviteCode
+			cacheInviteCode(inviteCode)
 		}
 	})
 }
@@ -626,6 +628,12 @@
 	const routeCode = normalizeInviteCode(routeInviteCode)
 	if (routeCode) {
 		form.inviteCode = routeCode
+		cacheInviteCode(routeCode)
+		return
+	}
+	const cachedInviteCode = getCachedInviteCode()
+	if (cachedInviteCode) {
+		form.inviteCode = cachedInviteCode
 	}
 }
 
@@ -819,6 +827,7 @@
 const handleRegisterSuccess = async (message: string) => {
 	// 注册成功后通知 openinstall,用于统计推广链路的注册转化。
 	reportOpeninstallRegister()
+	clearCachedInviteCode()
 	uni.showToast({
 		title: message,
 		icon: 'success'
diff --git a/traditionalApp/utils/invite-cache.js b/traditionalApp/utils/invite-cache.js
new file mode 100644
index 0000000..e72e208
--- /dev/null
+++ b/traditionalApp/utils/invite-cache.js
@@ -0,0 +1,34 @@
+import { extractInviteCode } from './utils.js'
+
+const INVITE_CODE_CACHE_KEY = 'openinstallInviteCode'
+
+/**
+ * 缓存 openinstall 或扫码解析出的医生邀请码,支持稍后注册时继续绑定关系。
+ */
+export const cacheInviteCode = (value) => {
+	const inviteCode = extractInviteCode(value)
+	if (!inviteCode) {
+		return ''
+	}
+	uni.setStorageSync(INVITE_CODE_CACHE_KEY, inviteCode)
+	return inviteCode
+}
+
+/**
+ * 读取本地缓存的邀请码,只返回合法的 6 位业务码。
+ */
+export const getCachedInviteCode = () => {
+	return extractInviteCode(uni.getStorageSync(INVITE_CODE_CACHE_KEY))
+}
+
+/**
+ * 清理已经完成注册使用的邀请码缓存,避免后续账号误绑定旧关系。
+ */
+export const clearCachedInviteCode = () => {
+	uni.removeStorageSync(INVITE_CODE_CACHE_KEY)
+}
+
+/**
+ * 暴露缓存 key 给清缓存页面保留业务必要数据。
+ */
+export const getInviteCodeCacheKey = () => INVITE_CODE_CACHE_KEY

--
Gitblit v1.7.1