diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1814ae8..92e2c8c 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -28,6 +28,7 @@ import { PricingModule } from './modules/schemas/pricing.module' import { ShareModule } from './modules/share/share.module' import { InterviewReviewModule } from './modules/interview-review/interview-review.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' @@ -64,6 +65,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin ShareModule, InterviewReviewModule, CareerAdviceModule, + VirtualPaymentModule, ], providers: [ JwtStrategy, diff --git a/backend/src/modules/admin/admin.controller.ts b/backend/src/modules/admin/admin.controller.ts index 52c6bdd..f431935 100644 --- a/backend/src/modules/admin/admin.controller.ts +++ b/backend/src/modules/admin/admin.controller.ts @@ -428,8 +428,8 @@ const DEFAULT_CONFIG = { } const DEFAULT_PRICING = { - interview: { pricePerSession: 500, creditsPerPurchase: 1 }, - resumeOptimize: { freeLimit: 3, pricePerOptimize: 300, creditsPerPurchase: 1 }, + interview: { pricePerSession: 600, creditsPerPurchase: 1 }, + resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 }, resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 }, plans: { growth: { diff --git a/backend/src/modules/payment/payment.controller.spec.ts b/backend/src/modules/payment/payment.controller.spec.ts index ddf5efc..63ae7c7 100644 --- a/backend/src/modules/payment/payment.controller.spec.ts +++ b/backend/src/modules/payment/payment.controller.spec.ts @@ -36,12 +36,12 @@ describe('PaymentController', () => { } mockPricingService = { getConfig: jest.fn().mockResolvedValue({ - interview: { pricePerSession: 500, creditsPerPurchase: 1 }, - resumeOptimize: { freeLimit: 3, pricePerOptimize: 300, creditsPerPurchase: 1 }, + interview: { pricePerSession: 600, creditsPerPurchase: 1 }, + resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 }, resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 }, plans: { - growth: { price: 1990, durationDays: 30, gravityPerMonth: 250, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: [] }, - sprint: { price: 4990, durationDays: 30, gravityPerMonth: 600, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: [] }, + growth: { price: 1990, durationDays: 30, gravityPerMonth: 80, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, 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(mockUser.save).toHaveBeenCalled() expect(mockUser.plan).toBe('growth') - expect(mockUser.gravity).toBe(250) + expect(mockUser.gravity).toBe(80) expect(mockUser.freeOptimizeUsed).toBe(3) }) diff --git a/zhiyin-app/src/composables/useGravityPurchase.ts b/zhiyin-app/src/composables/useGravityPurchase.ts index 3bc5691..240d95b 100644 --- a/zhiyin-app/src/composables/useGravityPurchase.ts +++ b/zhiyin-app/src/composables/useGravityPurchase.ts @@ -81,31 +81,41 @@ export function useGravityPurchase(onPaymentSuccess?: () => void) { if (isMp.value) { try { - let res = await uni.request({ - url: api('/payment/jsapi-product'), method: 'POST', - data: { type, quantity }, + const loginRes = await new Promise<{ code: string }>((resolve, reject) => { + uni.login({ provider: 'weixin', success: r => resolve(r), fail: reject }) + }) + 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' }, timeout: 30000, }) payLoading.value = false - if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.payParams) { - const pp = res.data.payParams as any - currentOutTradeNo.value = res.data.outTradeNo || '' - uni.requestPayment({ - provider: 'wxpay', - timeStamp: pp.timeStamp, - nonceStr: pp.nonceStr, - package: pp.package, - signType: pp.signType || 'RSA', - paySign: pp.paySign, + if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) { + const vp = res.data as any + currentOutTradeNo.value = vp.outTradeNo + + ;(wx as any).requestVirtualPayment({ + env: vp.env, + offerId: vp.offerId, + signData: vp.signData, + paySig: vp.paySig, + signature: vp.signature, success: () => { - const no = currentOutTradeNo.value || res.data.outTradeNo + const no = currentOutTradeNo.value || vp.outTradeNo pollPayResult(no) }, - fail: () => { + fail: (err: any) => { payError.value = '支付未完成' - uni.showToast({ title: '支付未完成', icon: 'none' }) + uni.showToast({ title: err?.errMsg || '支付未完成', icon: 'none' }) }, }) } else if (!res.statusCode || res.statusCode === 0) { @@ -116,9 +126,9 @@ export function useGravityPurchase(onPaymentSuccess?: () => void) { payError.value = errMsg uni.showToast({ title: errMsg, icon: 'none' }) } - } catch (e) { + } catch (e: any) { payLoading.value = false - payError.value = '网络错误,请重试' + payError.value = e?.errMsg || '网络错误,请重试' uni.showToast({ title: '网络错误', icon: 'none' }) } } else { diff --git a/zhiyin-app/src/config.ts b/zhiyin-app/src/config.ts index 602b50a..6e0b2d9 100644 --- a/zhiyin-app/src/config.ts +++ b/zhiyin-app/src/config.ts @@ -122,6 +122,10 @@ export const API_ENDPOINTS = { CHAT: '/career-advice/chat', POSITIONS: '/career-advice/positions', }, + VIRTUAL_PAYMENT: { + CREATE: '/virtual-payment/create', + CHECK: (outTradeNo: string) => `/virtual-payment/check/${outTradeNo}`, + }, } as const const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn' diff --git a/zhiyin-app/src/pages/interview/interview.vue b/zhiyin-app/src/pages/interview/interview.vue index a4e9152..24e0bcf 100644 --- a/zhiyin-app/src/pages/interview/interview.vue +++ b/zhiyin-app/src/pages/interview/interview.vue @@ -90,22 +90,22 @@ - + - - + + 引力值不足 您的引力值不足,请补充后继续面试(每次面试消耗 5 引力值) - - 官网购买引力值 - 前往网页版充值 - 打开官网 H5 页面,支持多种支付方式 + + 购买引力值 + 前往充值 + 小程序内直接购买,微信支付安全便捷 - 取消 + 取消 @@ -136,7 +136,7 @@ const scrollToId = ref('') const position = ref('') const avatarMode = ref(true) const showPositionPicker = ref(false) -const showH5BuyModal = ref(false) +const showGravityBuyModal = ref(false) const positions = ref([]) const positionsLoading = ref(false) const aiSpeechText = ref('') @@ -326,25 +326,13 @@ function onAvatarSilent() { const goResult = () => uni.navigateTo({ url: `/pages/report/report?interviewId=${interviewId.value}` }) -// 官网购买引力值 -const goH5Buy = () => { - showH5BuyModal.value = true +// 购买引力值 +const goGravityBuy = () => { + showGravityBuyModal.value = true } -const goH5BuyAndClose = () => { - showH5BuyModal.value = false - const token = uni.getStorageSync('token') || '' - 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 goGravityBuyAndClose = () => { + showGravityBuyModal.value = false + uni.navigateTo({ url: '/pages/member/member' }) } const scrollToBottom = () => { nextTick(() => { scrollToId.value = ''; setTimeout(() => { scrollToId.value = 'msg-bottom' }, 100) }) diff --git a/zhiyin-app/src/pages/user/user.vue b/zhiyin-app/src/pages/user/user.vue index ee8d702..a0b843f 100644 --- a/zhiyin-app/src/pages/user/user.vue +++ b/zhiyin-app/src/pages/user/user.vue @@ -53,7 +53,7 @@ 贡献面经 - 官网购买 + 购买引力值 @@ -77,14 +77,11 @@ 面试复盘 - - 📄 我的简历 @@ -148,12 +145,12 @@ - + 🛒 - 官网购买 - 通过官网网页端可购买引力值套餐,支持更多支付方式 + 购买引力值 + 小程序内直接购买,微信支付安全便捷 @@ -278,22 +275,7 @@ const goLogin = () => uni.navigateTo({ url: '/pages/login/login' }) const showGetGravityModal = ref(false) const openBuyModal = () => { showGetGravityModal.value = true } 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' }) - // #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' }) diff --git a/zhiyin-app/src/services/api.ts b/zhiyin-app/src/services/api.ts index cc5c821..9bd2d9a 100644 --- a/zhiyin-app/src/services/api.ts +++ b/zhiyin-app/src/services/api.ts @@ -105,6 +105,12 @@ const apiService = { request(API_ENDPOINTS.CAREER.CHAT, 'POST', { message, history }, 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