feat: 简化为'每天5次免费→充值星尘'模型——问答免费优先星尘扣减(10/次)+充值引导弹窗+我的页星尘余额卡(去掉免费/Pro分层)
This commit is contained in:
@@ -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}`) })
|
||||
}
|
||||
|
||||
@@ -55,39 +55,21 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订阅卡片 -->
|
||||
<view class="plan-card" @click="goPayment">
|
||||
<view class="plan-info">
|
||||
<text class="plan-title">{{ planTitle }}</text>
|
||||
<text class="plan-desc">{{ planDesc }}</text>
|
||||
<view class="plan-progress" v-if="userStore.subInfo">
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: planPercent + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ planExpiryText }}</text>
|
||||
<!-- 星尘余额 + 今日免费(简化模型:每天5次免费,用完充值星尘) -->
|
||||
<view class="balance-card">
|
||||
<view class="balance-left">
|
||||
<view class="coin-row">
|
||||
<text class="coin-icon">✨</text>
|
||||
<text class="coin-num">{{ vpayBalance }}</text>
|
||||
<text class="coin-name">星尘</text>
|
||||
</view>
|
||||
<text class="free-left">今日免费问答剩余 {{ freeRemaining }} 次</text>
|
||||
</view>
|
||||
<view class="plan-btn" :class="{ disabled: userStore.isSubscribed }">
|
||||
<text>{{ planBtnText }}</text>
|
||||
<view class="recharge-go" @click="goRecharge">
|
||||
<text>充值</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 问答配额 -->
|
||||
<view class="quota-card" @click="goChat" v-if="userStore.isLoggedIn && quotaInfo">
|
||||
<view class="quota-head">
|
||||
<AppIcon name="chat" :size="15" />
|
||||
<text class="quota-card-title">AI 问答 · 今日</text>
|
||||
</view>
|
||||
<view class="quota-bar">
|
||||
<view class="quota-fill" :style="{ width: quotaPercent + '%' }"></view>
|
||||
</view>
|
||||
<view class="quota-foot">
|
||||
<text class="quota-text">剩余 {{ quotaInfo.remainingQuota }} 次</text>
|
||||
<text class="quota-share" @click.stop="goShare">分享 +5 次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 星尘充值入口 -->
|
||||
<!-- 星尘充值入口 -->
|
||||
<view class="vpay-entry" @click="goRecharge">
|
||||
<view class="ve-icon">✨</view>
|
||||
<view class="ve-info">
|
||||
@@ -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); }
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.balance-card {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
background: linear-gradient(135deg, #5b5ff8, #8b5cf6);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.balance-left { flex: 1; }
|
||||
.coin-row { display: flex; align-items: baseline; gap: 6px; }
|
||||
.coin-icon { font-size: 20px; }
|
||||
.coin-num { font-size: 32px; font-weight: 800; color: #fff; }
|
||||
.coin-name { font-size: 13px; color: rgba(255,255,255,.7); }
|
||||
.free-left { display: block; font-size: 11px; color: rgba(255,255,255,.55); margin-top: 4px; }
|
||||
.recharge-go {
|
||||
padding: 10px 22px;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-full);
|
||||
color: #5b5ff8;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user