13404089107
2026-04-30 ad93fcac51bebf9ff6204e3cd2c3cba5cc5d6a6f
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/tcm-training
5个文件已添加
7个文件已修改
589 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/application.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/register/register.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/openinstall.js 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/utils.js 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
@@ -70,11 +70,22 @@
    private boolean isPublicAdminApi(String requestPath) {
        return "/api/auth/login".equals(requestPath)
                || "/api/auth/captcha".equals(requestPath)
                || isJpushManualPushApi(requestPath)
                || requestPath.startsWith("/api/open/")
                || requestPath.startsWith("/api/v1/open/");
    }
    /**
     * 判断当前后台路由是否属于极光手动推送联调接口。
     */
    private boolean isJpushManualPushApi(String requestPath) {
        return "/api/jpush/manual-push".equals(requestPath)
                || "/api/v1/jpush/manual-push".equals(requestPath)
                || requestPath.startsWith("/api/jpush/manual-push/")
                || requestPath.startsWith("/api/v1/jpush/manual-push/");
    }
    /**
     * 将本地上传目录映射为静态资源,可通过 /uploads/** 访问。
     */
    @Override
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/JpushManualPushController.java
New file
@@ -0,0 +1,76 @@
package com.tcm.training.admin.system.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.system.dto.JpushManualPushRequest;
import com.tcm.training.admin.system.service.JpushManualPushAppService;
import com.tcm.training.admin.system.vo.JpushManualPushVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
 * 极光手动推送联调接口。
 */
@Validated
@RestController
@RequestMapping({"/api/v1", "/api"})
public class JpushManualPushController {
    private final JpushManualPushAppService jpushManualPushAppService;
    public JpushManualPushController(JpushManualPushAppService jpushManualPushAppService) {
        this.jpushManualPushAppService = jpushManualPushAppService;
    }
    /**
     * 按APP用户ID推送固定联调消息。
     */
    @PostMapping("/jpush/manual-push")
    public ApiResponse<JpushManualPushVO> push(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.push(request));
    }
    /**
     * 生成会员到期提醒测试通知并推送。
     */
    @PostMapping("/jpush/manual-push/member-expire-remind")
    public ApiResponse<JpushManualPushVO> pushMemberExpireRemind(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.pushMemberExpireRemind(request));
    }
    /**
     * 生成直播提醒测试通知并推送。
     */
    @PostMapping("/jpush/manual-push/live-remind")
    public ApiResponse<JpushManualPushVO> pushLiveRemind(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.pushLiveRemind(request));
    }
    /**
     * 生成活动通知测试通知并推送。
     */
    @PostMapping("/jpush/manual-push/activity-notice")
    public ApiResponse<JpushManualPushVO> pushActivityNotice(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.pushActivityNotice(request));
    }
    /**
     * 生成订单状态更新测试通知并推送。
     */
    @PostMapping("/jpush/manual-push/order-status-update")
    public ApiResponse<JpushManualPushVO> pushOrderStatusUpdate(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.pushOrderStatusUpdate(request));
    }
    /**
     * 生成课程更新测试通知并推送。
     */
    @PostMapping("/jpush/manual-push/course-update-notice")
    public ApiResponse<JpushManualPushVO> pushCourseUpdateNotice(@Valid @RequestBody JpushManualPushRequest request) {
        return ApiResponse.ok(jpushManualPushAppService.pushCourseUpdateNotice(request));
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/dto/JpushManualPushRequest.java
New file
@@ -0,0 +1,18 @@
package com.tcm.training.admin.system.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
 * 极光手动推送联调请求。
 */
@Getter
@Setter
public class JpushManualPushRequest {
    /** 推送目标APP用户ID。 */
    @NotNull(message = "请选择推送用户")
    private Long appUserId;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/JpushManualPushAppService.java
New file
@@ -0,0 +1,163 @@
package com.tcm.training.admin.system.service;
import com.tcm.training.admin.app.notice.entity.AppSystemNotice;
import com.tcm.training.admin.app.notice.service.AppSystemNoticeDispatchCommand;
import com.tcm.training.admin.app.notice.service.AppSystemNoticeDispatchService;
import com.tcm.training.admin.app.notice.service.AppSystemNoticeType;
import com.tcm.training.admin.system.dto.JpushManualPushRequest;
import com.tcm.training.admin.system.vo.JpushManualPushVO;
import com.tcm.training.admin.utils.JGPushUtil;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * 极光手动推送联调应用服务。
 */
@Service
public class JpushManualPushAppService {
    private static final String TEST_TITLE = "极光推送联调";
    private static final String TEST_CONTENT = "这是一条极光推送联调测试消息";
    private static final String TEST_SCENE = "JPUSH_MANUAL_TEST";
    private static final String MANUAL_NOTICE_TEST_SCENE = "MANUAL_NOTICE_TEST";
    private static final String PUSH_STATUS_SUCCESS = "SUCCESS";
    private final JGPushUtil jgPushUtil;
    private final AppSystemNoticeDispatchService appSystemNoticeDispatchService;
    public JpushManualPushAppService(JGPushUtil jgPushUtil,
                                     AppSystemNoticeDispatchService appSystemNoticeDispatchService) {
        this.jgPushUtil = jgPushUtil;
        this.appSystemNoticeDispatchService = appSystemNoticeDispatchService;
    }
    /**
     * 按APP用户ID发起固定内容的极光推送联调。
     */
    public JpushManualPushVO push(JpushManualPushRequest request) {
        String alias = String.valueOf(request.getAppUserId());
        Map<String, String> payload = new LinkedHashMap<String, String>();
        payload.put("scene", TEST_SCENE);
        payload.put("appUserId", alias);
        // 极光APP端alias约定为APP用户ID字符串,用于验证设备绑定与推送链路。
        jgPushUtil.push(TEST_CONTENT, TEST_TITLE, payload, alias);
        JpushManualPushVO result = new JpushManualPushVO();
        result.setAppUserId(request.getAppUserId());
        result.setAlias(alias);
        result.setTitle(TEST_TITLE);
        result.setContent(TEST_CONTENT);
        result.setPushed(true);
        return result;
    }
    /**
     * 生成会员到期提醒测试通知并尝试极光推送。
     */
    public JpushManualPushVO pushMemberExpireRemind(JpushManualPushRequest request) {
        return pushNoticeType(request,
                AppSystemNoticeType.MEMBER_EXPIRE_REMIND,
                "会员到期提醒",
                "您的会员有效期将于测试日期到期",
                false);
    }
    /**
     * 生成直播提醒测试通知并尝试极光推送。
     */
    public JpushManualPushVO pushLiveRemind(JpushManualPushRequest request) {
        return pushNoticeType(request,
                AppSystemNoticeType.LIVE_REMIND,
                "直播提醒",
                "测试直播课程 即将开播",
                true);
    }
    /**
     * 生成活动通知测试通知并尝试极光推送。
     */
    public JpushManualPushVO pushActivityNotice(JpushManualPushRequest request) {
        return pushNoticeType(request,
                AppSystemNoticeType.ACTIVITY_NOTICE,
                "活动通知",
                "这是一条活动通知联调测试消息",
                true);
    }
    /**
     * 生成订单状态更新测试通知并尝试极光推送。
     */
    public JpushManualPushVO pushOrderStatusUpdate(JpushManualPushRequest request) {
        return pushNoticeType(request,
                AppSystemNoticeType.ORDER_STATUS_UPDATE,
                "订单状态更新",
                "您的礼品兑换订单状态已更新",
                true);
    }
    /**
     * 生成课程更新测试通知并尝试极光推送。
     */
    public JpushManualPushVO pushCourseUpdateNotice(JpushManualPushRequest request) {
        return pushNoticeType(request,
                AppSystemNoticeType.COURSE_UPDATE_NOTICE,
                "课程更新通知",
                "测试课程 已更新新章节",
                true);
    }
    /**
     * 按指定通知类型生成系统通知,并复用现有通知推送开关和极光推送链路。
     */
    private JpushManualPushVO pushNoticeType(JpushManualPushRequest request,
                                             String noticeTypeCode,
                                             String title,
                                             String content,
                                             boolean requireUserSetting) {
        Long referenceId = System.currentTimeMillis();
        AppSystemNoticeDispatchCommand command = new AppSystemNoticeDispatchCommand();
        command.setAppUserId(request.getAppUserId());
        command.setNoticeTypeCode(noticeTypeCode);
        command.setTitle(title);
        command.setContent(content);
        command.setReferenceType("MANUAL_TEST_" + noticeTypeCode);
        command.setReferenceId(referenceId);
        command.setRequireUserSetting(requireUserSetting);
        command.setPayload(buildManualNoticePayload(request.getAppUserId(), noticeTypeCode, referenceId));
        AppSystemNotice notice = appSystemNoticeDispatchService.createAndPush(command);
        return toNoticePushVO(notice);
    }
    /**
     * 构造手动通知测试透传参数,便于APP端区分联调消息。
     */
    private Map<String, String> buildManualNoticePayload(Long appUserId, String noticeTypeCode, Long referenceId) {
        Map<String, String> payload = new LinkedHashMap<String, String>();
        payload.put("scene", MANUAL_NOTICE_TEST_SCENE);
        payload.put("noticeTypeCode", noticeTypeCode);
        payload.put("appUserId", String.valueOf(appUserId));
        payload.put("referenceId", String.valueOf(referenceId));
        return payload;
    }
    /**
     * 转换系统通知推送结果为联调接口响应。
     */
    private JpushManualPushVO toNoticePushVO(AppSystemNotice notice) {
        JpushManualPushVO result = new JpushManualPushVO();
        result.setNoticeId(notice.getId());
        result.setAppUserId(notice.getAppUserId());
        result.setNoticeTypeCode(notice.getNoticeTypeCode());
        result.setAlias(String.valueOf(notice.getAppUserId()));
        result.setTitle(notice.getTitle());
        result.setContent(notice.getContent());
        result.setPushStatus(notice.getPushStatus());
        result.setPushErrorMessage(notice.getPushErrorMessage());
        result.setPushedAt(notice.getPushedAt());
        result.setPushed(PUSH_STATUS_SUCCESS.equals(notice.getPushStatus()));
        return result;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/vo/JpushManualPushVO.java
New file
@@ -0,0 +1,44 @@
package com.tcm.training.admin.system.vo;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
 * 极光手动推送联调结果。
 */
@Getter
@Setter
public class JpushManualPushVO {
    /** 系统通知ID。 */
    private Long noticeId;
    /** 推送目标APP用户ID。 */
    private Long appUserId;
    /** 通知类型编码。 */
    private String noticeTypeCode;
    /** 极光推送别名。 */
    private String alias;
    /** 推送标题。 */
    private String title;
    /** 推送内容。 */
    private String content;
    /** 推送状态。 */
    private String pushStatus;
    /** 推送错误信息。 */
    private String pushErrorMessage;
    /** 推送完成时间。 */
    private LocalDateTime pushedAt;
    /** 是否推送成功。 */
    private Boolean pushed;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/utils/JGPushUtil.java
@@ -50,7 +50,7 @@
                .setPlatform(Platform.all())//设置推送平台为全部平台
                .setAudience(Audience.alias(tags))//通过别名的方式发送(user_id)
                .setNotification(Notification.android(content, title, map))
                .setNotification(Notification.ios(content, map))
                // .setNotification(Notification.ios(content, map))
                // .setNotification(Notification.winphone(content, map))
                .build();
    }
tcm-training-admin-backend/src/main/resources/application.yml
@@ -122,10 +122,10 @@
      jwks-url: https://appleid.apple.com/auth/keys
  share:
    # 邀请二维码默认指向线上下载页,开发环境可在 application-local.yml 覆盖。
    landing-base-url: https://haizhentong.com/app-download/
    landing-base-url: https://ag8spw.oplinking.com/
jpush:
  appKey: d2c5362f73af203f25704b8d
  masterSecret: c6996db56be1a93ae16fc1b0
  appKey: b5aed7d076638a0d835786bd
  masterSecret: e17305459be4f0011ca15c9e
logging:
  level:
traditionalApp/App.vue
@@ -8,40 +8,70 @@
        extractInviteCode
    } from '@/utils/utils.js'
    import { initJPushLifecycle } from '@/utils/push.js'
    import {
        getOpeninstallInstallData,
        initOpeninstall,
        registerOpeninstallWakeUp
    } from '@/utils/openinstall.js'
    let lastHandledBindTicket = ''
    let lastHandledInvitePayload = ''
    /**
     * 应用启动时保留钩子,当前不再依赖系统剪贴板承接推广关系。
     * 应用启动时初始化极光与 openinstall,承接推广关系参数。
     */
    onLaunch(() => {
        // #ifdef APP-PLUS
        initJPushLifecycle()
        initOpeninstall()
        registerOpeninstallWakeUp(handleOpeninstallPayload)
        getOpeninstallInstallData(handleOpeninstallPayload)
        // #endif
    })
    /**
     * App 被外部 Scheme 拉起时,解析 bindTicket 并跳转注册页。
     * App 被外部 Scheme 拉起时,解析 bindTicket 或 inviteCode 并跳转注册页。
     */
    onShow(() => {
        handleOpenByScheme()
    })
    /**
     * 读取原生启动参数中的绑定票据,并在命中时跳转注册页。
     * 读取原生启动参数中的绑定票据或邀请码,并在命中时跳转注册页。
     */
    function handleOpenByScheme() {
        // #ifdef APP-PLUS
        const runtimeArguments = typeof plus !== 'undefined' && plus.runtime ? plus.runtime.arguments : ''
        const bindTicket = extractBindTicket(runtimeArguments)
        if (!bindTicket || bindTicket === lastHandledBindTicket) {
        handleInvitePayload(runtimeArguments)
        // #endif
    }
    /**
     * 处理 openinstall 安装参数或唤醒参数。
     */
    function handleOpeninstallPayload(payload) {
        handleInvitePayload(payload)
    }
    /**
     * 统一解析外部推广参数,并跳转到注册页回填。
     */
    function handleInvitePayload(payload) {
        const bindTicket = extractBindTicket(payload)
        const inviteCode = extractInviteCode(payload)
        if (!bindTicket && !inviteCode) {
            return
        }
        lastHandledBindTicket = bindTicket
        const inviteCode = extractInviteCode(runtimeArguments)
        const payloadKey = `${bindTicket || ''}_${inviteCode || ''}`
        if (payloadKey === lastHandledInvitePayload) {
            return
        }
        lastHandledInvitePayload = payloadKey
        const currentPages = getCurrentPages()
        const currentRoute = currentPages && currentPages.length > 0 ? currentPages[currentPages.length - 1].route : ''
        const targetParams = [`bindTicket=${encodeURIComponent(bindTicket)}`]
        const targetParams = []
        if (bindTicket) {
            targetParams.push(`bindTicket=${encodeURIComponent(bindTicket)}`)
        }
        if (inviteCode) {
            targetParams.push(`inviteCode=${encodeURIComponent(inviteCode)}`)
        }
@@ -55,7 +85,6 @@
        uni.navigateTo({
            url: targetUrl
        })
        // #endif
    }
</script>
traditionalApp/manifest.json
@@ -53,6 +53,11 @@
            /* ios打包配置 */
            "ios" : {
                "dSYMs" : false,
                "privacyDescription": {
                    "NSCameraUsageDescription": "需要使用相机拍摄照片 / 视频,用于扫码自动填充邀请码、拍摄照片作为头像",
                    "NSPhotoLibraryUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码",
                    "NSPhotoLibraryAddUsageDescription": "需要访问手机相册,用于选择图片 / 视频上传、更换个人头像、选择邀请二维码"
                },
                "capabilities" : {
                    "entitlements" : {
                        "com.apple.developer.associated-domains" : [
@@ -60,7 +65,8 @@
                        ]
                    }
                },
                "idfa" : false
                "idfa" : false,
                "urltypes": "ag8spw"
            },
            /* SDK配置 */
            "sdkConfigs" : {
@@ -124,6 +130,33 @@
            }
        },
        "nativePlugins" : {
            "openinstall-plugin": {
                "APP_KEY_ANDROID": "ag8spw",
                "APP_KEY_IOS": "ag8spw",
                "__plugin_info__": {
                    "name": "openinstall",
                    "description": "openinstall 一键拉起与免填邀请码插件",
                    "platforms": "Android,iOS",
                    "url": "https://ext.dcloud.net.cn/plugin?id=692",
                    "android_package_name": "com.HaiZhenKangDao.hzt",
                    "ios_bundle_id": "com.HaiZhenKangDao.hzt",
                    "isCloud": true,
                    "bought": 1,
                    "pid": "692",
                    "parameters": {
                        "APP_KEY_ANDROID": {
                            "des": "[Android]openinstall 控制台分配的 AppKey",
                            "key": "com.openinstall.APP_KEY",
                            "value": ""
                        },
                        "APP_KEY_IOS": {
                            "des": "[iOS]openinstall 控制台分配的 AppKey",
                            "key": "com.openinstall.APP_KEY",
                            "value": ""
                        }
                    }
                }
            },
            "JG-JCore" : {
                "JPUSH_APPKEY_ANDROID" : "b5aed7d076638a0d835786bd",
                "JPUSH_APPKEY_IOS" : "b5aed7d076638a0d835786bd",
traditionalApp/pages/register/register.vue
@@ -176,6 +176,7 @@
    validateInviteBindTicket
} from './service.js'
import { extractBindTicket, extractInviteCode, normalizeOptionalText } from '@/utils/utils.js'
import { reportOpeninstallRegister } from '@/utils/openinstall.js'
import { confirmCameraForInviteCodeScan, confirmMediaForLicense } from '@/utils/permission.js'
interface RegionItem {
@@ -816,6 +817,8 @@
 * 注册成功后提示并返回登录页。
 */
const handleRegisterSuccess = async (message: string) => {
    // 注册成功后通知 openinstall,用于统计推广链路的注册转化。
    reportOpeninstallRegister()
    uni.showToast({
        title: message,
        icon: 'success'
traditionalApp/utils/openinstall.js
New file
@@ -0,0 +1,98 @@
let openinstallPlugin = null
let initialized = false
let wakeUpRegistered = false
/**
 * 安全获取 openinstall 原生插件实例,非 APP 或插件不存在时返回空。
 */
export const getOpeninstallPlugin = () => {
    // #ifdef APP-PLUS
    if (openinstallPlugin) {
        return openinstallPlugin
    }
    try {
        openinstallPlugin = uni.requireNativePlugin('openinstall-plugin')
    } catch (error) {
        console.log('openinstall 插件不可用', error)
        openinstallPlugin = null
    }
    return openinstallPlugin
    // #endif
    // #ifndef APP-PLUS
    return null
    // #endif
}
/**
 * 初始化 openinstall 插件,避免多次启动重复初始化。
 */
export const initOpeninstall = () => {
    const plugin = getOpeninstallPlugin()
    if (!plugin || initialized) {
        return false
    }
    try {
        plugin.init()
        initialized = true
        return true
    } catch (error) {
        console.log('openinstall 初始化失败', error)
        return false
    }
}
/**
 * 注册 openinstall 拉起参数监听,用于已安装 APP 的扫码唤醒场景。
 */
export const registerOpeninstallWakeUp = (callback) => {
    const plugin = getOpeninstallPlugin()
    if (!plugin || wakeUpRegistered || typeof callback !== 'function') {
        return false
    }
    try {
        plugin.registerWakeUp((result) => {
            callback(result)
        })
        wakeUpRegistered = true
        return true
    } catch (error) {
        console.log('openinstall 拉起监听注册失败', error)
        return false
    }
}
/**
 * 读取 openinstall 安装参数,用于未安装 APP 下载后首次打开场景。
 */
export const getOpeninstallInstallData = (callback) => {
    const plugin = getOpeninstallPlugin()
    if (!plugin || typeof callback !== 'function') {
        return false
    }
    try {
        plugin.getInstall(8, (result) => {
            callback(result)
        })
        return true
    } catch (error) {
        console.log('openinstall 安装参数获取失败', error)
        return false
    }
}
/**
 * 注册成功后上报 openinstall,便于后台统计注册转化。
 */
export const reportOpeninstallRegister = () => {
    const plugin = getOpeninstallPlugin()
    if (!plugin) {
        return false
    }
    try {
        plugin.reportRegister()
        return true
    } catch (error) {
        console.log('openinstall 注册上报失败', error)
        return false
    }
}
traditionalApp/utils/utils.js
@@ -225,33 +225,101 @@
}
/**
 * 从 URL、纯文本或扫码内容中提取 6 位邀请码。
 * 将外部唤醒参数归一化为可解析文本,兼容 openinstall 对象、JSON 和 URL。
 */
export const normalizeInvitePayloadText = (value) => {
    if (!value) {
        return ''
    }
    if (typeof value === 'string') {
        return value.trim()
    }
    if (typeof value !== 'object') {
        return ''
    }
    const candidateKeys = ['bindData', 'data', 'channelData', 'params', 'wakeUpData']
    for (const key of candidateKeys) {
        const nestedValue = value[key]
        if (nestedValue) {
            const nestedText = normalizeInvitePayloadText(nestedValue)
            if (nestedText) {
                return nestedText
            }
        }
    }
    try {
        return JSON.stringify(value)
    } catch {
        return ''
    }
}
/**
 * 从 URL、纯文本、对象或扫码内容中提取 6 位邀请码。
 */
export const extractInviteCode = (value) => {
    const normalizedValue = normalizeOptionalText(value)
    const normalizedValue = normalizeInvitePayloadText(value)
    if (!normalizedValue) {
        return ''
    }
    const queryMatchedInviteCode = normalizedValue.match(/[?&]inviteCode=([^&#]+)/i)
    const objectInviteCode = extractObjectField(normalizedValue, 'inviteCode')
    if (objectInviteCode) {
        return normalizeInviteCodeText(objectInviteCode)
    }
    const queryMatchedInviteCode = normalizedValue.match(/(?:^|[?&])inviteCode=([^&#]+)/i)
    if (queryMatchedInviteCode && queryMatchedInviteCode[1]) {
        const decodedInviteCode = decodeURIComponent(queryMatchedInviteCode[1]).trim().toUpperCase()
        return /^[A-Z0-9]{6}$/.test(decodedInviteCode) ? decodedInviteCode : ''
        return normalizeInviteCodeText(decodeURIComponent(queryMatchedInviteCode[1]))
    }
    const matchedInviteCode = normalizedValue.toUpperCase().match(/[A-Z0-9]{6}/)
    return matchedInviteCode ? matchedInviteCode[0] : ''
}
/**
 * 从 URL 中提取 bindTicket 参数。
 * 从 URL、对象或 JSON 文本中提取 bindTicket 参数。
 */
export const extractBindTicket = (value) => {
    const normalizedValue = normalizeOptionalText(value)
    const normalizedValue = normalizeInvitePayloadText(value)
    if (!normalizedValue) {
        return ''
    }
    const queryMatchedBindTicket = normalizedValue.match(/[?&]bindTicket=([^&#]+)/i)
    const objectBindTicket = extractObjectField(normalizedValue, 'bindTicket')
    if (objectBindTicket) {
        return objectBindTicket.trim()
    }
    const queryMatchedBindTicket = normalizedValue.match(/(?:^|[?&])bindTicket=([^&#]+)/i)
    if (!queryMatchedBindTicket || !queryMatchedBindTicket[1]) {
        return ''
    }
    return decodeURIComponent(queryMatchedBindTicket[1]).trim()
}
/**
 * 从 JSON 字符串中读取指定字段。
 */
const extractObjectField = (value, fieldName) => {
    try {
        const parsedValue = JSON.parse(value)
        if (!parsedValue || typeof parsedValue !== 'object') {
            return ''
        }
        const directValue = parsedValue[fieldName]
        if (typeof directValue === 'string') {
            return directValue
        }
        const bindDataValue = parsedValue.bindData || parsedValue.data || parsedValue.params
        if (bindDataValue && bindDataValue !== value) {
            return extractObjectField(normalizeInvitePayloadText(bindDataValue), fieldName)
        }
    } catch {
        return ''
    }
    return ''
}
/**
 * 标准化邀请码文本。
 */
const normalizeInviteCodeText = (value) => {
    const inviteCode = normalizeOptionalText(value).toUpperCase()
    return /^[A-Z0-9]{6}$/.test(inviteCode) ? inviteCode : ''
}