feat: 微信分享钩子全页面接入 + 订阅信息结构化映射 + JWT 错误处理加强

- client: 全部页面接入 onShareAppMessage/onShareTimeline 分享钩子
- client: login 页增强、profile 订阅展示优化
- client/stores: 订阅字段扁平结构映射为 subInfo(plan/expiresAt/startDate)
- server: auth 路由 JWT 无效/过期统一返回 401 未授权响应
- server: models/tests 同步更新
This commit is contained in:
Yuzhiran Dev
2026-09-04 11:47:29 +08:00
parent de8ca9c420
commit 66de1dc396
37 changed files with 2138 additions and 1830 deletions
+192
View File
@@ -80,10 +80,35 @@
<view class="footer-spacer"></view>
</scroll-view>
<!-- H5 支付失败/备选方案 -->
<view v-if="showH5Fallback" class="h5-fallback-mask">
<view class="h5-fallback-content">
<view class="fallback-icon">📱</view>
<text class="fallback-title">请使用微信完成支付</text>
<text class="fallback-desc">
订单号:{{ h5OrderNo }}
金额:¥{{ currentPrice }}
</text>
<view class="fallback-actions">
<button class="fallback-btn primary" @click="retryH5Pay">
再次跳转微信支付
</button>
<button class="fallback-btn secondary" @click="copyH5Url">
复制链接到微信打开
</button>
<button class="fallback-btn text-btn" @click="closeH5Fallback">
稍后再说
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
import { paymentApi } from '@/utils/api'
import { useUserStore } from '@/stores/user'
@@ -102,6 +127,21 @@ const currentPrice = computed(() => PLAN_MAP[selectedPlan.value].price)
const currentProductId = computed(() => PLAN_MAP[selectedPlan.value].productId)
const loading = ref(false)
// H5 支付相关状态
const h5PayUrl = ref('')
const showH5Fallback = ref(false)
const h5OrderNo = ref('')
// 检测是否运行在 H5 环境
function isH5() {
// #ifdef H5
return true
// #endif
// #ifdef MP-WEIXIN
return false
// #endif
return false
}
async function handlePay() {
if (!userStore.isLoggedIn) {
@@ -119,6 +159,39 @@ async function handlePay() {
const amount = plan.price
loading.value = true
// ===== H5 环境:使用微信支付 H5 跳转链接 =====
if (isH5()) {
try {
const data = await paymentApi.createH5Order(productId, window.location.hostname, userStore.token)
h5OrderNo.value = data.orderNo
if (data.h5Url) {
h5PayUrl.value = data.h5Url
showH5Fallback.value = false
// 尝试直接跳转微信支付页面
// #ifdef H5
window.location.href = data.h5Url
// #endif
// H5 环境下跳转后不会返回,这里设置一个超时提示
setTimeout(() => {
showH5Fallback.value = true
}, 5000)
} else {
uni.showToast({ title: '获取支付链接失败', icon: 'none' })
loading.value = false
}
} catch (err) {
console.error('H5创建订单失败:', err)
uni.showToast({ title: err.message || '创建订单失败,请重试', icon: 'none' })
loading.value = false
}
return
}
// ===== 微信小程序环境:使用原生支付 =====
try {
const data = await paymentApi.createOrder(productId, userStore.token)
const payParam = data.payParams || data.wechatParam || data.paymentData
@@ -167,15 +240,52 @@ async function handlePay() {
}
}
/** H5 支付:复制链接到微信打开 */
function copyH5Url() {
// #ifdef H5
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(h5PayUrl.value).then(() => {
uni.showToast({ title: '链接已复制,请到微信中打开', icon: 'none' })
}).catch(() => {
// fallback
const textarea = document.createElement('textarea')
textarea.value = h5PayUrl.value
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
uni.showToast({ title: '链接已复制,请到微信中打开', icon: 'none' })
})
}
// #endif
}
/** H5 支付:再次跳转 */
function retryH5Pay() {
if (h5PayUrl.value) {
// #ifdef H5
window.location.href = h5PayUrl.value
// #endif
}
}
/** H5 支付:关闭窗口返回 */
function closeH5Fallback() {
showH5Fallback.value = false
loading.value = false
}
function goBack() {
uni.navigateBack()
}
// 微信小程序分享
// #ifdef MP-WEIXIN
onShareAppMessage(() => ({
title: 'AI 维度 - 你的AI知识助手',
path: '/pages/index/index'
}))
// #endif
</script>
<style lang="scss" scoped>
@@ -384,4 +494,86 @@ onShareAppMessage(() => ({
}
.footer-spacer { height: 40px; }
// ===== H5 支付备选方案 =====
.h5-fallback-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.h5-fallback-content {
background: var(--bg-primary);
border-radius: 20px;
padding: 32px 24px;
width: 85%;
max-width: 380px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.fallback-icon {
font-size: 48px;
margin-bottom: 8px;
}
.fallback-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.fallback-desc {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.8;
margin-bottom: 8px;
white-space: pre-line;
}
.fallback-actions {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
margin-top: 8px;
}
.fallback-btn {
padding: 12px 20px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
border: none;
cursor: pointer;
&.primary {
background: linear-gradient(135deg, #07C160, #06AE56);
color: white;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3);
}
&.secondary {
background: var(--glass-bg);
border: 1.5px solid var(--glass-border);
color: var(--text-primary);
}
&.text-btn {
background: transparent;
color: var(--text-tertiary);
font-size: 13px;
padding: 8px;
}
}
</style>