feat: 简化为'每天5次免费→充值星尘'模型——问答免费优先星尘扣减(10/次)+充值引导弹窗+我的页星尘余额卡(去掉免费/Pro分层)
This commit is contained in:
@@ -160,6 +160,21 @@ async function sendMessage() {
|
|||||||
const msg = e.message || 'AI 服务暂时不可用,请稍后再试'
|
const msg = e.message || 'AI 服务暂时不可用,请稍后再试'
|
||||||
if (msg.includes('游客体验已用完')) {
|
if (msg.includes('游客体验已用完')) {
|
||||||
guestExhausted.value = true
|
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 {
|
} else {
|
||||||
messages.value.push({ role: 'assistant', content: escapeHtml(`抱歉,${msg}`) })
|
messages.value.push({ role: 'assistant', content: escapeHtml(`抱歉,${msg}`) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,39 +55,21 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订阅卡片 -->
|
<!-- 星尘余额 + 今日免费(简化模型:每天5次免费,用完充值星尘) -->
|
||||||
<view class="plan-card" @click="goPayment">
|
<view class="balance-card">
|
||||||
<view class="plan-info">
|
<view class="balance-left">
|
||||||
<text class="plan-title">{{ planTitle }}</text>
|
<view class="coin-row">
|
||||||
<text class="plan-desc">{{ planDesc }}</text>
|
<text class="coin-icon">✨</text>
|
||||||
<view class="plan-progress" v-if="userStore.subInfo">
|
<text class="coin-num">{{ vpayBalance }}</text>
|
||||||
<view class="progress-track">
|
<text class="coin-name">星尘</text>
|
||||||
<view class="progress-fill" :style="{ width: planPercent + '%' }"></view>
|
|
||||||
</view>
|
|
||||||
<text class="progress-text">{{ planExpiryText }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="free-left">今日免费问答剩余 {{ freeRemaining }} 次</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="plan-btn" :class="{ disabled: userStore.isSubscribed }">
|
<view class="recharge-go" @click="goRecharge">
|
||||||
<text>{{ planBtnText }}</text>
|
<text>充值</text>
|
||||||
</view>
|
</view>
|
||||||
</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="vpay-entry" @click="goRecharge">
|
||||||
<view class="ve-icon">✨</view>
|
<view class="ve-icon">✨</view>
|
||||||
<view class="ve-info">
|
<view class="ve-info">
|
||||||
@@ -155,7 +137,7 @@
|
|||||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { knowledgeApi, aiChatApi } from '@/utils/api'
|
import { knowledgeApi, aiChatApi, request } from '@/utils/api'
|
||||||
import { DIMENSIONS } from '@/utils/dimensions'
|
import { DIMENSIONS } from '@/utils/dimensions'
|
||||||
import AppIcon from '@/components/AppIcon.vue'
|
import AppIcon from '@/components/AppIcon.vue'
|
||||||
import { APP_VERSION } from '@/utils/version'
|
import { APP_VERSION } from '@/utils/version'
|
||||||
@@ -169,6 +151,8 @@ const userStore = useUserStore()
|
|||||||
const dimensions = DIMENSIONS
|
const dimensions = DIMENSIONS
|
||||||
|
|
||||||
const collectedCount = ref(0)
|
const collectedCount = ref(0)
|
||||||
|
const vpayBalance = ref(0)
|
||||||
|
const freeRemaining = ref(5)
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const byDim = ref({})
|
const byDim = ref({})
|
||||||
const quotaInfo = ref(null)
|
const quotaInfo = ref(null)
|
||||||
@@ -218,12 +202,23 @@ const quotaPercent = computed(() => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadStats()
|
loadStats()
|
||||||
if (userStore.isLoggedIn) {
|
if (userStore.isLoggedIn) {
|
||||||
|
loadVpayInfo()
|
||||||
userStore.fetchMe()
|
userStore.fetchMe()
|
||||||
loadCollected()
|
loadCollected()
|
||||||
loadQuota()
|
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() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '')
|
const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '')
|
||||||
@@ -649,3 +644,28 @@ onShareTimeline(() => ({
|
|||||||
.ve-desc { font-size: 11px; color: var(--text-tertiary); }
|
.ve-desc { font-size: 11px; color: var(--text-tertiary); }
|
||||||
.ve-arrow { font-size: 14px; color: var(--text-muted); }
|
.ve-arrow { font-size: 14px; color: var(--text-muted); }
|
||||||
</style>
|
</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 express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const { AIChatQuota, AIModel } = require('../models')
|
const { AIChatQuota, AIModel, User } = require('../models')
|
||||||
const ApiResponse = require('../utils/response')
|
const ApiResponse = require('../utils/response')
|
||||||
const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth')
|
const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
@@ -81,10 +81,27 @@ router.post('/ask', askRateLimit, optionalAuthMiddleware, async (req, res) => {
|
|||||||
const quota = await getOrCreateQuota(openid, isGuest)
|
const quota = await getOrCreateQuota(openid, isGuest)
|
||||||
|
|
||||||
if (!quota.hasQuota()) {
|
if (!quota.hasQuota()) {
|
||||||
const msg = isGuest
|
if (isGuest) {
|
||||||
? `今日 ${GUEST_DAILY_QUOTA} 次游客体验已用完,登录后每天可问 5 次`
|
return ApiResponse.error(res, `今日 ${GUEST_DAILY_QUOTA} 次游客体验已用完,登录后每天可问 5 次`, 403, { isGuest, needLogin: true })
|
||||||
: '今日问答次数已用完,分享好友可获得更多次数'
|
}
|
||||||
return ApiResponse.error(res, msg, 403, { isGuest, needLogin: isGuest })
|
// 免费额度用完 → 检查星尘余额,有则扣星尘(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()
|
const aiModels = await AIModel.getActiveModels()
|
||||||
|
|||||||
Reference in New Issue
Block a user