| traditionalApp/androidPrivacy.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/manifest.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/courseDetail/courseDetail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/login/login.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/vip/vip.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/utils/appleIap.js | ●●●●● 补丁 | 查看 | 原始文档 | 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() : '' }