“mitao”
2026-04-27 f77f419987dab0b86c3d405e0c93361ddab3dce2
fix:扫码分享页修改
7个文件已修改
278 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/vo/AppInviteBindTicketVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/static/app-download/index.html 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/service/AppShareService.java
@@ -142,8 +142,9 @@
        }
        AppInviteBindTicketVO result = new AppInviteBindTicketVO();
        result.setBindTicket(bindTicket);
        result.setInviteCode(bindingTarget.getInviteCode());
        result.setExpireAt(expireAt.format(DATE_TIME_FORMATTER));
        result.setOpenAppUrl(buildOpenAppUrl(bindTicket));
        result.setOpenAppUrl(buildOpenAppUrl(bindTicket, bindingTarget.getInviteCode()));
        return result;
    }
@@ -373,13 +374,13 @@
    /**
     * 构造安卓 App 打开注册页的深链地址。
     */
    private String buildOpenAppUrl(String bindTicket) {
    private String buildOpenAppUrl(String bindTicket, String inviteCode) {
        String schemeUrl = trimToNull(appShareProperties.getAndroidAppSchemeUrl());
        if (schemeUrl == null) {
            throw new IllegalArgumentException("App 打开地址暂未配置");
        }
        String queryJoiner = schemeUrl.contains("?") ? "&" : "?";
        return schemeUrl + queryJoiner + "bindTicket=" + bindTicket;
        return schemeUrl + queryJoiner + "bindTicket=" + bindTicket + "&inviteCode=" + inviteCode;
    }
    /**
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/share/vo/AppInviteBindTicketVO.java
@@ -12,6 +12,8 @@
    private String bindTicket;
    private String inviteCode;
    private String expireAt;
    private String openAppUrl;
tcm-training-admin-backend/src/main/resources/application.yml
@@ -115,7 +115,7 @@
      access-token-url: https://api.weixin.qq.com/sns/oauth2/access_token
    apple:
      # 苹果登录 clientId,需与 iOS 包内 Sign in with Apple 配置保持一致
      client-id: "com.haizhentong.app"
      client-id: "com.HaiZhenKangDao.hzt"
      # 苹果 identity token 固定签发方
      issuer: https://appleid.apple.com
      # 苹果 JWKS 公钥地址
tcm-training-admin-backend/src/main/resources/static/app-download/index.html
@@ -232,13 +232,11 @@
        <div class="invite-box">
            <div class="invite-label">邀请码</div>
            <div class="invite-code" id="inviteCodeValue">--</div>
            <div class="copy-tip">安装完成后请返回本页,点击“打开 App 继续注册”完成绑定。</div>
            <div class="copy-tip" id="flowTip">点击下方按钮,已安装将直接打开 APP;未安装会自动下载。</div>
        </div>
        <div class="btn-group">
            <button class="download-btn" id="androidDownloadBtn" type="button">下载 Android 版</button>
            <button class="download-btn secondary" id="iosDownloadBtn" type="button">下载 iOS 版</button>
            <button class="download-btn secondary" id="openAppBtn" type="button">打开 App 继续注册</button>
            <button class="download-btn" id="primaryActionBtn" type="button">打开 APP 继续注册</button>
            <button class="copy-btn" id="copyInviteBtn" type="button">复制邀请码</button>
        </div>
@@ -247,8 +245,8 @@
        <div class="section">
            <div class="section-title">安装说明</div>
            <ol class="notice">
                <li>点击对应平台下载按钮,下载安装 APP。</li>
                <li>安装完成后返回本页,点击“打开 App 继续注册”。</li>
                <li>点击“打开 APP 继续注册”,页面会优先尝试打开 APP。</li>
                <li>若尚未安装,将自动下载对应平台安装包;安装完成后返回本页会继续打开 APP。</li>
                <li>普通用户注册成功后,将自动关联邀请医生所属诊所。</li>
            </ol>
        </div>
@@ -266,8 +264,16 @@
        iosDownloadUrl: '',
        wechatBrowser: false,
        openAppBaseUrl: '',
        bindTicketLoading: false
        bindTicketLoading: false,
        openAppUrl: '',
        autoOpenTimer: 0,
        openingApp: false,
        pendingResumeOpen: false
    }
    const PENDING_OPEN_STORAGE_KEY = 'appInvitePendingOpen'
    const AUTO_DOWNLOAD_DELAY_MS = 1600
    const RESUME_AUTO_OPEN_DELAY_MS = 500
    /**
     * 页面初始化。
@@ -276,6 +282,7 @@
        bindActions()
        detectWechatEnvironment()
        bindWechatEnvironmentListeners()
        bindResumeAutoOpenListeners()
        const inviteCode = normalizeInviteCode(readQueryParam('inviteCode'))
        state.inviteCode = inviteCode
        renderInviteCode(inviteCode)
@@ -321,6 +328,7 @@
            document.getElementById('updateNoticeText').textContent = '当前已提供最新安装包下载。'
        }
        updateButtonState()
        scheduleResumeAutoOpen()
    }
    /**
@@ -330,6 +338,7 @@
        document.getElementById('clinicTitle').textContent = '欢迎下载中医培训 APP'
        document.getElementById('inviteCodeValue').textContent = inviteCode || '--'
        document.getElementById('updateNoticeText').textContent = '如需绑定邀请关系,请复制邀请码后在注册页填写。'
        document.getElementById('flowTip').textContent = '邀请信息加载失败时,可复制邀请码后在 APP 注册页手动填写。'
        updateButtonState()
    }
@@ -337,11 +346,8 @@
     * 绑定页面按钮事件。
     */
    function bindActions() {
        document.getElementById('androidDownloadBtn').addEventListener('click', function () {
            handleDownload(state.androidDownloadUrl, 'ANDROID')
        })
        document.getElementById('iosDownloadBtn').addEventListener('click', function () {
            handleDownload(state.iosDownloadUrl, 'IOS')
        document.getElementById('primaryActionBtn').addEventListener('click', function () {
            void handlePrimaryAction(false)
        })
        document.getElementById('copyInviteBtn').addEventListener('click', function () {
            if (!state.inviteCode) {
@@ -350,47 +356,54 @@
            }
            copyInviteCode(state.inviteCode, true)
        })
        document.getElementById('openAppBtn').addEventListener('click', function () {
            void handleOpenApp()
        })
        document.getElementById('wechatGuideClose').addEventListener('click', function () {
            hideWechatGuide()
        })
    }
    /**
     * 处理下载动作,仅负责下载安装包。
     * 处理主按钮动作,优先拉起 APP,未安装时自动下载。
     */
    function handleDownload(url, platform) {
    async function handlePrimaryAction(fromResume) {
        if (shouldBlockDirectDownload()) {
            showWechatGuide()
            showStatus('请点击右上角,选择在浏览器中打开后再下载')
            return
        }
        const normalizedUrl = normalizeOptionalText(url)
        if (!normalizedUrl) {
            showStatus(platform === 'ANDROID' ? 'Android 安装包暂未配置' : 'iOS 安装包暂未配置')
            return
        }
        window.location.href = normalizedUrl
    }
    /**
     * 生成绑定票据并通过深链打开 App 继续注册。
     */
    async function handleOpenApp() {
        if (state.bindTicketLoading) {
            showStatus('请点击右上角,选择在浏览器中打开后继续')
            return
        }
        if (!state.inviteCode) {
            showStatus('当前二维码未包含邀请码,请改用手动填写邀请码')
            showStatus('当前二维码未包含邀请码,请复制邀请码后手动注册')
            return
        }
        const openAppUrl = await ensureOpenAppUrl()
        if (!openAppUrl) {
            return
        }
        persistPendingOpen()
        tryOpenApp(openAppUrl)
        if (fromResume) {
            clearPendingOpen()
            showStatus('正在尝试打开 APP,如未打开请点击按钮重试')
            return
        }
        scheduleDownloadFallback()
    }
    /**
     * 确保已拿到可打开 APP 的深链地址。
     */
    async function ensureOpenAppUrl() {
        if (state.openAppUrl) {
            return state.openAppUrl
        }
        if (state.bindTicketLoading) {
            return ''
        }
        if (!state.openAppBaseUrl) {
            showStatus('App 打开地址暂未配置')
            return
            return ''
        }
        state.bindTicketLoading = true
        updateButtonState()
        try {
            const response = await fetch('/api/open/app-invite/bind-ticket', {
                method: 'POST',
@@ -404,14 +417,166 @@
            const payload = await response.json()
            if (!payload || payload.code !== 0 || !payload.data || !payload.data.openAppUrl) {
                showStatus(payload && (payload.msg || payload.message) ? payload.msg || payload.message : '绑定凭证生成失败,请稍后重试')
                return
                return ''
            }
            window.location.href = payload.data.openAppUrl
            showStatus('若未自动打开 App,请安装后返回本页再次点击')
            state.openAppUrl = normalizeOptionalText(payload.data.openAppUrl)
            return state.openAppUrl
        } catch (error) {
            showStatus('打开 App 失败,请安装后返回本页再次点击')
            showStatus('绑定凭证生成失败,请稍后重试')
            return ''
        } finally {
            state.bindTicketLoading = false
            updateButtonState()
        }
    }
    /**
     * 通过 Scheme 拉起 APP。
     */
    function tryOpenApp(openAppUrl) {
        state.openingApp = true
        showStatus('正在打开 APP,请稍候...')
        window.location.href = openAppUrl
        window.setTimeout(function () {
            state.openingApp = false
        }, AUTO_DOWNLOAD_DELAY_MS)
    }
    /**
     * 拉起失败时按当前平台下载 APP。
     */
    function scheduleDownloadFallback() {
        clearAutoOpenTimer()
        state.autoOpenTimer = window.setTimeout(function () {
            if (document.hidden) {
                return
            }
            const downloadUrl = resolveCurrentPlatformDownloadUrl()
            if (!downloadUrl) {
                showStatus('当前平台安装包暂未配置,请复制邀请码后联系工作人员')
                return
            }
            document.getElementById('flowTip').textContent = '安装完成后返回本页,页面会自动打开 APP 继续注册。'
            document.getElementById('primaryActionBtn').textContent = '安装完成后自动打开中...'
            showStatus('未检测到 APP,正在为您下载对应安装包')
            window.location.href = downloadUrl
        }, AUTO_DOWNLOAD_DELAY_MS)
    }
    /**
     * 根据浏览器平台选择安装包地址。
     */
    function resolveCurrentPlatformDownloadUrl() {
        if (isIosBrowser()) {
            return state.iosDownloadUrl
        }
        if (isAndroidBrowser()) {
            return state.androidDownloadUrl
        }
        return state.androidDownloadUrl || state.iosDownloadUrl
    }
    /**
     * 判断当前访问环境是否为 iOS 浏览器。
     */
    function isIosBrowser() {
        const userAgent = String(window.navigator.userAgent || '').toLowerCase()
        return /iphone|ipad|ipod/.test(userAgent)
    }
    /**
     * 判断当前访问环境是否为 Android 浏览器。
     */
    function isAndroidBrowser() {
        const userAgent = String(window.navigator.userAgent || '').toLowerCase()
        return userAgent.indexOf('android') >= 0 || userAgent.indexOf('adr') >= 0
    }
    /**
     * 记录安装后返回页面需要继续打开 APP。
     */
    function persistPendingOpen() {
        try {
            window.localStorage.setItem(PENDING_OPEN_STORAGE_KEY, JSON.stringify({
                inviteCode: state.inviteCode,
                openAppUrl: state.openAppUrl,
                createdAt: Date.now()
            }))
        } catch (error) {
        }
    }
    /**
     * 根据本地待继续记录恢复自动打开 APP。
     */
    function scheduleResumeAutoOpen() {
        const pendingOpen = readPendingOpen()
        if (!pendingOpen || pendingOpen.inviteCode !== state.inviteCode || state.pendingResumeOpen) {
            return
        }
        state.pendingResumeOpen = true
        if (pendingOpen.openAppUrl) {
            state.openAppUrl = pendingOpen.openAppUrl
        }
        window.setTimeout(function () {
            if (!document.hidden) {
                void handlePrimaryAction(true)
            }
            state.pendingResumeOpen = false
        }, RESUME_AUTO_OPEN_DELAY_MS)
    }
    /**
     * 绑定页面返回前台时的自动继续动作。
     */
    function bindResumeAutoOpenListeners() {
        window.addEventListener('pageshow', function () {
            scheduleResumeAutoOpen()
        })
        document.addEventListener('visibilitychange', function () {
            if (!document.hidden && !state.openingApp) {
                scheduleResumeAutoOpen()
            }
        })
    }
    /**
     * 读取待继续打开 APP 的本地记录。
     */
    function readPendingOpen() {
        try {
            const rawValue = window.localStorage.getItem(PENDING_OPEN_STORAGE_KEY)
            if (!rawValue) {
                return null
            }
            const parsedValue = JSON.parse(rawValue)
            if (!parsedValue || Date.now() - Number(parsedValue.createdAt || 0) > 24 * 60 * 60 * 1000) {
                window.localStorage.removeItem(PENDING_OPEN_STORAGE_KEY)
                return null
            }
            return parsedValue
        } catch (error) {
            return null
        }
    }
    /**
     * 清理已经消费的待继续打开记录。
     */
    function clearPendingOpen() {
        try {
            window.localStorage.removeItem(PENDING_OPEN_STORAGE_KEY)
        } catch (error) {
        }
    }
    /**
     * 清理当前延迟打开或下载任务。
     */
    function clearAutoOpenTimer() {
        if (state.autoOpenTimer) {
            window.clearTimeout(state.autoOpenTimer)
            state.autoOpenTimer = 0
        }
    }
@@ -419,8 +584,9 @@
     * 根据当前接口数据刷新下载按钮状态。
     */
    function updateButtonState() {
        document.getElementById('androidDownloadBtn').disabled = !state.androidDownloadUrl
        document.getElementById('iosDownloadBtn').disabled = !state.iosDownloadUrl
        const primaryActionBtn = document.getElementById('primaryActionBtn')
        primaryActionBtn.disabled = state.bindTicketLoading || (!state.androidDownloadUrl && !state.iosDownloadUrl)
        primaryActionBtn.textContent = state.bindTicketLoading ? '正在准备注册信息...' : '打开 APP 继续注册'
    }
    /**
traditionalApp/App.vue
@@ -3,7 +3,7 @@
        onLaunch,
        onShow
    } from '@dcloudio/uni-app'
    import { extractBindTicket } from '@/utils/utils.js'
    import { extractBindTicket, extractInviteCode } from '@/utils/utils.js'
    let lastHandledBindTicket = ''
@@ -25,14 +25,20 @@
     */
    function handleOpenByScheme() {
        // #ifdef APP-PLUS
        const bindTicket = extractBindTicket(typeof plus !== 'undefined' && plus.runtime ? plus.runtime.arguments : '')
        const runtimeArguments = typeof plus !== 'undefined' && plus.runtime ? plus.runtime.arguments : ''
        const bindTicket = extractBindTicket(runtimeArguments)
        if (!bindTicket || bindTicket === lastHandledBindTicket) {
            return
        }
        lastHandledBindTicket = bindTicket
        const inviteCode = extractInviteCode(runtimeArguments)
        const currentPages = getCurrentPages()
        const currentRoute = currentPages && currentPages.length > 0 ? currentPages[currentPages.length - 1].route : ''
        const targetUrl = `/pages/register/register?bindTicket=${encodeURIComponent(bindTicket)}`
        const targetParams = [`bindTicket=${encodeURIComponent(bindTicket)}`]
        if (inviteCode) {
            targetParams.push(`inviteCode=${encodeURIComponent(inviteCode)}`)
        }
        const targetUrl = `/pages/register/register?${targetParams.join('&')}`
        if (currentRoute === 'pages/register/register') {
            uni.redirectTo({
                url: targetUrl
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/manifest.json
@@ -55,6 +55,9 @@
            },
            /* ios打包配置 */
            "ios": {
                "schemes" : [
                    "haizhentong"
                ],
                "dSYMs": false,
                "capabilities": {
                    "entitlements": {