From d44893d78dbf300b4340cca91407078e2be79de8 Mon Sep 17 00:00:00 2001 From: txai-dev Date: Mon, 14 Sep 2026 11:07:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=80=E5=8C=96=E4=B8=BA'=E6=AF=8F?= =?UTF-8?q?=E5=A4=A95=E6=AC=A1=E5=85=8D=E8=B4=B9=E2=86=92=E5=85=85?= =?UTF-8?q?=E5=80=BC=E6=98=9F=E5=B0=98'=E6=A8=A1=E5=9E=8B=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=97=AE=E7=AD=94=E5=85=8D=E8=B4=B9=E4=BC=98=E5=85=88?= =?UTF-8?q?=E6=98=9F=E5=B0=98=E6=89=A3=E5=87=8F(10/=E6=AC=A1)+=E5=85=85?= =?UTF-8?q?=E5=80=BC=E5=BC=95=E5=AF=BC=E5=BC=B9=E7=AA=97+=E6=88=91?= =?UTF-8?q?=E7=9A=84=E9=A1=B5=E6=98=9F=E5=B0=98=E4=BD=99=E9=A2=9D=E5=8D=A1?= =?UTF-8?q?(=E5=8E=BB=E6=8E=89=E5=85=8D=E8=B4=B9/Pro=E5=88=86=E5=B1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/chat/chat.vue | 15 ++++++ client/src/pages/profile/profile.vue | 80 +++++++++++++++++----------- server/src/routes/aiChat.js | 27 ++++++++-- 3 files changed, 87 insertions(+), 35 deletions(-) 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()