fix
13404089107
2026-04-27 ae13033b094e5dc99d21cdae2b40be80de739c5c
fix
2个文件已添加
4个文件已修改
547 ■■■■■ 已修改文件
traditionalApp/androidPrivacy.json 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/courseDetail/courseDetail.vue 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/vip/vip.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/appleIap.js 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/androidPrivacy.json
New file
@@ -0,0 +1,38 @@
{
    "version" : "1",
    "prompt" : "template",
    "title" : "服务协议和隐私政策",
    "message" : "欢迎使用海诊康道!海诊康道非常重视你的隐私和个人信息保护,为保障你的权益,请在使用前认真阅读<a href=\"https://haizhentong.com/agreement/?type=user\">《用户服务协议》</a>和<a href=\"https://haizhentong.com/agreement/?type=privacy\">《隐私政策》</a>,点击【同意并继续】表示已充分阅读并接受已上协议,政策的内容",
    "buttonAccept" : "同意并接受",
    "buttonRefuse" : "暂不同意",
    "hrefLoader" : "system",
    "backToExit" : "false",
    "second" : {
        "title" : "确认提示",
        "message" : "进入应用前,你需先同意<a href=\"https://haizhentong.com/agreement/?type=user\">《用户服务协议》</a>和<a href=\"https://haizhentong.com/agreement/?type=privacy\">《隐私政策》</a>,否则将退出应用。",
        "buttonAccept" : "同意并继续",
        "buttonRefuse" : "退出应用"
    },
    "disagreeMode" : {
        "support" : false,
        "loadNativePlugins" : false,
        "visitorEntry" : false,
        "showAlways" : false
    },
    "styles" : {
        "backgroundColor" : "#ffffff",
        "borderRadius" : "5px",
        "title" : {
            "color" : "#000000"
        },
        "buttonAccept" : {
            "color" : "#41A2EB"
        },
        "buttonRefuse" : {
            "color" : "#000000"
        },
        "buttonVisitor" : {
            "color" : "#000000"
        }
    }
}
traditionalApp/manifest.json
@@ -1,7 +1,7 @@
{
    "sassImplementationName" : "node-sass",
    "name" : "海诊康道",
    "appid" : "__UNI__1A90541",
    "appid": "__UNI__FB03FDD",
    "description" : "",
    "versionName" : "1.0.1",
    "versionCode" : 101,
@@ -22,7 +22,8 @@
            "Barcode" : {},
            "VideoPlayer" : {},
            "Share" : {},
            "OAuth" : {}
            "OAuth": {},
            "Payment": {}
        },
        /* 应用发布信息 */
        "distribute" : {
@@ -45,8 +46,8 @@
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ],
                "minSdkVersion" : 30,
                "targetSdkVersion" : 29
                "minSdkVersion": 28,
                "targetSdkVersion": 30
            },
            /* ios打包配置 */
            "ios" : {
@@ -74,7 +75,14 @@
                        "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                },
                "payment" : {}
                "payment": {
                    "appleiap": {},
                    "weixin": {
                        "__platform__": ["ios", "android"],
                        "appid": "wx074c1f0cf5a0ecf4",
                        "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
                    }
                }
            },
            "icons" : {
                "android" : {
@@ -107,6 +115,10 @@
                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
                    }
                }
            },
            "splashscreen": {
                "androidStyle": "common",
                "useOriginalMsgbox": true
            }
        }
    },
traditionalApp/pages/courseDetail/courseDetail.vue
@@ -62,7 +62,7 @@
        </view>
        <!-- 页面滚动内容区域 -->
        <scroll-view scroll-y class="scroll-content">
        <scroll-view scroll-y class="scroll-content" :style="scrollContentStyle">
            <template v-if="currentTab === 'course'">
                <!-- 基本信息模块 -->
                <view class="info-section">
@@ -112,14 +112,14 @@
                                <view class="item-right-box">
                                    <text class="item-time">{{ item.durationText }}</text>
                                    <image
                                        v-if="shouldShowVideoDownload(item)"
                                        v-if="!isReadonlyDisplayMode && shouldShowVideoDownload(item)"
                                        class="download-icon"
                                        src="/static/image/course/redDownload@2x.png"
                                        mode="aspectFit"
                                        @tap.stop="handleVideoDownload(item)"
                                    ></image>
                                    <view
                                        v-else
                                        v-else-if="!isReadonlyDisplayMode"
                                        class="cost-btn"
                                        :class="{ 'cost-btn--purchased': item.purchased || courseDetail.courseOwned }"
                                        @tap.stop="handlePaidVideoTap(item)"
@@ -190,14 +190,14 @@
                                <view class="item-right-box">
                                    <text class="item-time">{{ item.durationText }}</text>
                                    <image
                                        v-if="shouldShowVideoDownload(item)"
                                        v-if="!isReadonlyDisplayMode && shouldShowVideoDownload(item)"
                                        class="download-icon"
                                        src="/static/image/course/redDownload@2x.png"
                                        mode="aspectFit"
                                        @tap.stop="handleVideoDownload(item)"
                                    ></image>
                                    <view
                                        v-else
                                        v-else-if="!isReadonlyDisplayMode"
                                        class="cost-btn"
                                        :class="{ 'cost-btn--purchased': item.purchased || courseDetail.courseOwned }"
                                        @tap.stop="handlePaidVideoTap(item)"
@@ -214,10 +214,15 @@
            </template>
            <view v-if="loading" class="loading-text">加载中...</view>
            <view v-if="isReadonlyDisplayMode" class="open-app-banner" @tap="openApp">
                <view class="app-logo-placeholder"></view>
                <text class="banner-text">打开APP,了解更多</text>
                <view class="open-btn">立即打开</view>
            </view>
        </scroll-view>
        <!-- 底部悬浮按钮 -->
        <view class="bottom-bar">
        <view v-if="!isReadonlyDisplayMode" class="bottom-bar">
            <view class="action-icons">
                <view class="action-icon-item" @tap="handleConsult">
                    <image class="icon-img icon-listen" src="/static/image/healthCourses/listen.png" mode="aspectFit"></image>
@@ -247,6 +252,15 @@
                <view class="payment-methods">
                    <text class="method-title">选择支付方式</text>
                    <view v-if="isIosApp" class="method-item" @tap="selectPaymentMethod('appleiap')">
                        <view class="method-left">
                            <view class="pay-icon pay-icon--apple">A</view>
                            <text class="method-name">App Store 支付</text>
                        </view>
                        <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
                    </view>
                    <template v-else>
                    <view class="method-item" @tap="selectPaymentMethod('wechat')">
                        <view class="method-left">
                            <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image>
@@ -262,6 +276,7 @@
                        </view>
                        <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
                    </view>
                    </template>
                </view>
                <view class="popup-footer">
@@ -271,7 +286,6 @@
                </view>
            </view>
        </u-popup>
    </view>
</template>
@@ -289,6 +303,7 @@
    unfavoriteHealthCourse,
} from './service'
import { startCourseVideoDownload, hasCourseDownloadTask } from '@/utils/course-download.js'
import { requestAppleIapPayment } from '@/utils/appleIap.js'
import { resolveCurrentAppUserId } from '@/utils/app-user.js'
import {
    bindVideoCaptureListener,
@@ -316,6 +331,9 @@
    purchased: boolean
    locked: boolean
    videoUrl?: string
    appleProductId?: string
    productId?: string
    iosProductId?: string
}
interface CourseDetail {
@@ -342,6 +360,9 @@
    memberCourseJoined: boolean
    courseOwned: boolean
    coursePurchased: boolean
    appleProductId?: string
    productId?: string
    iosProductId?: string
}
interface SwiperChangeEvent {
@@ -350,7 +371,7 @@
    }
}
type PaymentMethod = 'wechat' | 'alipay'
type PaymentMethod = 'wechat' | 'alipay' | 'appleiap'
type PurchaseTarget = 'COURSE' | 'VIDEO'
const VIDEO_PLAYER_ID = 'course-detail-video-player'
@@ -371,7 +392,7 @@
const purchaseSubmitting = ref(false)
const memberJoinSubmitting = ref(false)
const showPaymentPopup = ref(false)
const selectedPaymentMethod = ref<PaymentMethod>('wechat')
const selectedPaymentMethod = ref<PaymentMethod>(isIosApp ? 'appleiap' : 'wechat')
const activePurchaseTarget = ref<PurchaseTarget>('COURSE')
const activePurchaseVideoId = ref<number | null>(null)
const courseId = ref('')
@@ -460,6 +481,20 @@
})
const currentVideoUrl = computed(() => currentVideo.value?.videoUrl || '')
const currentVideoPoster = computed(() => courseDetail.value.bannerImages[0] || '/static/image/home/class.png')
const isReadonlyDisplayMode = isH5
const scrollContentStyle = computed(() => {
    if (isReadonlyDisplayMode) {
        return {
            paddingBottom: 'calc(140rpx + env(safe-area-inset-bottom))',
        }
    }
    if (!isAppPlus) {
        return {
            paddingBottom: 'calc(260rpx + env(safe-area-inset-bottom))',
        }
    }
    return {}
})
const videoWatermarkText = computed(() => {
    const currentUserId = resolveCurrentAppUserId()
    if (!currentUserId) {
@@ -625,6 +660,9 @@
 * 统一处理章节点击,可播放时切换顶部播放器,否则保留原有购买弹窗。
 */
function handleVideoSelect(item: CourseVideo) {
    if (isReadonlyDisplayMode) {
        return
    }
    if (canPlayVideo(item)) {
        playSelectedVideo(item)
        return
@@ -704,6 +742,9 @@
 * 对当前播放器执行播放重试,避免首次切换章节时无感失败。
 */
function retryPlayback() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!videoContext || !currentVideoUrl.value) {
        return
    }
@@ -751,6 +792,9 @@
 * 跳转专家介绍页,并携带专家主键加载真实内容。
 */
function goToExpertDetail() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseDetail.value.expert?.id) {
        return
    }
@@ -763,6 +807,9 @@
 * 拨打后台系统配置中的客服电话,未配置时给出兜底提示。
 */
function handleConsult() {
    if (isReadonlyDisplayMode) {
        return
    }
    const servicePhone = courseDetail.value.servicePhone.trim()
    if (!servicePhone) {
        uni.showToast({
@@ -774,6 +821,20 @@
    uni.makePhoneCall({
        phoneNumber: servicePhone,
    })
}
/**
 * 拉起或跳转下载 APP
 */
function openApp() {
    if (isReadonlyDisplayMode) {
        return
    }
    uni.showToast({
        title: '即将打开APP',
        icon: 'none',
    })
    // 可在此处补充跳转下载引导页的具体逻辑
}
/**
@@ -799,6 +860,9 @@
 * 通过微信好友聊天场景分享当前课程。
 */
function shareToWechatSession() {
    if (isReadonlyDisplayMode) {
        return
    }
    // #ifdef APP-PLUS
    plus.share.getServices((services) => {
        const weixinService = services.find((service) => service.id === 'weixin')
@@ -906,12 +970,18 @@
 * 打开整课购买支付弹窗。
 */
function handleCoursePurchase() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseId.value || courseDetail.value.courseOwned || memberJoinSubmitting.value) {
        return
    }
    if (courseDetail.value.isMember) {
        submitMemberJoin()
        return
    }
    if (isIosApp) {
        selectedPaymentMethod.value = 'appleiap'
    }
    activePurchaseTarget.value = 'COURSE'
    activePurchaseVideoId.value = null
@@ -922,8 +992,14 @@
 * 处理付费小节点击,未购买时进入对应支付弹窗。
 */
function handlePaidVideoTap(item: CourseVideo) {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseId.value || courseDetail.value.courseOwned || item.purchased) {
        return
    }
    if (isIosApp) {
        selectedPaymentMethod.value = 'appleiap'
    }
    activePurchaseTarget.value = 'VIDEO'
    activePurchaseVideoId.value = item.id
@@ -942,6 +1018,9 @@
 * 下载已购买章节视频,H5 直接打开资源地址,其它端走文件下载。
 */
function handleVideoDownload(item: CourseVideo) {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!shouldShowVideoDownload(item)) {
        return
    }
@@ -1004,6 +1083,9 @@
 * 提交当前购买目标的直付订单。
 */
async function submitPurchase() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseId.value || purchaseSubmitting.value) {
        return
    }
@@ -1012,8 +1094,13 @@
    }
    purchaseSubmitting.value = true
    try {
        const payload = {
        const payload = selectedPaymentMethod.value === 'appleiap'
            ? await buildAppleIapPurchasePayload()
            : {
            paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY',
            }
        if (!payload) {
            return
        }
        const response = activePurchaseTarget.value === 'VIDEO' && activePurchaseVideoId.value !== null
            ? await purchaseHealthCourseVideo(courseId.value, activePurchaseVideoId.value, payload)
@@ -1031,6 +1118,37 @@
        })
    } finally {
        purchaseSubmitting.value = false
    }
}
/**
 * 拉起苹果内购并组装课程/章节购买所需的验单参数。
 */
async function buildAppleIapPurchasePayload() {
    const appleProductId = resolveActiveAppleProductId()
    if (!appleProductId) {
        uni.showToast({
            title: '当前商品未配置苹果内购商品',
            icon: 'none',
        })
        return null
    }
    const iapResult = await requestAppleIapPayment({
        productId: appleProductId,
        username: resolveAppleIapUsername(),
    })
    if (!normalizeOptionalText(iapResult?.transactionReceipt)) {
        uni.showToast({
            title: '苹果内购凭证缺失',
            icon: 'none',
        })
        return null
    }
    return {
        paymentMethod: 'APPLE_IAP',
        appleProductId,
        transactionReceipt: iapResult.transactionReceipt,
        transactionIdentifier: iapResult.transactionIdentifier,
    }
}
@@ -1068,10 +1186,29 @@
    }
}
function resolveActiveAppleProductId() {
    if (activePurchaseTarget.value === 'VIDEO' && activePurchaseVideoId.value !== null) {
        const currentVideo = courseDetail.value.videos.find((item) => Number(item.id) === Number(activePurchaseVideoId.value))
        return normalizeOptionalText(currentVideo?.appleProductId || currentVideo?.productId || currentVideo?.iosProductId)
    }
    return normalizeOptionalText(courseDetail.value.appleProductId || courseDetail.value.productId || courseDetail.value.iosProductId)
}
function resolveAppleIapUsername() {
    const userId = resolveCurrentAppUserId()
    if (userId > 0) {
        return String(userId)
    }
    return 'guest'
}
/**
 * 提交会员加课请求,并将整课直接回写为已拥有。
 */
async function submitMemberJoin() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseId.value || memberJoinSubmitting.value || courseDetail.value.courseOwned || !courseDetail.value.isMember) {
        return
    }
@@ -1111,6 +1248,9 @@
 * 切换课程收藏状态,并直接回写详情页当前展示数据。
 */
async function toggleFavorite() {
    if (isReadonlyDisplayMode) {
        return
    }
    if (!courseId.value || favoriteSubmitting.value) {
        return
    }
@@ -1465,6 +1605,47 @@
    color: #999;
}
.open-app-banner {
    position: fixed;
    left: 32rpx;
    right: 32rpx;
    bottom: calc(32rpx + env(safe-area-inset-bottom));
    height: 96rpx;
    background-color: #FFF1F0;
    border-radius: 16rpx;
    display: flex;
    align-items: center;
    padding: 0 24rpx;
    box-sizing: border-box;
    z-index: 90;
    .app-logo-placeholder {
        width: 48rpx;
        height: 48rpx;
        background-color: #FFFFFF;
        border-radius: 8rpx;
        margin-right: 16rpx;
    }
    .banner-text {
        flex: 1;
        font-size: 26rpx;
        color: #C63520;
    }
    .open-btn {
        width: 140rpx;
        height: 56rpx;
        background-color: #C63520;
        border-radius: 8rpx;
        color: #FFFFFF;
        font-size: 24rpx;
        display: flex;
        justify-content: center;
        align-items: center;
    }
}
.bottom-bar {
    position: fixed;
    bottom: 0;
@@ -1592,6 +1773,17 @@
    margin-right: 16rpx;
}
.pay-icon--apple {
    border-radius: 12rpx;
    background: #111111;
    color: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28rpx;
    font-weight: 600;
}
.method-name {
    font-size: 28rpx;
    color: #1A1A1A;
traditionalApp/pages/login/login.vue
@@ -236,27 +236,7 @@
                    })
                    return
                }
                weixinService.authorize((event : any) => {
                    const code = normalizeOptionalText(event?.code)
                    if (!code) {
                        reject({
                            code: -99,
                            errMsg: '微信授权未返回有效code'
                        })
                        return
                    }
                    resolve({
                        code
                    })
                }, (error : any) => {
                    reject({
                        code: error?.code,
                        errMsg: error?.message || '微信授权失败'
                    })
                }, {
                    scope: 'snsapi_userinfo',
                    state: 'login'
                })
                requestWechatAuthorizeByService(weixinService, resolve, reject)
            }, (error : any) => {
                reject({
                    code: error?.code,
@@ -279,6 +259,62 @@
    }
    /**
     * 通过微信原生服务执行授权;iOS 端先清理残留授权态,再重新拉起微信。
     */
    const requestWechatAuthorizeByService = (
        weixinService : any,
        resolve : (value : WechatLoginResult) => void,
        reject : (reason ?: any) => void
    ) => {
        const executeAuthorize = () => {
            weixinService.authorize((event : any) => {
                const code = normalizeOptionalText(event?.code)
                const authResult = weixinService.authResult || {}
                const accessToken = normalizeOptionalText(authResult.access_token || authResult.accessToken)
                const openid = normalizeOptionalText(authResult.openid || authResult.openId)
                if (!code && !(accessToken && openid)) {
                    reject({
                        code: -99,
                        errMsg: '微信授权未返回有效凭证'
                    })
                    return
                }
                resolve({
                    code,
                    authResult: {
                        access_token: authResult.access_token,
                        accessToken: authResult.accessToken,
                        openid: authResult.openid,
                        openId: authResult.openId,
                        unionid: authResult.unionid,
                        unionId: authResult.unionId
                    }
                })
            }, (error : any) => {
                reject({
                    code: error?.code,
                    errMsg: error?.message || '微信授权失败'
                })
            }, {
                scope: 'snsapi_userinfo',
                state: 'login'
            })
        }
        const authResult = weixinService?.authResult || {}
        const hasAuthorized = !!normalizeOptionalText(authResult.access_token || authResult.accessToken || authResult.openid || authResult.openId)
        if (!hasAuthorized || typeof weixinService.logout !== 'function') {
            executeAuthorize()
            return
        }
        weixinService.logout(() => {
            executeAuthorize()
        }, () => {
            executeAuthorize()
        })
    }
    /**
     * 从 uni.login 返回结果中提取微信登录接口需要的参数。
     */
    const buildWechatLoginPayload = (loginRes : WechatLoginResult) => {
traditionalApp/pages/vip/vip.vue
@@ -115,6 +115,17 @@
                <view class="payment-methods">
                    <text class="method-title">选择支付方式</text>
                    <view v-if="isIosApp" class="method-item" @tap="selectPayment('appleiap')">
                        <view class="method-left">
                            <view class="pay-icon pay-icon--apple">A</view>
                            <text class="method-name">App Store 支付</text>
                        </view>
                        <view class="method-radio">
                            <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
                        </view>
                    </view>
                    <template v-else>
                    <view class="method-item" @tap="selectPayment('wechat')">
                        <view class="method-left">
                            <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image>
@@ -134,6 +145,7 @@
                            <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
                        </view>
                    </view>
                    </template>
                </view>
                <view class="popup-footer">
@@ -150,8 +162,10 @@
import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getMemberOpenPage, purchaseMember } from './service'
import { requestAppleIapPayment, isIosAppRuntime } from '@/utils/appleIap.js'
import { resolveCurrentAppUserId } from '@/utils/app-user.js'
type PaymentMethod = 'wechat' | 'alipay'
type PaymentMethod = 'wechat' | 'alipay' | 'appleiap'
type PurchaseMemberType = 'MONTH_CARD' | 'QUARTER_CARD'
@@ -160,6 +174,9 @@
    title: string
    price: number | string | null
    days: number | null
    appleProductId?: string
    productId?: string
    iosProductId?: string
}
interface MemberBenefitItem {
@@ -177,6 +194,7 @@
}
interface AppUserInfo {
    id?: number | null
    nickname?: string
    phone?: string
    avatar?: string
@@ -198,7 +216,8 @@
const statusBarHeight = ref(20)
const currentPlan = ref('')
const showPaymentPopup = ref(false)
const selectedPaymentMethod = ref<PaymentMethod>('alipay')
const isIosApp = isIosAppRuntime()
const selectedPaymentMethod = ref<PaymentMethod>(isIosApp ? 'appleiap' : 'alipay')
const purchaseSubmitting = ref(false)
const memberOpenPage = ref<MemberOpenPageData>(createDefaultMemberOpenPage())
const appUserInfo = ref<AppUserInfo>(createDefaultAppUserInfo())
@@ -409,6 +428,9 @@
        })
        return
    }
    if (isIosApp) {
        selectedPaymentMethod.value = 'appleiap'
    }
    showPaymentPopup.value = true
}
@@ -463,6 +485,19 @@
    }
    purchaseSubmitting.value = true
    try {
        if (selectedPaymentMethod.value === 'appleiap') {
            const response = await submitAppleIapMemberPurchase(memberType)
            if (response?.code !== 0 || !response.data) {
                return
            }
            applyPurchaseResult(response.data)
            showPaymentPopup.value = false
            uni.showToast({
                title: '开通成功',
                icon: 'none',
            })
            return
        }
        const response = await purchaseMember({
            memberType,
            paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY',
@@ -479,6 +514,38 @@
    } finally {
        purchaseSubmitting.value = false
    }
}
/**
 * 拉起苹果内购后,将交易凭证回传会员购买接口完成验单。
 */
async function submitAppleIapMemberPurchase(memberType: PurchaseMemberType) {
    const appleProductId = resolveMemberAppleProductId(selectedPlan.value)
    if (!appleProductId) {
        uni.showToast({
            title: '当前套餐未配置苹果内购商品',
            icon: 'none',
        })
        return null
    }
    const iapResult = await requestAppleIapPayment({
        productId: appleProductId,
        username: resolveAppleIapUsername(),
    })
    if (!normalizeOptionalText(iapResult?.transactionReceipt)) {
        uni.showToast({
            title: '苹果内购凭证缺失',
            icon: 'none',
        })
        return null
    }
    return purchaseMember({
        memberType,
        paymentMethod: 'APPLE_IAP',
        appleProductId,
        transactionReceipt: iapResult.transactionReceipt,
        transactionIdentifier: iapResult.transactionIdentifier,
    })
}
/**
@@ -506,6 +573,21 @@
    }
    uni.setStorageSync('appUserInfo', nextUserInfo)
    syncCachedUserInfo()
}
function resolveMemberAppleProductId(plan?: MemberOpenPackage | null) {
    if (!plan) {
        return ''
    }
    return normalizeOptionalText(plan.appleProductId || plan.productId || plan.iosProductId)
}
function resolveAppleIapUsername() {
    const userId = resolveCurrentAppUserId()
    if (userId > 0) {
        return String(userId)
    }
    return normalizeOptionalText(appUserInfo.value.phone) || 'guest'
}
/**
@@ -1068,6 +1150,19 @@
        width: 100rpx;
        height: 68.57rpx;
    }
    .pay-icon--apple {
        width: 68rpx;
        height: 68rpx;
        border-radius: 16rpx;
        background: #111111;
        color: #ffffff;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 34rpx;
        font-weight: 600;
    }
}
.method-name {
traditionalApp/utils/appleIap.js
New file
@@ -0,0 +1,98 @@
/**
 * 判断当前是否为 iOS App 运行环境。
 */
export function isIosAppRuntime() {
    // #ifndef APP-PLUS
    return false
    // #endif
    // #ifdef APP-PLUS
    try {
        return plus.os?.name === 'iOS'
    } catch {
        return false
    }
    // #endif
}
/**
 * 读取苹果内购支付通道。
 */
export function getAppleIapChannel() {
    return new Promise((resolve, reject) => {
        // #ifndef APP-PLUS
        reject({
            code: -3,
            message: '当前环境不支持苹果内购',
        })
        return
        // #endif
        // #ifdef APP-PLUS
        plus.payment.getChannels((channels) => {
            const appleChannel = Array.isArray(channels) ? channels.find((item) => item.id === 'appleiap') : null
            if (!appleChannel) {
                reject({
                    code: -3,
                    message: '当前应用未启用苹果内购',
                })
                return
            }
            resolve(appleChannel)
        }, (error) => {
            reject(error || {
                code: -99,
                message: '获取苹果内购通道失败',
            })
        })
        // #endif
    })
}
/**
 * 拉起苹果内购支付并返回原始交易结果。
 */
export async function requestAppleIapPayment({ productId, username } = {}) {
    const normalizedProductId = typeof productId === 'string' ? productId.trim() : ''
    if (!normalizedProductId) {
        throw {
            code: -1,
            message: '缺少苹果内购商品ID',
        }
    }
    const channel = await getAppleIapChannel()
    return new Promise((resolve, reject) => {
        const orderInfo = {
            productid: normalizedProductId,
        }
        const normalizedUsername = typeof username === 'string' ? username.trim() : ''
        if (normalizedUsername) {
            orderInfo.username = normalizedUsername
        }
        plus.payment.request(channel, orderInfo, (result) => {
            resolve(normalizeAppleIapResult(result, normalizedProductId))
        }, (error) => {
            reject(error || {
                code: -99,
                message: '苹果内购支付失败',
            })
        })
    })
}
/**
 * 归一化苹果内购结果,方便业务页直接拿 receipt 回传后端。
 */
function normalizeAppleIapResult(result, productId) {
    const receipt = normalizeOptionalText(result?.transactionReceipt || result?.receipt)
    return {
        productId,
        transactionDate: normalizeOptionalText(result?.transactionDate),
        transactionIdentifier: normalizeOptionalText(result?.transactionIdentifier || result?.transactionId),
        transactionReceipt: receipt,
        receiptData: receipt,
        raw: result || null,
    }
}
function normalizeOptionalText(value) {
    return typeof value === 'string' ? value.trim() : ''
}