feat:openinstall落地页接入websdk
| | |
| | | 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; |
| | |
| | | */ |
| | | @Autowired |
| | | public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) { |
| | | this(properties, objectMapper, new RestTemplate()); |
| | | this(properties, objectMapper, createUtf8RestTemplate()); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 创建默认按 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 { |
| | |
| | | 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"; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 获取二维码图片公开接口基础地址。 |
| | | */ |
| | | public String getQrcodeImageBaseUrl() { |
| | | return qrcodeImageBaseUrl; |
| | | } |
| | | |
| | | /** |
| | | * 设置二维码图片公开接口基础地址。 |
| | | */ |
| | | public void setQrcodeImageBaseUrl(String qrcodeImageBaseUrl) { |
| | | this.qrcodeImageBaseUrl = qrcodeImageBaseUrl; |
| | | } |
| | | |
| | | /** |
| | | * 获取安卓 App 自定义 Scheme 基础地址。 |
| | | */ |
| | | public String getAndroidAppSchemeUrl() { |
| | |
| | | * 构建二维码图片公开访问地址。 |
| | | */ |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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/"); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 判断当前是否为生产环境,生产环境手动推送接口必须校验后台登录态。 |
| | | */ |
| | | private boolean isProdProfile() { |
| | | return Arrays.asList(environment.getActiveProfiles()).contains("prod"); |
| | | } |
| | | |
| | | /** |
| | | * 将本地上传目录映射为静态资源,可通过 /uploads/** 访问。 |
| | | */ |
| | | @Override |
| | |
| | | } |
| | | |
| | | /** |
| | | * 生成礼品发货测试通知并推送。 |
| | | */ |
| | | @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") |
| | |
| | | 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; |
| | | |
| | |
| | | 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; |
| | |
| | | * 生成会员到期提醒测试通知并尝试极光推送。 |
| | | */ |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | # 苹果 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 |
| | |
| | | <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> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script src="https://res.opstatistics.com/openinstall-ag8spw.js"></script> |
| | | <script> |
| | | const state = { |
| | | inviteCode: '', |
| | |
| | | resumeWatchTimer: 0, |
| | | openConfirmTimer: 0, |
| | | openingApp: false, |
| | | pendingResumeOpen: false |
| | | pendingResumeOpen: false, |
| | | openinstallInstance: null, |
| | | openinstallReady: false, |
| | | openinstallInitializing: false |
| | | } |
| | | |
| | | const PENDING_OPEN_STORAGE_KEY = 'appInvitePendingOpen' |
| | |
| | | state.inviteCode = inviteCode |
| | | renderInviteCode(inviteCode) |
| | | await loadInviteLanding(inviteCode) |
| | | initOpeninstall() |
| | | } |
| | | |
| | | /** |
| | |
| | | showStatus('当前二维码未包含邀请码,请复制邀请码后手动注册') |
| | | return |
| | | } |
| | | if (tryOpeninstallWakeupOrInstall()) { |
| | | return |
| | | } |
| | | const openAppUrl = await ensureOpenAppUrl() |
| | | if (!openAppUrl) { |
| | | return |
| | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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 继续注册' |
| | | } |
| | | |
| | | /** |
| | |
| | | extractBindTicket, |
| | | extractInviteCode |
| | | } from '@/utils/utils.js' |
| | | import { cacheInviteCode } from '@/utils/invite-cache.js' |
| | | import { initJPushLifecycle } from '@/utils/push.js' |
| | | import { |
| | | getOpeninstallInstallData, |
| | |
| | | if (!bindTicket && !inviteCode) { |
| | | return |
| | | } |
| | | if (inviteCode) { |
| | | cacheInviteCode(inviteCode) |
| | | } |
| | | const payloadKey = `${bindTicket || ''}_${inviteCode || ''}` |
| | | if (payloadKey === lastHandledInvitePayload) { |
| | | return |
| | |
| | | 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/' |
| | | }; |
| | |
| | | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue' |
| | | import { getInviteCodeCacheKey } from '@/utils/invite-cache.js' |
| | | |
| | | const statusBarHeight = ref(20) |
| | | const cacheSize = ref('0M') |
| | |
| | | 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) |
| | |
| | | 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') { |
| | |
| | | } 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 { |
| | |
| | | return |
| | | } |
| | | form.inviteCode = inviteCode |
| | | cacheInviteCode(inviteCode) |
| | | } |
| | | }) |
| | | } |
| | |
| | | const routeCode = normalizeInviteCode(routeInviteCode) |
| | | if (routeCode) { |
| | | form.inviteCode = routeCode |
| | | cacheInviteCode(routeCode) |
| | | return |
| | | } |
| | | const cachedInviteCode = getCachedInviteCode() |
| | | if (cachedInviteCode) { |
| | | form.inviteCode = cachedInviteCode |
| | | } |
| | | } |
| | | |
| | |
| | | const handleRegisterSuccess = async (message: string) => { |
| | | // 注册成功后通知 openinstall,用于统计推广链路的注册转化。 |
| | | reportOpeninstallRegister() |
| | | clearCachedInviteCode() |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: 'success' |
| New file |
| | |
| | | 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 |