补充 VP 模块注册 + 前端 API 封装
- app.module.ts: 注册 VirtualPaymentModule - config.ts + api.ts: 添加虚拟支付 API 端点 - useGravityPurchase.ts: VP 购买逻辑 - interview.vue / user.vue: 适配 VP 调用 - admin.controller.ts: 管理端虚拟支付相关调整 - payment.controller.spec.ts: 测试适配
This commit is contained in:
@@ -28,6 +28,7 @@ import { PricingModule } from './modules/schemas/pricing.module'
|
|||||||
import { ShareModule } from './modules/share/share.module'
|
import { ShareModule } from './modules/share/share.module'
|
||||||
import { InterviewReviewModule } from './modules/interview-review/interview-review.module'
|
import { InterviewReviewModule } from './modules/interview-review/interview-review.module'
|
||||||
import { CareerAdviceModule } from './modules/career-advice/career-advice.module'
|
import { CareerAdviceModule } from './modules/career-advice/career-advice.module'
|
||||||
|
import { VirtualPaymentModule } from './modules/virtual-payment/virtual-payment.module'
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin'
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin'
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin
|
|||||||
ShareModule,
|
ShareModule,
|
||||||
InterviewReviewModule,
|
InterviewReviewModule,
|
||||||
CareerAdviceModule,
|
CareerAdviceModule,
|
||||||
|
VirtualPaymentModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
|
|||||||
@@ -428,8 +428,8 @@ const DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PRICING = {
|
const DEFAULT_PRICING = {
|
||||||
interview: { pricePerSession: 500, creditsPerPurchase: 1 },
|
interview: { pricePerSession: 600, creditsPerPurchase: 1 },
|
||||||
resumeOptimize: { freeLimit: 3, pricePerOptimize: 300, creditsPerPurchase: 1 },
|
resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 },
|
||||||
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
||||||
plans: {
|
plans: {
|
||||||
growth: {
|
growth: {
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ describe('PaymentController', () => {
|
|||||||
}
|
}
|
||||||
mockPricingService = {
|
mockPricingService = {
|
||||||
getConfig: jest.fn().mockResolvedValue({
|
getConfig: jest.fn().mockResolvedValue({
|
||||||
interview: { pricePerSession: 500, creditsPerPurchase: 1 },
|
interview: { pricePerSession: 600, creditsPerPurchase: 1 },
|
||||||
resumeOptimize: { freeLimit: 3, pricePerOptimize: 300, creditsPerPurchase: 1 },
|
resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 },
|
||||||
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
||||||
plans: {
|
plans: {
|
||||||
growth: { price: 1990, durationDays: 30, gravityPerMonth: 250, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: [] },
|
growth: { price: 1990, durationDays: 30, gravityPerMonth: 80, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: [] },
|
||||||
sprint: { price: 4990, durationDays: 30, gravityPerMonth: 600, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: [] },
|
sprint: { price: 4990, durationDays: 30, gravityPerMonth: 200, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: [] },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ describe('PaymentController', () => {
|
|||||||
expect(result.plan).toBe('growth')
|
expect(result.plan).toBe('growth')
|
||||||
expect(mockUser.save).toHaveBeenCalled()
|
expect(mockUser.save).toHaveBeenCalled()
|
||||||
expect(mockUser.plan).toBe('growth')
|
expect(mockUser.plan).toBe('growth')
|
||||||
expect(mockUser.gravity).toBe(250)
|
expect(mockUser.gravity).toBe(80)
|
||||||
expect(mockUser.freeOptimizeUsed).toBe(3)
|
expect(mockUser.freeOptimizeUsed).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -81,31 +81,41 @@ export function useGravityPurchase(onPaymentSuccess?: () => void) {
|
|||||||
|
|
||||||
if (isMp.value) {
|
if (isMp.value) {
|
||||||
try {
|
try {
|
||||||
let res = await uni.request({
|
const loginRes = await new Promise<{ code: string }>((resolve, reject) => {
|
||||||
url: api('/payment/jsapi-product'), method: 'POST',
|
uni.login({ provider: 'weixin', success: r => resolve(r), fail: reject })
|
||||||
data: { type, quantity },
|
})
|
||||||
|
if (!loginRes.code) {
|
||||||
|
payLoading.value = false
|
||||||
|
payError.value = '微信登录失败,请重试'
|
||||||
|
uni.showToast({ title: '微信登录失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await uni.request({
|
||||||
|
url: api('/virtual-payment/create'), method: 'POST',
|
||||||
|
data: { type, quantity, wxCode: loginRes.code },
|
||||||
header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
})
|
})
|
||||||
payLoading.value = false
|
payLoading.value = false
|
||||||
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.payParams) {
|
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) {
|
||||||
const pp = res.data.payParams as any
|
const vp = res.data as any
|
||||||
currentOutTradeNo.value = res.data.outTradeNo || ''
|
currentOutTradeNo.value = vp.outTradeNo
|
||||||
uni.requestPayment({
|
|
||||||
provider: 'wxpay',
|
;(wx as any).requestVirtualPayment({
|
||||||
timeStamp: pp.timeStamp,
|
env: vp.env,
|
||||||
nonceStr: pp.nonceStr,
|
offerId: vp.offerId,
|
||||||
package: pp.package,
|
signData: vp.signData,
|
||||||
signType: pp.signType || 'RSA',
|
paySig: vp.paySig,
|
||||||
paySign: pp.paySign,
|
signature: vp.signature,
|
||||||
success: () => {
|
success: () => {
|
||||||
const no = currentOutTradeNo.value || res.data.outTradeNo
|
const no = currentOutTradeNo.value || vp.outTradeNo
|
||||||
pollPayResult(no)
|
pollPayResult(no)
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: (err: any) => {
|
||||||
payError.value = '支付未完成'
|
payError.value = '支付未完成'
|
||||||
uni.showToast({ title: '支付未完成', icon: 'none' })
|
uni.showToast({ title: err?.errMsg || '支付未完成', icon: 'none' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (!res.statusCode || res.statusCode === 0) {
|
} else if (!res.statusCode || res.statusCode === 0) {
|
||||||
@@ -116,9 +126,9 @@ export function useGravityPurchase(onPaymentSuccess?: () => void) {
|
|||||||
payError.value = errMsg
|
payError.value = errMsg
|
||||||
uni.showToast({ title: errMsg, icon: 'none' })
|
uni.showToast({ title: errMsg, icon: 'none' })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
payLoading.value = false
|
payLoading.value = false
|
||||||
payError.value = '网络错误,请重试'
|
payError.value = e?.errMsg || '网络错误,请重试'
|
||||||
uni.showToast({ title: '网络错误', icon: 'none' })
|
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ export const API_ENDPOINTS = {
|
|||||||
CHAT: '/career-advice/chat',
|
CHAT: '/career-advice/chat',
|
||||||
POSITIONS: '/career-advice/positions',
|
POSITIONS: '/career-advice/positions',
|
||||||
},
|
},
|
||||||
|
VIRTUAL_PAYMENT: {
|
||||||
|
CREATE: '/virtual-payment/create',
|
||||||
|
CHECK: (outTradeNo: string) => `/virtual-payment/check/${outTradeNo}`,
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn'
|
const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn'
|
||||||
|
|||||||
@@ -90,22 +90,22 @@
|
|||||||
|
|
||||||
<view class="complete-bar" v-else>
|
<view class="complete-bar" v-else>
|
||||||
<button class="cta-btn" @click="goResult">查看面试报告</button>
|
<button class="cta-btn" @click="goResult">查看面试报告</button>
|
||||||
<button class="buy-btn" v-if="completedReason === 'noCredits'" @click="goH5Buy">引力值不足,官网购买 ›</button>
|
<button class="buy-btn" v-if="completedReason === 'noCredits'" @click="goGravityBuy">引力值不足,立即补充 ›</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 官网购买弹窗 -->
|
<!-- 引力值不足弹窗 -->
|
||||||
<view class="modal-overlay" v-if="showH5BuyModal" @click="showH5BuyModal = false">
|
<view class="modal-overlay" v-if="showGravityBuyModal" @click="showGravityBuyModal = false">
|
||||||
<view class="modal-content" @click.stop>
|
<view class="modal-content" @click.stop>
|
||||||
<text class="modal-title">引力值不足</text>
|
<text class="modal-title">引力值不足</text>
|
||||||
<text class="modal-hint">您的引力值不足,请补充后继续面试(每次面试消耗 5 引力值)</text>
|
<text class="modal-hint">您的引力值不足,请补充后继续面试(每次面试消耗 5 引力值)</text>
|
||||||
<view class="purchase-options">
|
<view class="purchase-options">
|
||||||
<view class="purchase-option" @click="goH5BuyAndClose">
|
<view class="purchase-option" @click="goGravityBuyAndClose">
|
||||||
<text class="purchase-name">官网购买引力值</text>
|
<text class="purchase-name">购买引力值</text>
|
||||||
<text class="purchase-price">前往网页版充值</text>
|
<text class="purchase-price">前往充值</text>
|
||||||
<text class="purchase-desc">打开官网 H5 页面,支持多种支付方式</text>
|
<text class="purchase-desc">小程序内直接购买,微信支付安全便捷</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="modal-close" @click="showH5BuyModal = false">取消</text>
|
<text class="modal-close" @click="showGravityBuyModal = false">取消</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -136,7 +136,7 @@ const scrollToId = ref('')
|
|||||||
const position = ref('')
|
const position = ref('')
|
||||||
const avatarMode = ref(true)
|
const avatarMode = ref(true)
|
||||||
const showPositionPicker = ref(false)
|
const showPositionPicker = ref(false)
|
||||||
const showH5BuyModal = ref(false)
|
const showGravityBuyModal = ref(false)
|
||||||
const positions = ref([])
|
const positions = ref([])
|
||||||
const positionsLoading = ref(false)
|
const positionsLoading = ref(false)
|
||||||
const aiSpeechText = ref('')
|
const aiSpeechText = ref('')
|
||||||
@@ -326,25 +326,13 @@ function onAvatarSilent() {
|
|||||||
|
|
||||||
const goResult = () => uni.navigateTo({ url: `/pages/report/report?interviewId=${interviewId.value}` })
|
const goResult = () => uni.navigateTo({ url: `/pages/report/report?interviewId=${interviewId.value}` })
|
||||||
|
|
||||||
// 官网购买引力值
|
// 购买引力值
|
||||||
const goH5Buy = () => {
|
const goGravityBuy = () => {
|
||||||
showH5BuyModal.value = true
|
showGravityBuyModal.value = true
|
||||||
}
|
}
|
||||||
const goH5BuyAndClose = () => {
|
const goGravityBuyAndClose = () => {
|
||||||
showH5BuyModal.value = false
|
showGravityBuyModal.value = false
|
||||||
const token = uni.getStorageSync('token') || ''
|
uni.navigateTo({ url: '/pages/member/member' })
|
||||||
const url = `https://zhiyin.yzrcloud.cn/?buy=gravity${token ? '&token=' + token : ''}`
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: url,
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({ title: '链接已复制,请在手机浏览器中打开', icon: 'none', duration: 3000 })
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
uni.showToast({ title: '复制失败,请手动访问 zhiyin.yzrcloud.cn', icon: 'none', duration: 3000 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// #endif
|
|
||||||
}
|
}
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
nextTick(() => { scrollToId.value = ''; setTimeout(() => { scrollToId.value = 'msg-bottom' }, 100) })
|
nextTick(() => { scrollToId.value = ''; setTimeout(() => { scrollToId.value = 'msg-bottom' }, 100) })
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<view class="gravity-actions">
|
<view class="gravity-actions">
|
||||||
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
|
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
|
||||||
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
|
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
|
||||||
<text class="gravity-btn h5buy" @click="goH5Buy">官网购买</text>
|
<text class="gravity-btn h5buy" @click="goH5Buy">购买引力值</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -77,14 +77,11 @@
|
|||||||
<text class="menu-text">面试复盘</text>
|
<text class="menu-text">面试复盘</text>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 会员中心菜单注释隐藏(微信审核) -->
|
|
||||||
<!--
|
|
||||||
<view class="menu-item" @click="goVip">
|
<view class="menu-item" @click="goVip">
|
||||||
<view class="menu-icon-wrap wrap-purple"><text class="menu-icon">💎</text></view>
|
<view class="menu-icon-wrap wrap-purple"><text class="menu-icon">💎</text></view>
|
||||||
<text class="menu-text">会员中心</text>
|
<text class="menu-text">会员中心</text>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
-->
|
|
||||||
<view class="menu-item" @click="requireLogin(goResume, '我的简历')">
|
<view class="menu-item" @click="requireLogin(goResume, '我的简历')">
|
||||||
<view class="menu-icon-wrap wrap-green"><text class="menu-icon">📄</text></view>
|
<view class="menu-icon-wrap wrap-green"><text class="menu-icon">📄</text></view>
|
||||||
<text class="menu-text">我的简历</text>
|
<text class="menu-text">我的简历</text>
|
||||||
@@ -148,12 +145,12 @@
|
|||||||
<text class="gp-method-arrow">›</text>
|
<text class="gp-method-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 官网购买 -->
|
<!-- 购买引力值 -->
|
||||||
<view class="gp-get-method" @click="goH5Buy">
|
<view class="gp-get-method" @click="goH5Buy">
|
||||||
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
|
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
|
||||||
<view class="gp-method-info">
|
<view class="gp-method-info">
|
||||||
<text class="gp-method-name">官网购买</text>
|
<text class="gp-method-name">购买引力值</text>
|
||||||
<text class="gp-method-desc">通过官网网页端可购买引力值套餐,支持更多支付方式</text>
|
<text class="gp-method-desc">小程序内直接购买,微信支付安全便捷</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="gp-method-arrow">›</text>
|
<text class="gp-method-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -278,22 +275,7 @@ const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
|
|||||||
const showGetGravityModal = ref(false)
|
const showGetGravityModal = ref(false)
|
||||||
const openBuyModal = () => { showGetGravityModal.value = true }
|
const openBuyModal = () => { showGetGravityModal.value = true }
|
||||||
const goH5Buy = () => {
|
const goH5Buy = () => {
|
||||||
const token = uni.getStorageSync('token') || ''
|
|
||||||
const url = `https://zhiyin.yzrcloud.cn/?buy=gravity${token ? '&token=' + token : ''}`
|
|
||||||
// #ifdef H5
|
|
||||||
uni.navigateTo({ url: '/pages/member/member' })
|
uni.navigateTo({ url: '/pages/member/member' })
|
||||||
// #endif
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: url,
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({ title: '链接已复制,请在手机浏览器中打开', icon: 'none', duration: 3000 })
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
uni.showToast({ title: '复制失败,请手动访问 zhiyin.yzrcloud.cn', icon: 'none', duration: 3000 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// #endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
|
const goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ const apiService = {
|
|||||||
request(API_ENDPOINTS.CAREER.CHAT, 'POST', { message, history }, true),
|
request(API_ENDPOINTS.CAREER.CHAT, 'POST', { message, history }, true),
|
||||||
positions: () => request(API_ENDPOINTS.CAREER.POSITIONS, 'GET', undefined, true),
|
positions: () => request(API_ENDPOINTS.CAREER.POSITIONS, 'GET', undefined, true),
|
||||||
},
|
},
|
||||||
|
virtualPayment: {
|
||||||
|
create: (data: { type: string; quantity: number; wxCode: string }) =>
|
||||||
|
request(API_ENDPOINTS.VIRTUAL_PAYMENT.CREATE, 'POST', data, true),
|
||||||
|
check: (outTradeNo: string) =>
|
||||||
|
request(API_ENDPOINTS.VIRTUAL_PAYMENT.CHECK(outTradeNo), 'GET', undefined, true),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|||||||
Reference in New Issue
Block a user