本文档介绍了 PitchMaster.ai 项目中新增的订阅检查功能,基于 Boxn API 的订阅状态检查接口。
在 src/api/payment.ts 中新增了订阅检查接口:
// 订阅检查响应接口
export interface SubscriptionCheckResponse {
hasValidSubscription: boolean; // 用户是否有有效订阅
subscriptionType?: string; // 订阅类型
expiresAt?: string; // 订阅过期日期
features?: string[]; // 可用功能
}
// 订阅检查 API 函数
async checkHasValidSubscription(): Promise<SubscriptionCheckResponse> {
const response = await paymentApiClient.get('/api/payments/subscription/check');
return response.data;
}在 src/stores/pay.ts 中新增了订阅相关的状态和方法:
// 订阅状态
const subscriptionStatus = ref<SubscriptionCheckResponse | null>(null);
const isSubscriptionLoading = ref(false);// 检查用户是否有有效订阅
const hasValidSubscription = computed(() => {
return subscriptionStatus.value?.hasValidSubscription ?? false;
});
// 获取订阅类型
const subscriptionType = computed(() => {
return subscriptionStatus.value?.subscriptionType ?? null;
});
// 获取订阅过期日期
const subscriptionExpiresAt = computed(() => {
return subscriptionStatus.value?.expiresAt ?? null;
});
// 获取可用订阅功能
const subscriptionFeatures = computed(() => {
return subscriptionStatus.value?.features ?? [];
});// 检查用户订阅状态
const checkSubscription = async () => {
try {
isSubscriptionLoading.value = true;
error.value = null;
const response = await paymentApi.checkHasValidSubscription();
subscriptionStatus.value = response;
return response;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to check subscription status';
throw err;
} finally {
isSubscriptionLoading.value = false;
}
};在 src/stores/user.ts 中更新了付费状态检查方法:
// 检查用户付费状态
async checkPaymentStatus() {
try {
// 使用新的订阅检查 API
const paymentStore = usePaymentStore();
const subscriptionStatus = await paymentStore.checkSubscription();
// 更新付费状态
this.hasPaid = subscriptionStatus.hasValidSubscription;
// 如果用户有有效订阅,标记为已付费
if (this.hasPaid) {
localStorage.setItem('userPaymentStatus', 'paid');
} else {
localStorage.removeItem('userPaymentStatus');
}
return this.hasPaid;
} catch (error) {
console.error('检查付费状态失败:', error);
// 如果 API 调用失败,回退到 localStorage
const paymentStatus = localStorage.getItem('userPaymentStatus');
this.hasPaid = paymentStatus === 'paid';
return this.hasPaid;
}
}新增了 src/components/SubscriptionStatus.vue 组件,用于显示用户的订阅状态:
- 实时显示订阅状态(有效/无效)
- 显示订阅类型、到期时间和可用功能
- 支持手动刷新订阅状态
- 提供升级订阅的入口
- 错误处理和加载状态
<template>
<SubscriptionStatus />
</template>
<script setup>
import SubscriptionStatus from '@/components/SubscriptionStatus.vue';
</script><template>
<div>
<div v-if="hasValidSubscription">
<h2>欢迎,高级用户!</h2>
<p>您的订阅类型: {{ subscriptionType }}</p>
<p>到期时间: {{ subscriptionExpiresAt }}</p>
<div>
<h3>可用功能:</h3>
<ul>
<li v-for="feature in subscriptionFeatures" :key="feature">
{{ feature }}
</li>
</ul>
</div>
</div>
<div v-else>
<h2>升级到高级版本</h2>
<p>解锁更多功能</p>
<button @click="upgrade">立即升级</button>
</div>
</div>
</template>
<script setup>
import { usePaymentStore } from '@/stores/pay';
const paymentStore = usePaymentStore();
const {
hasValidSubscription,
subscriptionType,
subscriptionExpiresAt,
subscriptionFeatures,
checkSubscription
} = paymentStore;
const upgrade = () => {
// 导航到支付页面
router.push('/payment');
};
// 页面加载时检查订阅状态
onMounted(async () => {
await checkSubscription();
});
</script>// router/index.ts
import { usePaymentStore } from '@/stores/pay';
router.beforeEach(async (to, from, next) => {
const paymentStore = usePaymentStore();
// 检查需要订阅的页面
if (to.meta.requiresSubscription) {
try {
await paymentStore.checkSubscription();
if (!paymentStore.hasValidSubscription) {
// 重定向到支付页面
next('/payment');
return;
}
} catch (error) {
console.error('Failed to check subscription:', error);
// 如果检查失败,允许访问(可以根据需要调整)
next();
return;
}
}
next();
});// 在 API 请求失败时检查订阅状态
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 403) {
// 可能是订阅过期,重新检查订阅状态
const paymentStore = usePaymentStore();
await paymentStore.checkSubscription();
if (!paymentStore.hasValidSubscription) {
// 重定向到支付页面
router.push('/payment');
}
}
return Promise.reject(error);
}
);请求:
GET /api/payments/subscription/check
Authorization: Bearer {access_token}
响应:
{
"hasValidSubscription": true,
"subscriptionType": "premium",
"expiresAt": "2024-12-31T23:59:59Z",
"features": [
"unlimited_pitches",
"advanced_analytics",
"priority_support"
]
}字段说明:
hasValidSubscription: 布尔值,表示用户是否有有效订阅subscriptionType: 字符串,订阅类型(如 "basic", "premium", "enterprise")expiresAt: 字符串,订阅过期时间(ISO 8601 格式)features: 字符串数组,用户可用的功能列表
-
网络错误
try { await paymentStore.checkSubscription(); } catch (error) { if (error.code === 'NETWORK_ERROR') { // 显示网络错误提示 showNetworkError(); } }
-
认证失败
// 在 API 拦截器中处理 401 错误 if (error.response?.status === 401) { // 清除用户会话,重定向到登录页面 userStore.logout(); router.push('/login'); }
-
服务器错误
if (error.response?.status >= 500) { // 显示服务器错误提示 showServerError(); }
// 缓存订阅状态,避免频繁请求
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟
const checkSubscriptionWithCache = async () => {
const cached = localStorage.getItem('subscriptionCache');
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
return data;
}
}
const result = await paymentStore.checkSubscription();
// 缓存结果
localStorage.setItem('subscriptionCache', JSON.stringify({
data: result,
timestamp: Date.now()
}));
return result;
};// 定期检查订阅状态
const startSubscriptionCheck = () => {
setInterval(async () => {
try {
await paymentStore.checkSubscription();
} catch (error) {
console.error('Periodic subscription check failed:', error);
}
}, 10 * 60 * 1000); // 每10分钟检查一次
};// 在关键操作前检查订阅状态
const performPremiumAction = async () => {
if (!paymentStore.hasValidSubscription) {
// 显示升级提示
showUpgradeModal();
return;
}
// 执行高级功能
await executePremiumFeature();
};import { describe, it, expect, vi } from 'vitest';
import { usePaymentStore } from '@/stores/pay';
describe('Subscription Check', () => {
it('should check subscription status successfully', async () => {
const store = usePaymentStore();
const mockResponse = {
hasValidSubscription: true,
subscriptionType: 'premium',
expiresAt: '2024-12-31T23:59:59Z',
features: ['feature1', 'feature2']
};
vi.spyOn(paymentApi, 'checkHasValidSubscription').mockResolvedValue(mockResponse);
const result = await store.checkSubscription();
expect(result).toEqual(mockResponse);
expect(store.hasValidSubscription).toBe(true);
expect(store.subscriptionType).toBe('premium');
});
});- 环境变量: 确保生产环境中正确配置了 Boxn API 的基础 URL 和认证信息
- CORS: 确保 API 服务器允许前端域名的跨域请求
- 缓存: 在生产环境中考虑使用 Redis 等缓存系统来缓存订阅状态
- 监控: 添加订阅检查 API 的监控和告警机制
- v1.0.0: 初始版本,支持基本的订阅状态检查功能
- v1.1.0: 添加了订阅状态组件和用户 Store 集成
- v1.2.0: 优化了错误处理和缓存策略