From 907bca509a96bbb7e028fc4dd4ddadda432b4fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=B7=E5=B0=8F=E5=89=8D?= Date: Wed, 21 Jan 2026 15:54:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0ios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MeAgent/ios/app/app.entitlements | 4 + MeAgent/navigation/Screens.js | 10 + MeAgent/src/screens/Profile/ProfileScreen.js | 2 + .../Subscription/SubscriptionScreen.js | 611 ++++++++++++++++++ MeAgent/src/screens/Subscription/index.js | 2 + MeAgent/src/services/api.js | 70 ++ MeAgent/src/services/authService.js | 23 +- MeAgent/src/services/iapService.js | 441 +++++++++++++ app.py | 440 +++++++++++++ migrations/add_apple_payment_fields.sql | 59 ++ .../add_apple_payment_fields_mysql57.sql | 76 +++ src/views/AgentChat/index.js | 13 + 12 files changed, 1745 insertions(+), 6 deletions(-) create mode 100644 MeAgent/src/screens/Subscription/SubscriptionScreen.js create mode 100644 MeAgent/src/screens/Subscription/index.js create mode 100644 MeAgent/src/services/iapService.js create mode 100644 migrations/add_apple_payment_fields.sql create mode 100644 migrations/add_apple_payment_fields_mysql57.sql diff --git a/MeAgent/ios/app/app.entitlements b/MeAgent/ios/app/app.entitlements index 018a6e20..0675789a 100644 --- a/MeAgent/ios/app/app.entitlements +++ b/MeAgent/ios/app/app.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.in-app-payments + + merchant.com.valuefrontier.meagent + \ No newline at end of file diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js index bd7185a5..d3d7897c 100644 --- a/MeAgent/navigation/Screens.js +++ b/MeAgent/navigation/Screens.js @@ -62,6 +62,9 @@ import MemberList from "../src/screens/Community/MemberList"; // 新个人中心页面 import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile"; +// 订阅管理页面 +import { SubscriptionScreen } from "../src/screens/Subscription"; + // 认证页面 import { LoginScreen } from "../src/screens/Auth"; @@ -510,6 +513,13 @@ function NewProfileStack(props) { cardStyle: { backgroundColor: "#0A0A0F" }, }} /> + ); } diff --git a/MeAgent/src/screens/Profile/ProfileScreen.js b/MeAgent/src/screens/Profile/ProfileScreen.js index b89e6f83..68d8d306 100644 --- a/MeAgent/src/screens/Profile/ProfileScreen.js +++ b/MeAgent/src/screens/Profile/ProfileScreen.js @@ -354,6 +354,8 @@ const ProfileScreen = () => { const handleMenuItemPress = useCallback((item) => { if (item.route === 'WatchlistDrawer') { navigation.navigate('WatchlistDrawer'); + } else if (item.route === 'Subscription') { + navigation.navigate('Subscription'); } else if (item.route === 'Settings') { navigation.navigate('SettingsDrawer'); } else if (item.route === 'About') { diff --git a/MeAgent/src/screens/Subscription/SubscriptionScreen.js b/MeAgent/src/screens/Subscription/SubscriptionScreen.js new file mode 100644 index 00000000..21d6c2fe --- /dev/null +++ b/MeAgent/src/screens/Subscription/SubscriptionScreen.js @@ -0,0 +1,611 @@ +/** + * 订阅管理页面 + * iOS 苹果内购订阅功能 + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { + StyleSheet, + ScrollView, + Alert, + Platform, + Linking, +} from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, + Pressable, + Spinner, + Button, + Badge, + Divider, +} from 'native-base'; +import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useNavigation } from '@react-navigation/native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { useAuth } from '../../contexts/AuthContext'; +import { IAP_PRODUCTS, getProductByPlanAndCycle, iapService } from '../../services/iapService'; + +// 订阅计划配置 +const SUBSCRIPTION_PLANS = [ + { + name: 'pro', + displayName: 'Pro 专业版', + icon: 'diamond-outline', + color: '#7C3AED', + gradientColors: ['#7C3AED', '#A855F7'], + description: '专业投资者的首选', + features: [ + '事件关联股票深度分析', + '历史事件智能对比复盘', + '事件概念关联与挖掘', + '概念板块个股追踪', + '概念深度研报与解读', + '个股异动实时预警', + ], + pricingOptions: [ + { cycleKey: 'monthly', label: '月付', months: 1, price: 299, discount: 0 }, + { cycleKey: 'quarterly', label: '季付', months: 3, price: 799, originalPrice: 897, discount: 11 }, + { cycleKey: 'semiannual', label: '半年付', months: 6, price: 1499, originalPrice: 1794, discount: 16 }, + { cycleKey: 'yearly', label: '年付', months: 12, price: 2699, originalPrice: 3588, discount: 25 }, + ], + }, + { + name: 'max', + displayName: 'Max 旗舰版', + icon: 'crown-outline', + color: '#D4AF37', + gradientColors: ['#D4AF37', '#F5D85A'], + description: '重度用户的极致体验', + popular: true, + features: [ + '包含Pro版全部功能', + '事件传导链路智能分析', + '概念演变时间轴追溯', + '个股全方位深度研究', + '价小前投研助手无限使用', + '新功能优先体验权', + '专属客服一对一服务', + ], + pricingOptions: [ + { cycleKey: 'monthly', label: '月付', months: 1, price: 599, discount: 0 }, + { cycleKey: 'quarterly', label: '季付', months: 3, price: 1599, originalPrice: 1797, discount: 11 }, + { cycleKey: 'semiannual', label: '半年付', months: 6, price: 2999, originalPrice: 3594, discount: 17 }, + { cycleKey: 'yearly', label: '年付', months: 12, price: 5399, originalPrice: 7188, discount: 25 }, + ], + }, +]; + +// 周期选择按钮组件 +const CycleSelector = ({ cycles, selectedCycle, onSelect }) => { + return ( + + {cycles.map((cycle) => ( + onSelect(cycle.cycleKey)} + mb={2} + > + + + {cycle.label} + + {cycle.discount > 0 && ( + + + 省{cycle.discount}% + + + )} + + + ))} + + ); +}; + +// 价格显示组件 +const PriceDisplay = ({ plan, selectedCycle }) => { + const option = plan.pricingOptions.find(o => o.cycleKey === selectedCycle); + if (!option) return null; + + return ( + + + ¥ + {option.price} + /{option.label} + + {option.originalPrice && ( + + + 原价 ¥{option.originalPrice} + + + 立省 ¥{option.originalPrice - option.price} + + + )} + + 相当于 ¥{(option.price / option.months).toFixed(0)}/月 + + + ); +}; + +// 功能列表组件 +const FeatureList = ({ features, color }) => { + return ( + + {features.map((feature, index) => ( + + + + {feature} + + + ))} + + ); +}; + +// 订阅卡片组件 +const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPlan, loading }) => { + const currentOption = plan.pricingOptions.find(o => o.cycleKey === selectedCycle); + + return ( + + {/* 热门标签 */} + {plan.popular && ( + + + + 最受欢迎 + + + + )} + + {/* 卡片标题区域 */} + + + + + + {plan.displayName} + + + {plan.description} + + + + + + {/* 卡片内容 */} + + {/* 周期选择 */} + + + {/* 价格显示 */} + + + {/* 功能列表 */} + + + {/* 订阅按钮 */} + onSubscribe(plan, selectedCycle)} + disabled={loading} + > + + {loading ? ( + + ) : ( + + {isCurrentPlan ? '续费' : `订阅${plan.displayName}`} + + )} + + + + {/* 自动续费说明 */} + + 自动续费,可随时取消 + + + + ); +}; + +// 当前订阅状态卡片 +const CurrentSubscriptionCard = ({ subscription, onManage }) => { + if (!subscription || !subscription.is_active) { + return null; + } + + const getPlanInfo = () => { + if (subscription.type === 'max') { + return { name: 'Max 旗舰版', color: '#D4AF37', icon: 'crown-outline' }; + } + if (subscription.type === 'pro') { + return { name: 'Pro 专业版', color: '#7C3AED', icon: 'diamond-outline' }; + } + return { name: '会员', color: '#3B82F6', icon: 'star-outline' }; + }; + + const planInfo = getPlanInfo(); + + return ( + + + + + + + + + + {planInfo.name} + + + 使用中 + + + {subscription.end_date && ( + + 到期时间: {new Date(subscription.end_date).toLocaleDateString('zh-CN')} + + )} + + + + 管理 + + + + ); +}; + +// 主页面组件 +const SubscriptionScreen = () => { + const navigation = useNavigation(); + const { user, subscription, refreshUser } = useAuth(); + + const [selectedCycles, setSelectedCycles] = useState({ + pro: 'yearly', + max: 'yearly', + }); + const [loading, setLoading] = useState(false); + const [loadingPlan, setLoadingPlan] = useState(null); + + // 初始化 IAP 服务 + useEffect(() => { + if (Platform.OS === 'ios') { + iapService.initialize(); + } + + return () => { + iapService.cleanup(); + }; + }, []); + + // 处理周期选择 + const handleCycleSelect = useCallback((planName, cycle) => { + setSelectedCycles(prev => ({ + ...prev, + [planName]: cycle, + })); + }, []); + + // 处理订阅 + const handleSubscribe = useCallback(async (plan, cycle) => { + if (!user) { + Alert.alert( + '请先登录', + '登录后即可订阅会员服务', + [ + { text: '取消', style: 'cancel' }, + { + text: '去登录', + onPress: () => navigation.getParent()?.navigate('Login'), + }, + ] + ); + return; + } + + if (Platform.OS !== 'ios') { + Alert.alert('提示', '苹果支付仅支持 iOS 设备'); + return; + } + + const productInfo = getProductByPlanAndCycle(plan.name, cycle); + if (!productInfo) { + Alert.alert('错误', '无效的订阅选项'); + return; + } + + setLoadingPlan(plan.name); + setLoading(true); + + try { + // 发起购买 + const purchase = await iapService.purchaseProduct(productInfo.productId); + + // 验证收据并激活订阅 + if (purchase && purchase.transactionReceipt) { + const result = await iapService.verifyAndActivateSubscription( + purchase.transactionReceipt, + productInfo.productId + ); + + if (result.success) { + // 完成交易(传入完整的 purchase 对象) + await iapService.finishPurchase(purchase); + + // 刷新用户信息 + await refreshUser(); + + Alert.alert( + '订阅成功', + `您已成功订阅${plan.displayName}!`, + [{ text: '好的', onPress: () => navigation.goBack() }] + ); + } + } + } catch (error) { + console.error('[SubscriptionScreen] 订阅失败:', error); + Alert.alert( + '订阅失败', + error.message || '请稍后重试', + [{ text: '确定' }] + ); + } finally { + setLoading(false); + setLoadingPlan(null); + } + }, [user, navigation, refreshUser]); + + // 管理订阅(打开苹果订阅管理页面) + const handleManageSubscription = useCallback(() => { + // iOS 订阅管理页面 URL + Linking.openURL('https://apps.apple.com/account/subscriptions'); + }, []); + + // 恢复购买 + const handleRestorePurchases = useCallback(async () => { + if (Platform.OS !== 'ios') { + Alert.alert('提示', '恢复购买仅支持 iOS 设备'); + return; + } + + setLoading(true); + try { + const purchases = await iapService.restorePurchases(); + + if (purchases && purchases.length > 0) { + // 验证最新的购买记录 + const latestPurchase = purchases[0]; + if (latestPurchase.transactionReceipt) { + const productInfo = Object.values(IAP_PRODUCTS).find( + p => p.productId === latestPurchase.productId + ); + + if (productInfo) { + await iapService.verifyAndActivateSubscription( + latestPurchase.transactionReceipt, + latestPurchase.productId + ); + + await refreshUser(); + Alert.alert('恢复成功', '您的订阅已恢复'); + } + } + } else { + Alert.alert('提示', '没有找到可恢复的购买记录'); + } + } catch (error) { + console.error('[SubscriptionScreen] 恢复购买失败:', error); + Alert.alert('恢复失败', error.message || '请稍后重试'); + } finally { + setLoading(false); + } + }, [refreshUser]); + + return ( + + + {/* 背景装饰 */} + + + + {/* 标题栏 */} + + navigation.goBack()} hitSlop={10}> + + + + 订阅管理 + + {/* 占位,保持标题居中 */} + + + {/* 当前订阅状态 */} + + + {/* 标题 */} + + + 订阅方案 + + + 开启智能投资之旅 + + + 选择适合您的会员计划,解锁全部高级功能 + + + + {/* 订阅卡片 */} + {SUBSCRIPTION_PLANS.map((plan) => ( + handleCycleSelect(plan.name, cycle)} + onSubscribe={handleSubscribe} + isCurrentPlan={subscription?.type === plan.name && subscription?.is_active} + loading={loading && loadingPlan === plan.name} + /> + ))} + + {/* 恢复购买按钮 */} + + + + + 恢复购买 + + + + + {/* 说明文字 */} + + + 订阅将通过您的 Apple ID 账户付款 + + + 除非在当前订阅期结束前至少 24 小时关闭自动续订,否则将自动续订 + + + 您可以在 App Store 账户设置中管理和取消订阅 + + + + Linking.openURL('https://valuefrontier.cn/htmls/pro-member-agreement.html')}> + + 服务协议 + + + Linking.openURL('https://valuefrontier.cn/privacy')}> + + 隐私政策 + + + + + + {/* 底部间距 */} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + cardHeader: { + paddingVertical: 16, + paddingHorizontal: 20, + }, + popularBadge: { + paddingHorizontal: 12, + paddingVertical: 4, + borderBottomLeftRadius: 10, + }, + subscribeButton: { + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default SubscriptionScreen; diff --git a/MeAgent/src/screens/Subscription/index.js b/MeAgent/src/screens/Subscription/index.js new file mode 100644 index 00000000..f60859b6 --- /dev/null +++ b/MeAgent/src/screens/Subscription/index.js @@ -0,0 +1,2 @@ +export { default as SubscriptionScreen } from './SubscriptionScreen'; +export { default } from './SubscriptionScreen'; diff --git a/MeAgent/src/services/api.js b/MeAgent/src/services/api.js index a5f280c9..866d1653 100644 --- a/MeAgent/src/services/api.js +++ b/MeAgent/src/services/api.js @@ -13,6 +13,49 @@ export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shangha // Token 存储键名(与 authService 保持一致) const ACCESS_TOKEN_KEY = '@auth_access_token'; +// 🍪 Cookie 存储键名(用于手动管理 Cookie) +const SESSION_COOKIE_KEY = '@session_cookie'; + +/** + * 从响应头中提取 Set-Cookie + */ +const extractSetCookie = (response) => { + // React Native 的 fetch 可能无法直接获取 Set-Cookie + // 但我们尝试获取 + const setCookie = response.headers.get('set-cookie'); + if (setCookie) { + console.log('[API] 收到 Set-Cookie:', setCookie.substring(0, 50) + '...'); + return setCookie; + } + return null; +}; + +/** + * 保存 Cookie 到 AsyncStorage + */ +const saveCookie = async (cookie) => { + if (cookie) { + try { + await AsyncStorage.setItem(SESSION_COOKIE_KEY, cookie); + console.log('[API] Cookie 已保存'); + } catch (error) { + console.error('[API] 保存 Cookie 失败:', error); + } + } +}; + +/** + * 从 AsyncStorage 获取 Cookie + */ +const getCookie = async () => { + try { + return await AsyncStorage.getItem(SESSION_COOKIE_KEY); + } catch (error) { + console.error('[API] 读取 Cookie 失败:', error); + return null; + } +}; + /** * 通用 API 请求函数 * @param {string} url - API 路径 @@ -29,6 +72,9 @@ export const apiRequest = async (url, options = {}) => { // 获取存储的 access_token const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY); + // 🍪 获取存储的 Cookie + const storedCookie = await getCookie(); + // 构建请求头 const headers = { 'Content-Type': 'application/json', @@ -40,6 +86,12 @@ export const apiRequest = async (url, options = {}) => { headers['Authorization'] = `Bearer ${accessToken}`; } + // 🍪 手动添加 Cookie header(React Native 真机可能需要) + if (storedCookie) { + headers['Cookie'] = storedCookie; + console.log('[API] 添加 Cookie header'); + } + const response = await fetch(fullUrl, { ...options, headers, @@ -47,6 +99,12 @@ export const apiRequest = async (url, options = {}) => { credentials: 'include', }); + // 🍪 尝试从响应中提取并保存 Cookie + const setCookie = extractSetCookie(response); + if (setCookie) { + await saveCookie(setCookie); + } + // 处理 403 权限不足的情况 if (response.status === 403) { const errorData = await response.json().catch(() => ({})); @@ -75,4 +133,16 @@ export const apiRequest = async (url, options = {}) => { } }; +/** + * 清除存储的 Cookie(退出登录时调用) + */ +export const clearSessionCookie = async () => { + try { + await AsyncStorage.removeItem(SESSION_COOKIE_KEY); + console.log('[API] Cookie 已清除'); + } catch (error) { + console.error('[API] 清除 Cookie 失败:', error); + } +}; + export default apiRequest; diff --git a/MeAgent/src/services/authService.js b/MeAgent/src/services/authService.js index 08054e10..0d601c78 100644 --- a/MeAgent/src/services/authService.js +++ b/MeAgent/src/services/authService.js @@ -4,7 +4,7 @@ */ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { apiRequest } from './api'; +import { apiRequest, clearSessionCookie } from './api'; // 存储键名 const STORAGE_KEYS = { @@ -89,18 +89,27 @@ export const authService = { */ getCurrentUser: async (clearOnFail = false) => { try { - const response = await apiRequest('/api/account/user-info'); + // ✅ 修复:使用正确的 API 端点 /api/auth/session + const response = await apiRequest('/api/auth/session'); - if (response.success && response.user) { + // 🔍 调试日志:打印完整响应 + console.log('[AuthService] /api/auth/session 响应:', JSON.stringify(response, null, 2)); + + if (response.success && response.isAuthenticated && response.user) { // 更新本地存储 await AsyncStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(response.user)); await AsyncStorage.setItem(STORAGE_KEYS.IS_LOGGED_IN, 'true'); + + // 🔍 调试日志:打印用户订阅信息 + console.log('[AuthService] 用户订阅类型:', response.user.subscription_type); + console.log('[AuthService] 用户订阅状态:', response.user.subscription_status); + return response.user; } - // 如果返回 401/403,说明 token 无效 - if (response.status === 401 || response.status === 403) { - console.warn('[AuthService] Token 无效,需要重新登录'); + // 未认证或返回 401/403 + if (!response.isAuthenticated || response.status === 401 || response.status === 403) { + console.warn('[AuthService] 未认证或 Token 无效,需要重新登录'); if (clearOnFail) { await authService.clearLocalAuth(); } @@ -142,6 +151,8 @@ export const authService = { await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO); await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN); await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN); + // 🍪 同时清除 Cookie + await clearSessionCookie(); } catch (error) { console.error('[AuthService] 清除本地状态失败:', error); } diff --git a/MeAgent/src/services/iapService.js b/MeAgent/src/services/iapService.js new file mode 100644 index 00000000..52a2913c --- /dev/null +++ b/MeAgent/src/services/iapService.js @@ -0,0 +1,441 @@ +/** + * 苹果内购服务 (In-App Purchase Service) + * 处理 iOS 订阅购买逻辑 + * + * 使用 react-native-iap 库实现苹果支付 + */ + +import { Platform } from 'react-native'; +import { + initConnection, + endConnection, + getProducts, + getSubscriptions, + requestSubscription, + getAvailablePurchases, + finishTransaction, + purchaseUpdatedListener, + purchaseErrorListener, + flushFailedPurchasesCachedAsPendingAndroid, +} from 'react-native-iap'; +import { API_BASE_URL } from './api'; + +// 苹果内购产品 ID 配置 +// 格式: com.valuefrontier.meagent.{plan}_{cycle} +export const IAP_PRODUCTS = { + // Pro 专业版 + pro_monthly: { + productId: 'com.valuefrontier.meagent.pro_monthly', + plan: 'pro', + cycle: 'monthly', + months: 1, + price: 299, // 人民币价格(供显示,实际以苹果返回为准) + }, + pro_quarterly: { + productId: 'com.valuefrontier.meagent.pro_quarterly', + plan: 'pro', + cycle: 'quarterly', + months: 3, + price: 799, + }, + pro_semiannual: { + productId: 'com.valuefrontier.meagent.pro_semiannual', + plan: 'pro', + cycle: 'semiannual', + months: 6, + price: 1499, + }, + pro_yearly: { + productId: 'com.valuefrontier.meagent.pro_yearly', + plan: 'pro', + cycle: 'yearly', + months: 12, + price: 2699, + }, + // Max 旗舰版 + max_monthly: { + productId: 'com.valuefrontier.meagent.max_monthly', + plan: 'max', + cycle: 'monthly', + months: 1, + price: 599, + }, + max_quarterly: { + productId: 'com.valuefrontier.meagent.max_quarterly', + plan: 'max', + cycle: 'quarterly', + months: 3, + price: 1599, + }, + max_semiannual: { + productId: 'com.valuefrontier.meagent.max_semiannual', + plan: 'max', + cycle: 'semiannual', + months: 6, + price: 2999, + }, + max_yearly: { + productId: 'com.valuefrontier.meagent.max_yearly', + plan: 'max', + cycle: 'yearly', + months: 12, + price: 5399, + }, +}; + +// 获取所有产品 ID 列表 +export const getAllProductIds = () => { + return Object.values(IAP_PRODUCTS).map(p => p.productId); +}; + +// 根据计划和周期获取产品信息 +export const getProductByPlanAndCycle = (plan, cycle) => { + const key = `${plan}_${cycle}`; + return IAP_PRODUCTS[key] || null; +}; + +// 根据产品 ID 获取产品信息 +export const getProductById = (productId) => { + return Object.values(IAP_PRODUCTS).find(p => p.productId === productId) || null; +}; + +/** + * IAP 服务类 + * 使用 react-native-iap 实现苹果内购功能 + */ +class IAPService { + constructor() { + this.isInitialized = false; + this.products = []; + this.subscriptions = []; + this.purchaseUpdateSubscription = null; + this.purchaseErrorSubscription = null; + } + + /** + * 初始化 IAP 服务 + * @returns {Promise} + */ + async initialize() { + if (Platform.OS !== 'ios') { + console.log('[IAPService] 非 iOS 平台,跳过初始化'); + return false; + } + + if (this.isInitialized) { + console.log('[IAPService] 已经初始化,跳过'); + return true; + } + + try { + // 初始化连接 + const result = await initConnection(); + console.log('[IAPService] IAP 连接初始化成功:', result); + + // 获取订阅产品信息 + const productIds = getAllProductIds(); + console.log('[IAPService] 获取产品列表:', productIds); + + try { + // 对于订阅类产品,使用 getSubscriptions + this.subscriptions = await getSubscriptions({ skus: productIds }); + console.log('[IAPService] 获取到订阅产品:', this.subscriptions.length); + } catch (productError) { + console.warn('[IAPService] 获取产品信息失败(可能产品未在 App Store Connect 配置):', productError); + // 继续初始化,不阻止流程 + } + + // 设置购买监听器 + this.setupPurchaseListeners(); + + this.isInitialized = true; + console.log('[IAPService] IAP 服务初始化完成'); + return true; + } catch (error) { + console.error('[IAPService] 初始化失败:', error); + return false; + } + } + + /** + * 设置购买事件监听器 + */ + setupPurchaseListeners() { + // 移除旧的监听器 + if (this.purchaseUpdateSubscription) { + this.purchaseUpdateSubscription.remove(); + } + if (this.purchaseErrorSubscription) { + this.purchaseErrorSubscription.remove(); + } + + // 购买成功/更新监听器 + this.purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { + console.log('[IAPService] 购买更新:', purchase); + + if (purchase.transactionReceipt) { + try { + // 验证收据 + const verifyResult = await this.verifyAndActivateSubscription( + purchase.transactionReceipt, + purchase.productId + ); + + if (verifyResult.success) { + // 完成交易 + await finishTransaction({ purchase, isConsumable: false }); + console.log('[IAPService] 交易完成'); + } + } catch (error) { + console.error('[IAPService] 处理购买失败:', error); + } + } + }); + + // 购买错误监听器 + this.purchaseErrorSubscription = purchaseErrorListener((error) => { + console.error('[IAPService] 购买错误:', error); + }); + + console.log('[IAPService] 购买监听器已设置'); + } + + /** + * 获取可用产品列表(从苹果服务器获取最新价格) + * @returns {Promise} + */ + async getAvailableProducts() { + if (!this.isInitialized) { + await this.initialize(); + } + + // 如果成功获取了苹果的产品信息,返回真实数据 + if (this.subscriptions && this.subscriptions.length > 0) { + return this.subscriptions.map(subscription => { + const localProduct = getProductById(subscription.productId); + return { + ...localProduct, + productId: subscription.productId, + localizedPrice: subscription.localizedPrice, + price: subscription.price, + currency: subscription.currency, + title: subscription.title, + description: subscription.description, + }; + }); + } + + // 否则返回本地配置的产品信息(用于测试或产品未配置时) + console.log('[IAPService] 使用本地产品配置'); + return Object.values(IAP_PRODUCTS).map(product => ({ + ...product, + localizedPrice: `¥${product.price}`, + currency: 'CNY', + })); + } + + /** + * 发起订阅购买请求 + * @param {string} productId - 产品 ID + * @returns {Promise} 购买结果 + */ + async purchaseProduct(productId) { + if (Platform.OS !== 'ios') { + throw new Error('苹果支付仅支持 iOS 设备'); + } + + const productInfo = getProductById(productId); + if (!productInfo) { + throw new Error('无效的产品 ID'); + } + + if (!this.isInitialized) { + await this.initialize(); + } + + try { + console.log('[IAPService] 发起订阅购买:', productId); + + // 对于订阅类产品,使用 requestSubscription + const purchase = await requestSubscription({ + sku: productId, + andDangerouslyFinishTransactionAutomaticallyIOS: false, // 手动完成交易 + }); + + console.log('[IAPService] 购买请求已发起:', purchase); + return purchase; + } catch (error) { + console.error('[IAPService] 购买失败:', error); + + // 处理用户取消的情况 + if (error.code === 'E_USER_CANCELLED') { + throw new Error('您取消了购买'); + } + + // 处理其他错误 + throw error; + } + } + + /** + * 验证收据并激活订阅 + * @param {string} receiptData - 苹果收据数据 + * @param {string} productId - 产品 ID + * @returns {Promise} + */ + async verifyAndActivateSubscription(receiptData, productId) { + const productInfo = getProductById(productId); + if (!productInfo) { + throw new Error('无效的产品 ID'); + } + + try { + console.log('[IAPService] 发送收据到后端验证...'); + + const response = await fetch(`${API_BASE_URL}/api/payment/apple/verify-receipt`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + receipt_data: receiptData, + product_id: productId, + plan_name: productInfo.plan, + billing_cycle: productInfo.cycle, + }), + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || '收据验证失败'); + } + + console.log('[IAPService] 收据验证成功:', data); + return data; + } catch (error) { + console.error('[IAPService] 收据验证失败:', error); + throw error; + } + } + + /** + * 恢复购买(用于重新安装后恢复订阅) + * @returns {Promise} 恢复的购买记录 + */ + async restorePurchases() { + if (Platform.OS !== 'ios') { + throw new Error('恢复购买仅支持 iOS 设备'); + } + + if (!this.isInitialized) { + await this.initialize(); + } + + try { + console.log('[IAPService] 开始恢复购买...'); + + const purchases = await getAvailablePurchases(); + console.log('[IAPService] 找到购买记录:', purchases.length); + + // 筛选出我们的产品 + const ourProductIds = getAllProductIds(); + const ourPurchases = purchases.filter(p => ourProductIds.includes(p.productId)); + + console.log('[IAPService] 匹配的购买记录:', ourPurchases.length); + + // 如果有购买记录,验证最新的一条 + if (ourPurchases.length > 0) { + // 按购买时间排序,取最新的 + ourPurchases.sort((a, b) => { + const timeA = a.transactionDate ? new Date(a.transactionDate).getTime() : 0; + const timeB = b.transactionDate ? new Date(b.transactionDate).getTime() : 0; + return timeB - timeA; + }); + + const latestPurchase = ourPurchases[0]; + + if (latestPurchase.transactionReceipt) { + // 向后端验证并激活 + await this.verifyAndActivateSubscription( + latestPurchase.transactionReceipt, + latestPurchase.productId + ); + } + } + + return ourPurchases; + } catch (error) { + console.error('[IAPService] 恢复购买失败:', error); + throw error; + } + } + + /** + * 完成交易(确认购买已处理) + * @param {Object} purchase - 购买对象 + */ + async finishPurchase(purchase) { + try { + await finishTransaction({ purchase, isConsumable: false }); + console.log('[IAPService] 交易已完成:', purchase.transactionId); + } catch (error) { + console.error('[IAPService] 完成交易失败:', error); + } + } + + /** + * 检查是否有未完成的交易 + * @returns {Promise} + */ + async checkPendingPurchases() { + if (Platform.OS !== 'ios') { + return []; + } + + try { + const purchases = await getAvailablePurchases(); + const ourProductIds = getAllProductIds(); + + return purchases.filter(p => ourProductIds.includes(p.productId)); + } catch (error) { + console.error('[IAPService] 检查待处理购买失败:', error); + return []; + } + } + + /** + * 清理资源 + */ + async cleanup() { + try { + // 移除监听器 + if (this.purchaseUpdateSubscription) { + this.purchaseUpdateSubscription.remove(); + this.purchaseUpdateSubscription = null; + } + if (this.purchaseErrorSubscription) { + this.purchaseErrorSubscription.remove(); + this.purchaseErrorSubscription = null; + } + + // 断开连接 + if (this.isInitialized) { + await endConnection(); + } + + this.isInitialized = false; + this.products = []; + this.subscriptions = []; + + console.log('[IAPService] 资源已清理'); + } catch (error) { + console.error('[IAPService] 清理资源失败:', error); + } + } +} + +// 导出单例 +export const iapService = new IAPService(); +export default iapService; diff --git a/app.py b/app.py index e3593e31..2e0fae7e 100755 --- a/app.py +++ b/app.py @@ -1646,6 +1646,8 @@ class PaymentOrder(db.Model): payment_method = db.Column(db.String(20), default='wechat') # 支付方式: wechat/alipay wechat_order_id = db.Column(db.String(64), nullable=True) # 微信交易号 alipay_trade_no = db.Column(db.String(64), nullable=True) # 支付宝交易号 + transaction_id = db.Column(db.String(64), nullable=True, index=True) # 苹果交易ID (Apple IAP) + payment_source = db.Column(db.String(20), default='web') # 支付来源: web/ios/android prepay_id = db.Column(db.String(64), nullable=True) qr_code_url = db.Column(db.String(200), nullable=True) # 微信支付二维码URL pay_url = db.Column(db.String(2000), nullable=True) # 支付宝支付链接(较长) @@ -3721,6 +3723,444 @@ def check_alipay_order_status_by_no(order_no): return jsonify({'success': False, 'error': '查询失败'}), 500 +# ===================== 苹果支付 (Apple In-App Purchase) ===================== + +# 苹果收据验证 URL +APPLE_VERIFY_RECEIPT_URL_SANDBOX = 'https://sandbox.itunes.apple.com/verifyReceipt' +APPLE_VERIFY_RECEIPT_URL_PRODUCTION = 'https://buy.itunes.apple.com/verifyReceipt' + +# 苹果共享密钥 (在 App Store Connect 中获取) +# 重要:生产环境必须通过环境变量设置,不要使用默认值 +APPLE_SHARED_SECRET = os.environ.get('APPLE_SHARED_SECRET') +if not APPLE_SHARED_SECRET: + print("⚠️ 警告: APPLE_SHARED_SECRET 环境变量未设置,苹果支付功能将无法正常工作") + APPLE_SHARED_SECRET = '' # 空值,API 调用时会失败 + +# 产品 ID 到套餐的映射 +APPLE_PRODUCT_MAP = { + 'com.valuefrontier.meagent.pro_monthly': {'plan': 'pro', 'cycle': 'monthly', 'months': 1}, + 'com.valuefrontier.meagent.pro_quarterly': {'plan': 'pro', 'cycle': 'quarterly', 'months': 3}, + 'com.valuefrontier.meagent.pro_semiannual': {'plan': 'pro', 'cycle': 'semiannual', 'months': 6}, + 'com.valuefrontier.meagent.pro_yearly': {'plan': 'pro', 'cycle': 'yearly', 'months': 12}, + 'com.valuefrontier.meagent.max_monthly': {'plan': 'max', 'cycle': 'monthly', 'months': 1}, + 'com.valuefrontier.meagent.max_quarterly': {'plan': 'max', 'cycle': 'quarterly', 'months': 3}, + 'com.valuefrontier.meagent.max_semiannual': {'plan': 'max', 'cycle': 'semiannual', 'months': 6}, + 'com.valuefrontier.meagent.max_yearly': {'plan': 'max', 'cycle': 'yearly', 'months': 12}, +} + + +def verify_apple_receipt(receipt_data, use_sandbox=False): + """ + 向苹果服务器验证收据 + + Args: + receipt_data: Base64编码的收据数据 + use_sandbox: 是否使用沙盒环境 + + Returns: + dict: 苹果返回的验证结果 + """ + import requests + + url = APPLE_VERIFY_RECEIPT_URL_SANDBOX if use_sandbox else APPLE_VERIFY_RECEIPT_URL_PRODUCTION + + payload = { + 'receipt-data': receipt_data, + 'password': APPLE_SHARED_SECRET, + 'exclude-old-transactions': True + } + + try: + response = requests.post(url, json=payload, timeout=30) + result = response.json() + + # 状态码 21007 表示这是一个沙盒收据但发送到了生产环境 + # 需要重新发送到沙盒环境 + if result.get('status') == 21007 and not use_sandbox: + return verify_apple_receipt(receipt_data, use_sandbox=True) + + return result + except Exception as e: + print(f"[Apple IAP] 验证收据失败: {e}") + return {'status': -1, 'error': str(e)} + + +@app.route('/api/payment/apple/verify-receipt', methods=['POST']) +def verify_apple_receipt_api(): + """ + 验证苹果内购收据并激活订阅 + + Request Body: + { + "receipt_data": "Base64编码的收据数据", + "product_id": "com.valuefrontier.meagent.pro_monthly", + "plan_name": "pro", + "billing_cycle": "monthly" + } + """ + try: + if 'user_id' not in session: + return jsonify({'success': False, 'error': '未登录'}), 401 + + data = request.get_json() + receipt_data = data.get('receipt_data') + product_id = data.get('product_id') + plan_name = data.get('plan_name') + billing_cycle = data.get('billing_cycle') + + if not receipt_data: + return jsonify({'success': False, 'error': '收据数据不能为空'}), 400 + + if not product_id or product_id not in APPLE_PRODUCT_MAP: + return jsonify({'success': False, 'error': '无效的产品 ID'}), 400 + + # 获取产品信息 + product_info = APPLE_PRODUCT_MAP[product_id] + + # 验证 plan_name 和 billing_cycle 是否匹配 + if plan_name and plan_name != product_info['plan']: + return jsonify({'success': False, 'error': '套餐信息不匹配'}), 400 + if billing_cycle and billing_cycle != product_info['cycle']: + return jsonify({'success': False, 'error': '计费周期不匹配'}), 400 + + # 向苹果验证收据 + verify_result = verify_apple_receipt(receipt_data) + + if verify_result.get('status') != 0: + error_messages = { + 21000: '苹果服务器无法读取请求的JSON', + 21002: '收据数据格式错误', + 21003: '收据无法验证', + 21004: '共享密钥不匹配', + 21005: '收据服务器暂时不可用', + 21006: '收据有效但订阅已过期', + 21007: '收据为沙盒收据(已自动处理)', + 21008: '收据为生产收据(已自动处理)', + 21010: '此收据无法被授权', + } + status_code = verify_result.get('status', -1) + error_msg = error_messages.get(status_code, f'验证失败 (status: {status_code})') + return jsonify({'success': False, 'error': error_msg}), 400 + + # 解析收据信息 + receipt = verify_result.get('receipt', {}) + latest_receipt_info = verify_result.get('latest_receipt_info', []) + + # 查找对应的购买记录 + purchase_info = None + for item in latest_receipt_info: + if item.get('product_id') == product_id: + purchase_info = item + break + + if not purchase_info: + # 尝试从 in_app 中查找 + in_app = receipt.get('in_app', []) + for item in in_app: + if item.get('product_id') == product_id: + purchase_info = item + break + + if not purchase_info: + return jsonify({'success': False, 'error': '未找到对应的购买记录'}), 400 + + # 获取交易信息 + transaction_id = purchase_info.get('transaction_id') or purchase_info.get('original_transaction_id') + + # 检查订阅是否过期(对于订阅类产品) + expires_date_ms = purchase_info.get('expires_date_ms') + if expires_date_ms: + try: + from datetime import datetime + expires_date = datetime.fromtimestamp(int(expires_date_ms) / 1000) + if expires_date < datetime.now(): + print(f"[Apple IAP] 订阅已过期: expires_date={expires_date}") + return jsonify({ + 'success': False, + 'error': '订阅已过期,请重新购买', + 'expired_at': expires_date.isoformat() + }), 400 + except Exception as exp_error: + print(f"[Apple IAP] 解析过期时间失败: {exp_error}") + + # 检查是否已经处理过这个交易 + existing_order = PaymentOrder.query.filter_by( + transaction_id=transaction_id + ).first() + + if existing_order: + return jsonify({ + 'success': True, + 'message': '此交易已处理', + 'data': { + 'order_id': existing_order.id, + 'already_processed': True + } + }) + + # 创建订单记录 + try: + order = PaymentOrder( + user_id=session['user_id'], + plan_name=product_info['plan'], + billing_cycle=product_info['cycle'], + amount=0, # 苹果支付的金额由苹果处理,这里记录0 + original_amount=0, + discount_amount=0 + ) + # 设置苹果支付特有的属性 + order.status = 'paid' + order.transaction_id = transaction_id + order.payment_method = 'apple' + order.payment_source = 'ios' + order.remark = f"苹果内购 - {product_id}" + order.paid_at = beijing_now() + db.session.add(order) + + # 激活用户订阅 + subscription = activate_user_subscription( + session['user_id'], + product_info['plan'], + product_info['cycle'] + ) + + db.session.commit() + + print(f"[Apple IAP] 订阅激活成功: user_id={session['user_id']}, plan={product_info['plan']}, cycle={product_info['cycle']}") + + return jsonify({ + 'success': True, + 'message': '订阅激活成功', + 'data': { + 'order_id': order.id, + 'plan': product_info['plan'], + 'cycle': product_info['cycle'], + 'end_date': subscription.end_date.isoformat() if subscription else None + } + }) + + except Exception as e: + db.session.rollback() + print(f"[Apple IAP] 激活订阅失败: {e}") + return jsonify({'success': False, 'error': f'激活订阅失败: {str(e)}'}), 500 + + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({'success': False, 'error': f'处理失败: {str(e)}'}), 500 + + +@app.route('/api/payment/apple/subscription-status', methods=['GET']) +def get_apple_subscription_status(): + """ + 获取用户的苹果订阅状态 + 用于检查订阅是否有效、是否需要续费等 + """ + try: + if 'user_id' not in session: + return jsonify({'success': False, 'error': '未登录'}), 401 + + # 获取用户订阅信息 + subscription = get_user_subscription_safe(session['user_id']) + + return jsonify({ + 'success': True, + 'data': { + 'is_active': subscription.is_active if subscription else False, + 'plan': subscription.subscription_type if subscription else None, + 'cycle': subscription.billing_cycle if subscription else None, + 'end_date': subscription.end_date.isoformat() if subscription and subscription.end_date else None, + 'days_left': subscription.days_left if subscription else 0 + } + }) + + except Exception as e: + return jsonify({'success': False, 'error': f'获取订阅状态失败: {str(e)}'}), 500 + + +@app.route('/api/payment/apple/webhook', methods=['POST']) +def apple_webhook(): + """ + 处理苹果服务器推送的订阅状态通知 (App Store Server Notifications V2) + + 苹果会在以下事件发生时推送通知: + - SUBSCRIBED: 用户订阅 + - DID_RENEW: 自动续费成功 + - DID_FAIL_TO_RENEW: 续费失败 + - DID_CHANGE_RENEWAL_STATUS: 用户更改续订状态(取消/恢复自动续订) + - EXPIRED: 订阅过期 + - GRACE_PERIOD_EXPIRED: 宽限期过期 + - REFUND: 退款 + - REVOKE: 撤销(家庭共享移除等) + + 文档: https://developer.apple.com/documentation/appstoreservernotifications + """ + try: + # 获取请求数据 + data = request.get_json() + + if not data: + print("[Apple Webhook] 收到空请求") + return jsonify({'success': False}), 400 + + # V2 通知格式使用 signedPayload + signed_payload = data.get('signedPayload') + + if signed_payload: + # V2 格式: 需要解码 JWT + # TODO: 实现 JWT 签名验证(使用苹果的公钥) + # 目前先解码 payload 部分(不验证签名,生产环境应验证) + try: + import base64 + import json as json_lib + + # JWT 格式: header.payload.signature + parts = signed_payload.split('.') + if len(parts) >= 2: + # 解码 payload(第二部分) + payload_b64 = parts[1] + # 添加 padding + padding = 4 - len(payload_b64) % 4 + if padding != 4: + payload_b64 += '=' * padding + + payload_json = base64.urlsafe_b64decode(payload_b64) + notification_data = json_lib.loads(payload_json) + else: + print("[Apple Webhook] 无效的 JWT 格式") + return jsonify({'success': False}), 400 + except Exception as decode_error: + print(f"[Apple Webhook] 解码 JWT 失败: {decode_error}") + return jsonify({'success': False}), 400 + else: + # V1 格式(旧版,直接使用 data) + notification_data = data + + # 提取通知类型 + notification_type = notification_data.get('notificationType') or notification_data.get('notification_type') + subtype = notification_data.get('subtype', '') + + print(f"[Apple Webhook] 收到通知: type={notification_type}, subtype={subtype}") + + # 提取交易信息 + transaction_info = None + if 'data' in notification_data: + # V2 格式 + signed_transaction = notification_data['data'].get('signedTransactionInfo') + if signed_transaction: + # 解码交易信息 + try: + parts = signed_transaction.split('.') + if len(parts) >= 2: + payload_b64 = parts[1] + padding = 4 - len(payload_b64) % 4 + if padding != 4: + payload_b64 += '=' * padding + transaction_info = json_lib.loads(base64.urlsafe_b64decode(payload_b64)) + except: + pass + + # 获取原始交易 ID(用于查找用户) + original_transaction_id = None + if transaction_info: + original_transaction_id = transaction_info.get('originalTransactionId') + elif 'unified_receipt' in notification_data: + # V1 格式 + latest_receipt_info = notification_data.get('unified_receipt', {}).get('latest_receipt_info', []) + if latest_receipt_info: + original_transaction_id = latest_receipt_info[0].get('original_transaction_id') + + if not original_transaction_id: + print("[Apple Webhook] 未找到 original_transaction_id") + # 仍然返回成功,避免苹果重试 + return jsonify({'success': True}) + + # 查找对应的订单 + order = PaymentOrder.query.filter_by(transaction_id=original_transaction_id).first() + + if not order: + print(f"[Apple Webhook] 未找到订单: transaction_id={original_transaction_id}") + return jsonify({'success': True}) + + user_id = order.user_id + + # 根据通知类型处理 + if notification_type in ['DID_RENEW', 'SUBSCRIBED']: + # 续费成功或新订阅 + print(f"[Apple Webhook] 续费/订阅成功: user_id={user_id}") + + # 延长订阅时间 + subscription = get_user_subscription_safe(user_id) + if subscription: + # 获取订阅周期 + product_info = APPLE_PRODUCT_MAP.get(order.remark.replace('苹果内购 - ', '')) + if product_info: + months = product_info.get('months', 1) + if subscription.end_date and subscription.end_date > beijing_now(): + # 在现有到期时间基础上延长 + subscription.end_date = subscription.end_date + timedelta(days=30 * months) + else: + # 从现在开始计算 + subscription.end_date = beijing_now() + timedelta(days=30 * months) + subscription.is_active = True + db.session.commit() + print(f"[Apple Webhook] 订阅已延长至: {subscription.end_date}") + + elif notification_type == 'EXPIRED': + # 订阅过期 + print(f"[Apple Webhook] 订阅过期: user_id={user_id}") + subscription = get_user_subscription_safe(user_id) + if subscription: + subscription.is_active = False + subscription.auto_renewal = False + db.session.commit() + + elif notification_type == 'DID_CHANGE_RENEWAL_STATUS': + # 用户更改自动续订状态 + auto_renew = subtype != 'AUTO_RENEW_DISABLED' + print(f"[Apple Webhook] 自动续订状态变更: user_id={user_id}, auto_renew={auto_renew}") + subscription = get_user_subscription_safe(user_id) + if subscription: + subscription.auto_renewal = auto_renew + db.session.commit() + + elif notification_type in ['REFUND', 'REVOKE']: + # 退款或撤销 + print(f"[Apple Webhook] 退款/撤销: user_id={user_id}") + subscription = get_user_subscription_safe(user_id) + if subscription: + subscription.is_active = False + subscription.auto_renewal = False + db.session.commit() + + # 更新订单状态 + order.status = 'refunded' + order.remark = f"{order.remark} - {notification_type}" + db.session.commit() + + elif notification_type == 'DID_FAIL_TO_RENEW': + # 续费失败(进入宽限期或计费重试期) + print(f"[Apple Webhook] 续费失败: user_id={user_id}, subtype={subtype}") + # 可以在这里发送提醒通知给用户 + + elif notification_type == 'GRACE_PERIOD_EXPIRED': + # 宽限期结束 + print(f"[Apple Webhook] 宽限期结束: user_id={user_id}") + subscription = get_user_subscription_safe(user_id) + if subscription: + subscription.is_active = False + db.session.commit() + + return jsonify({'success': True}) + + except Exception as e: + import traceback + traceback.print_exc() + print(f"[Apple Webhook] 处理失败: {e}") + # 返回成功以避免苹果重试(错误已记录) + return jsonify({'success': True}) + + @app.route('/api/auth/session', methods=['GET']) def get_session_info(): """获取当前登录用户信息""" diff --git a/migrations/add_apple_payment_fields.sql b/migrations/add_apple_payment_fields.sql new file mode 100644 index 00000000..8148078e --- /dev/null +++ b/migrations/add_apple_payment_fields.sql @@ -0,0 +1,59 @@ +-- ============================================================ +-- 数据库迁移: 添加苹果支付相关字段 +-- 表: payment_orders +-- 日期: 2025-01-21 +-- 描述: 为支持 Apple IAP (In-App Purchase) 添加必要的字段 +-- ============================================================ + +-- ============================================================ +-- 1. 添加新字段 +-- ============================================================ + +-- 添加苹果交易ID字段 (用于存储 Apple 的 transaction_id 或 original_transaction_id) +ALTER TABLE payment_orders +ADD COLUMN IF NOT EXISTS transaction_id VARCHAR(64) NULL +COMMENT '苹果交易ID (Apple IAP transaction_id)'; + +-- 添加支付来源字段 (区分 web/ios/android 等来源) +ALTER TABLE payment_orders +ADD COLUMN IF NOT EXISTS payment_source VARCHAR(20) DEFAULT 'web' +COMMENT '支付来源: web/ios/android'; + + +-- ============================================================ +-- 2. 创建索引 (提升查询性能) +-- ============================================================ + +-- 为 transaction_id 创建索引 (用于防重复和 Webhook 查找) +-- 使用 IF NOT EXISTS 语法(MySQL 8.0+)或先检查再创建 +CREATE INDEX idx_payment_orders_transaction_id +ON payment_orders(transaction_id); + +-- 为 payment_source 创建索引 (用于统计分析) +CREATE INDEX idx_payment_orders_payment_source +ON payment_orders(payment_source); + + +-- ============================================================ +-- 3. 验证迁移结果 +-- ============================================================ + +-- 查看表结构确认字段已添加 +-- DESCRIBE payment_orders; + +-- 查看索引确认已创建 +-- SHOW INDEX FROM payment_orders WHERE Key_name LIKE '%transaction%' OR Key_name LIKE '%payment_source%'; + + +-- ============================================================ +-- 回滚脚本 (如需回滚,执行以下 SQL) +-- ============================================================ +/* +-- 删除索引 +DROP INDEX idx_payment_orders_transaction_id ON payment_orders; +DROP INDEX idx_payment_orders_payment_source ON payment_orders; + +-- 删除字段 +ALTER TABLE payment_orders DROP COLUMN transaction_id; +ALTER TABLE payment_orders DROP COLUMN payment_source; +*/ diff --git a/migrations/add_apple_payment_fields_mysql57.sql b/migrations/add_apple_payment_fields_mysql57.sql new file mode 100644 index 00000000..f6341391 --- /dev/null +++ b/migrations/add_apple_payment_fields_mysql57.sql @@ -0,0 +1,76 @@ +-- ============================================================ +-- 数据库迁移: 添加苹果支付相关字段 (MySQL 5.7 兼容版) +-- 表: payment_orders +-- 日期: 2025-01-21 +-- 描述: 为支持 Apple IAP (In-App Purchase) 添加必要的字段 +-- ============================================================ + +-- ============================================================ +-- 执行前检查 (可选) +-- ============================================================ +-- 查看当前表结构 +-- DESCRIBE payment_orders; + +-- ============================================================ +-- 1. 添加新字段 (MySQL 5.7 兼容语法) +-- ============================================================ + +-- 先检查字段是否存在,如果不存在则添加 +-- 注意:MySQL 5.7 不支持 IF NOT EXISTS,需要手动检查或使用存储过程 + +-- 方式一:直接执行(如果字段已存在会报错,可忽略) +ALTER TABLE payment_orders +ADD COLUMN transaction_id VARCHAR(64) NULL +COMMENT '苹果交易ID (Apple IAP transaction_id)'; + +ALTER TABLE payment_orders +ADD COLUMN payment_source VARCHAR(20) DEFAULT 'web' +COMMENT '支付来源: web/ios/android'; + + +-- ============================================================ +-- 2. 创建索引 +-- ============================================================ + +-- 为 transaction_id 创建索引 +ALTER TABLE payment_orders +ADD INDEX idx_payment_orders_transaction_id (transaction_id); + +-- 为 payment_source 创建索引 +ALTER TABLE payment_orders +ADD INDEX idx_payment_orders_payment_source (payment_source); + + +-- ============================================================ +-- 3. 更新现有数据的 payment_source (可选) +-- ============================================================ + +-- 将所有现有订单的 payment_source 设置为 'web'(如果为空) +UPDATE payment_orders +SET payment_source = 'web' +WHERE payment_source IS NULL OR payment_source = ''; + + +-- ============================================================ +-- 4. 验证迁移结果 +-- ============================================================ + +-- 查看表结构确认字段已添加 +DESCRIBE payment_orders; + +-- 查看索引确认已创建 +SHOW INDEX FROM payment_orders WHERE Key_name LIKE 'idx_payment_orders%'; + + +-- ============================================================ +-- 回滚脚本 (如需回滚,执行以下 SQL) +-- ============================================================ +/* +-- 删除索引 +ALTER TABLE payment_orders DROP INDEX idx_payment_orders_transaction_id; +ALTER TABLE payment_orders DROP INDEX idx_payment_orders_payment_source; + +-- 删除字段 +ALTER TABLE payment_orders DROP COLUMN transaction_id; +ALTER TABLE payment_orders DROP COLUMN payment_source; +*/ diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js index 47317096..1f8d4fa4 100644 --- a/src/views/AgentChat/index.js +++ b/src/views/AgentChat/index.js @@ -56,6 +56,19 @@ const AgentChat = () => { // ==================== MAX 权限检查 ==================== const isMaxUser = user?.subscription_type === 'max' || hasSubscriptionLevel('max'); + // 🔍 调试日志:排查会员权限问题 + React.useEffect(() => { + console.log('🔍 [AgentChat] 会员权限调试信息:', { + isAuthenticated, + userId: user?.id, + userSubscriptionType: user?.subscription_type, + userSubscriptionStatus: user?.subscription_status, + hasMaxLevel: hasSubscriptionLevel('max'), + isMaxUser, + fullUser: user + }); + }, [user, isAuthenticated, isMaxUser]); + // ==================== 聊天模式状态 ==================== const [chatMode, setChatMode] = useState(ChatMode.SINGLE);