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