From ae13033b094e5dc99d21cdae2b40be80de739c5c Mon Sep 17 00:00:00 2001
From: 13404089107 <puwei@sinata.cn>
Date: 星期一, 27 四月 2026 17:08:02 +0800
Subject: [PATCH] fix
---
traditionalApp/pages/vip/vip.vue | 121 +++++++
traditionalApp/pages/login/login.vue | 78 +++-
traditionalApp/pages/courseDetail/courseDetail.vue | 234 ++++++++++++++-
traditionalApp/androidPrivacy.json | 38 ++
traditionalApp/manifest.json | 306 ++++++++++---------
traditionalApp/utils/appleIap.js | 98 ++++++
6 files changed, 673 insertions(+), 202 deletions(-)
diff --git a/traditionalApp/androidPrivacy.json b/traditionalApp/androidPrivacy.json
new file mode 100644
index 0000000..580db9d
--- /dev/null
+++ b/traditionalApp/androidPrivacy.json
@@ -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"
+ }
+ }
+}
diff --git a/traditionalApp/manifest.json b/traditionalApp/manifest.json
index 926fce1..8a818de 100644
--- a/traditionalApp/manifest.json
+++ b/traditionalApp/manifest.json
@@ -1,148 +1,160 @@
{
- "sassImplementationName" : "node-sass",
- "name" : "海诊康道",
- "appid" : "__UNI__1A90541",
- "description" : "",
- "versionName" : "1.0.1",
- "versionCode" : 101,
- "transformPx" : false,
- /* 5+App特有相关 */
- "app-plus" : {
- "usingComponents" : true,
- "nvueStyleCompiler" : "uni-app",
- "compilerVersion" : 3,
- "splashscreen" : {
- "alwaysShowBeforeRender" : true,
- "waiting" : true,
- "autoclose" : true,
- "delay" : 0
- },
- /* 模块配置 */
- "modules" : {
- "Barcode" : {},
- "VideoPlayer" : {},
- "Share" : {},
- "OAuth" : {}
- },
- /* 应用发布信息 */
- "distribute" : {
- /* android打包配置 */
- "android" : {
- "permissions" : [
- "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
- "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
- "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
- "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
- "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
- "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
- "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
- "<uses-permission android:name=\"android.permission.CAMERA\"/>",
- "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
- "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
- "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
- "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
- "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
- "<uses-feature android:name=\"android.hardware.camera\"/>",
- "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
- ],
- "minSdkVersion" : 30,
- "targetSdkVersion" : 29
- },
- /* ios打包配置 */
- "ios" : {
- "dSYMs" : false,
- "capabilities" : {
- "entitlements" : {
- "com.apple.developer.associated-domains" : [
- "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com"
- ]
- }
- }
- },
- /* SDK配置 */
- "sdkConfigs" : {
- "push" : {},
- "share" : {
- "weixin" : {
- "appid" : "wx074c1f0cf5a0ecf4",
- "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
- }
- },
- "oauth" : {
- "weixin" : {
- "appid" : "wx074c1f0cf5a0ecf4",
- "UniversalLinks" : "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
- }
- },
- "payment" : {}
- },
- "icons" : {
- "android" : {
- "hdpi" : "unpackage/res/icons/72x72.png",
- "xhdpi" : "unpackage/res/icons/96x96.png",
- "xxhdpi" : "unpackage/res/icons/144x144.png",
- "xxxhdpi" : "unpackage/res/icons/192x192.png"
- },
- "ios" : {
- "appstore" : "unpackage/res/icons/1024x1024.png",
- "ipad" : {
- "app" : "unpackage/res/icons/76x76.png",
- "app@2x" : "unpackage/res/icons/152x152.png",
- "notification" : "unpackage/res/icons/20x20.png",
- "notification@2x" : "unpackage/res/icons/40x40.png",
- "proapp@2x" : "unpackage/res/icons/167x167.png",
- "settings" : "unpackage/res/icons/29x29.png",
- "settings@2x" : "unpackage/res/icons/58x58.png",
- "spotlight" : "unpackage/res/icons/40x40.png",
- "spotlight@2x" : "unpackage/res/icons/80x80.png"
- },
- "iphone" : {
- "app@2x" : "unpackage/res/icons/120x120.png",
- "app@3x" : "unpackage/res/icons/180x180.png",
- "notification@2x" : "unpackage/res/icons/40x40.png",
- "notification@3x" : "unpackage/res/icons/60x60.png",
- "settings@2x" : "unpackage/res/icons/58x58.png",
- "settings@3x" : "unpackage/res/icons/87x87.png",
- "spotlight@2x" : "unpackage/res/icons/80x80.png",
- "spotlight@3x" : "unpackage/res/icons/120x120.png"
- }
- }
- }
- }
- },
- /* 快应用特有相关 */
- "quickapp" : {},
- /* 小程序特有相关 */
- "mp-weixin" : {
- "appid" : "wxa17e8d1331e50934",
- "setting" : {
- "urlCheck" : false,
- "minified" : true
- },
- "usingComponents" : true
- },
- "mp-alipay" : {
- "usingComponents" : true
- },
- "mp-baidu" : {
- "usingComponents" : true
- },
- "mp-toutiao" : {
- "usingComponents" : true
- },
- "uniStatistics" : {
- "enable" : false
- },
- "vueVersion" : "3",
- "permission" : {
- "scope.userLocation" : {
- "desc" : "下载文件"
- }
- },
- "h5" : {
- "unipush" : {
- "enable" : false
- }
- },
- "_spaceID" : "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8"
-}
+ "sassImplementationName": "node-sass",
+ "name": "海诊康道",
+ "appid": "__UNI__FB03FDD",
+ "description": "",
+ "versionName": "1.0.1",
+ "versionCode": 101,
+ "transformPx": false,
+ /* 5+App特有相关 */
+ "app-plus": {
+ "usingComponents": true,
+ "nvueStyleCompiler": "uni-app",
+ "compilerVersion": 3,
+ "splashscreen": {
+ "alwaysShowBeforeRender": true,
+ "waiting": true,
+ "autoclose": true,
+ "delay": 0
+ },
+ /* 模块配置 */
+ "modules": {
+ "Barcode": {},
+ "VideoPlayer": {},
+ "Share": {},
+ "OAuth": {},
+ "Payment": {}
+ },
+ /* 应用发布信息 */
+ "distribute": {
+ /* android打包配置 */
+ "android": {
+ "permissions": [
+ "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+ "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+ "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+ "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+ "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+ "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+ "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+ "<uses-feature android:name=\"android.hardware.camera\"/>",
+ "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+ ],
+ "minSdkVersion": 28,
+ "targetSdkVersion": 30
+ },
+ /* ios打包配置 */
+ "ios": {
+ "dSYMs": false,
+ "capabilities": {
+ "entitlements": {
+ "com.apple.developer.associated-domains": [
+ "applinks:static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com"
+ ]
+ }
+ }
+ },
+ /* SDK配置 */
+ "sdkConfigs": {
+ "push": {},
+ "share": {
+ "weixin": {
+ "appid": "wx074c1f0cf5a0ecf4",
+ "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
+ }
+ },
+ "oauth": {
+ "weixin": {
+ "appid": "wx074c1f0cf5a0ecf4",
+ "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
+ }
+ },
+ "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": {
+ "hdpi": "unpackage/res/icons/72x72.png",
+ "xhdpi": "unpackage/res/icons/96x96.png",
+ "xxhdpi": "unpackage/res/icons/144x144.png",
+ "xxxhdpi": "unpackage/res/icons/192x192.png"
+ },
+ "ios": {
+ "appstore": "unpackage/res/icons/1024x1024.png",
+ "ipad": {
+ "app": "unpackage/res/icons/76x76.png",
+ "app@2x": "unpackage/res/icons/152x152.png",
+ "notification": "unpackage/res/icons/20x20.png",
+ "notification@2x": "unpackage/res/icons/40x40.png",
+ "proapp@2x": "unpackage/res/icons/167x167.png",
+ "settings": "unpackage/res/icons/29x29.png",
+ "settings@2x": "unpackage/res/icons/58x58.png",
+ "spotlight": "unpackage/res/icons/40x40.png",
+ "spotlight@2x": "unpackage/res/icons/80x80.png"
+ },
+ "iphone": {
+ "app@2x": "unpackage/res/icons/120x120.png",
+ "app@3x": "unpackage/res/icons/180x180.png",
+ "notification@2x": "unpackage/res/icons/40x40.png",
+ "notification@3x": "unpackage/res/icons/60x60.png",
+ "settings@2x": "unpackage/res/icons/58x58.png",
+ "settings@3x": "unpackage/res/icons/87x87.png",
+ "spotlight@2x": "unpackage/res/icons/80x80.png",
+ "spotlight@3x": "unpackage/res/icons/120x120.png"
+ }
+ }
+ },
+ "splashscreen": {
+ "androidStyle": "common",
+ "useOriginalMsgbox": true
+ }
+ }
+ },
+ /* 快应用特有相关 */
+ "quickapp": {},
+ /* 小程序特有相关 */
+ "mp-weixin": {
+ "appid": "wxa17e8d1331e50934",
+ "setting": {
+ "urlCheck": false,
+ "minified": true
+ },
+ "usingComponents": true
+ },
+ "mp-alipay": {
+ "usingComponents": true
+ },
+ "mp-baidu": {
+ "usingComponents": true
+ },
+ "mp-toutiao": {
+ "usingComponents": true
+ },
+ "uniStatistics": {
+ "enable": false
+ },
+ "vueVersion": "3",
+ "permission": {
+ "scope.userLocation": {
+ "desc": "下载文件"
+ }
+ },
+ "h5": {
+ "unipush": {
+ "enable": false
+ }
+ },
+ "_spaceID": "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8"
+}
\ No newline at end of file
diff --git a/traditionalApp/pages/courseDetail/courseDetail.vue b/traditionalApp/pages/courseDetail/courseDetail.vue
index c639755..c3c233f 100644
--- a/traditionalApp/pages/courseDetail/courseDetail.vue
+++ b/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,21 +252,31 @@
<view class="payment-methods">
<text class="method-title">选择支付方式</text>
- <view class="method-item" @tap="selectPaymentMethod('wechat')">
+ <view v-if="isIosApp" class="method-item" @tap="selectPaymentMethod('appleiap')">
<view class="method-left">
- <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image>
- <text class="method-name">微信支付</text>
+ <view class="pay-icon pay-icon--apple">A</view>
+ <text class="method-name">App Store 支付</text>
</view>
- <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
+ <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
</view>
- <view class="method-item" @tap="selectPaymentMethod('alipay')">
- <view class="method-left">
- <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image>
- <text class="method-name">支付宝支付</text>
+ <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>
+ <text class="method-name">微信支付</text>
+ </view>
+ <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
</view>
- <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
- </view>
+
+ <view class="method-item" @tap="selectPaymentMethod('alipay')">
+ <view class="method-left">
+ <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image>
+ <text class="method-name">支付宝支付</text>
+ </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 = {
- paymentMethod: selectedPaymentMethod.value === 'wechat' ? 'WECHAT' : 'ALIPAY',
+ 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;
diff --git a/traditionalApp/pages/login/login.vue b/traditionalApp/pages/login/login.vue
index c056a92..765f665 100644
--- a/traditionalApp/pages/login/login.vue
+++ b/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) => {
diff --git a/traditionalApp/pages/vip/vip.vue b/traditionalApp/pages/vip/vip.vue
index 896a0bf..5d29164 100644
--- a/traditionalApp/pages/vip/vip.vue
+++ b/traditionalApp/pages/vip/vip.vue
@@ -115,25 +115,37 @@
<view class="payment-methods">
<text class="method-title">选择支付方式</text>
- <view class="method-item" @tap="selectPayment('wechat')">
+ <view v-if="isIosApp" class="method-item" @tap="selectPayment('appleiap')">
<view class="method-left">
- <image class="pay-icon" src="/static/image/vip/weixinPay.png" mode="aspectFit"></image>
- <text class="method-name">微信支付</text>
+ <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 === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
+ <image class="radio-img" :src="selectedPaymentMethod === 'appleiap' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
</view>
</view>
- <view class="method-item" @tap="selectPayment('alipay')">
- <view class="method-left">
- <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image>
- <text class="method-name">支付宝支付</text>
+ <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>
+ <text class="method-name">微信支付</text>
+ </view>
+ <view class="method-radio">
+ <image class="radio-img" :src="selectedPaymentMethod === 'wechat' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
+ </view>
</view>
- <view class="method-radio">
- <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
+
+ <view class="method-item" @tap="selectPayment('alipay')">
+ <view class="method-left">
+ <image class="pay-icon" src="/static/image/vip/aliPay.png" mode="aspectFit"></image>
+ <text class="method-name">支付宝支付</text>
+ </view>
+ <view class="method-radio">
+ <image class="radio-img" :src="selectedPaymentMethod === 'alipay' ? '/static/image/vip/check.png' : '/static/image/vip/uncheck.png'" mode="aspectFit"></image>
+ </view>
</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 {
diff --git a/traditionalApp/utils/appleIap.js b/traditionalApp/utils/appleIap.js
new file mode 100644
index 0000000..1d67438
--- /dev/null
+++ b/traditionalApp/utils/appleIap.js
@@ -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() : ''
+}
--
Gitblit v1.7.1