“mitao”
2026-04-30 0c98579fff979ae80fc30224b87c9c6eb626f49e
feat:openinstall落地页接入websdk
1个文件已添加
12个文件已修改
329 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/config/AppShareProperties.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/application.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/static/app-download/index.html 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/clearCache/clearCache.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/register/register.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/invite-cache.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 {
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() {
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;
    }
    /**
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
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")
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;
    }
    /**
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
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 继续注册'
    }
    /**
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
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/'
};
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') {
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'
traditionalApp/utils/invite-cache.js
New file
@@ -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