diff --git a/client/src/pages/chat/chat.vue b/client/src/pages/chat/chat.vue index 3cd98af..1216fe2 100644 --- a/client/src/pages/chat/chat.vue +++ b/client/src/pages/chat/chat.vue @@ -160,6 +160,21 @@ async function sendMessage() { const msg = e.message || 'AI 服务暂时不可用,请稍后再试' if (msg.includes('游客体验已用完')) { guestExhausted.value = true + } else if (msg.includes('免费次数已用完') || msg.includes('星尘余额不足') || msg.includes('充值星尘')) { + // 引导充值 + uni.showModal({ + title: '免费次数用完了', + content: '充值星尘可继续提问(10星尘/次),还可以享受更多功能', + confirmText: '去充值', + cancelText: '分享得次数', + success: (m) => { + if (m.confirm) { + uni.navigateTo({ url: '/pages/vpay/recharge' }) + } else { + onQuotaTap() + } + }, + }) } else { messages.value.push({ role: 'assistant', content: escapeHtml(`抱歉,${msg}`) }) } diff --git a/client/src/pages/profile/profile.vue b/client/src/pages/profile/profile.vue index b3910a3..bf6715a 100644 --- a/client/src/pages/profile/profile.vue +++ b/client/src/pages/profile/profile.vue @@ -55,39 +55,21 @@ - - - - {{ planTitle }} - {{ planDesc }} - - - - - {{ planExpiryText }} + + + + + ✨ + {{ vpayBalance }} + 星尘 + 今日免费问答剩余 {{ freeRemaining }} 次 - - {{ planBtnText }} + + 充值 - - - - - - AI 问答 · 今日 - - - - - - 剩余 {{ quotaInfo.remainingQuota }} 次 - 分享 +5 次 - - - - + ✨ @@ -155,7 +137,7 @@ import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { ref, computed, onMounted } from 'vue' import { useUserStore } from '@/stores/user' -import { knowledgeApi, aiChatApi } from '@/utils/api' +import { knowledgeApi, aiChatApi, request } from '@/utils/api' import { DIMENSIONS } from '@/utils/dimensions' import AppIcon from '@/components/AppIcon.vue' import { APP_VERSION } from '@/utils/version' @@ -169,6 +151,8 @@ const userStore = useUserStore() const dimensions = DIMENSIONS const collectedCount = ref(0) +const vpayBalance = ref(0) +const freeRemaining = ref(5) const stats = ref(null) const byDim = ref({}) const quotaInfo = ref(null) @@ -218,12 +202,23 @@ const quotaPercent = computed(() => { onMounted(() => { loadStats() if (userStore.isLoggedIn) { + loadVpayInfo() userStore.fetchMe() loadCollected() loadQuota() } }) +async function loadVpayInfo() { + try { + const data = await request('GET', '/api/vpay/info', null, userStore.token) + vpayBalance.value = data.balance || 0 + if (userStore.quota) freeRemaining.value = userStore.quota.remainingQuota ?? 5 + } catch (e) { + // 未开通或未登录 + } +} + async function loadStats() { try { const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '') @@ -649,3 +644,28 @@ onShareTimeline(() => ({ .ve-desc { font-size: 11px; color: var(--text-tertiary); } .ve-arrow { font-size: 14px; color: var(--text-muted); } + + diff --git a/server/src/routes/aiChat.js b/server/src/routes/aiChat.js index fbff8a6..414ca49 100755 --- a/server/src/routes/aiChat.js +++ b/server/src/routes/aiChat.js @@ -1,6 +1,6 @@ const express = require('express') const router = express.Router() -const { AIChatQuota, AIModel } = require('../models') +const { AIChatQuota, AIModel, User } = require('../models') const ApiResponse = require('../utils/response') const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth') const logger = require('../utils/logger') @@ -81,10 +81,27 @@ router.post('/ask', askRateLimit, optionalAuthMiddleware, async (req, res) => { const quota = await getOrCreateQuota(openid, isGuest) if (!quota.hasQuota()) { - const msg = isGuest - ? `今日 ${GUEST_DAILY_QUOTA} 次游客体验已用完,登录后每天可问 5 次` - : '今日问答次数已用完,分享好友可获得更多次数' - return ApiResponse.error(res, msg, 403, { isGuest, needLogin: isGuest }) + if (isGuest) { + return ApiResponse.error(res, `今日 ${GUEST_DAILY_QUOTA} 次游客体验已用完,登录后每天可问 5 次`, 403, { isGuest, needLogin: true }) + } + // 免费额度用完 → 检查星尘余额,有则扣星尘(10星尘/次) + const user = await User.findOne({ openid }) + const coins = (user && user.vpayCoins) || 0 + if (coins < 10) { + return ApiResponse.error(res, '今日免费次数已用完,充值星尘可继续提问(10星尘/次)', 402, { needRecharge: true, coins, costPerAsk: 10 }) + } + // 扣星尘(原子操作防并发) + const updated = await User.updateOne({ _id: user._id, vpayCoins: { $gte: 10 } }, { $inc: { vpayCoins: -10 } }) + if (updated.modifiedCount === 0) { + return ApiResponse.error(res, '星尘余额不足,请充值后继续', 402, { needRecharge: true }) + } + // 跳过正常 quota 扣减,直接用星尘 + const aiModels2 = await AIModel.getActiveModels() + if (!aiModels2 || aiModels2.length === 0) { + const env2 = [{ name: 'env-model', apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', apiKey: process.env.OPENAI_API_KEY, modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo' }] + return await tryModels(env2, question, dimension, res, quota, isGuest) + } + return await tryModels(aiModels2, question, dimension, res, quota, isGuest) } const aiModels = await AIModel.getActiveModels()