From 1d5bb5b9b19085d77befe9a6953fd3aa8afa1922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 2 Jul 2026 16:01:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(pay):=20=E4=BC=98=E5=8C=96=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E6=94=AF=E4=BB=98=E6=B5=81=E7=A8=8B=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=97=A5=E5=BF=97=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象 - 支付页面新增大量日志,详尽打印参数、请求和响应信息 - 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试 - 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录 - 增加自动登录失败交互,提示未注册用户先登录 - 支付相关请求均使用 returnRaw 方便错误排查 - request 请求模块添加统一请求和响应日志,增强调试能力 - request 认证错误和业务错误时加入详细日志输出 --- .workbuddy/memory/2026-07-01.md | 15 ++++ src/api/passport/wx-login/index.ts | 20 +++-- src/passport/pay/index.tsx | 127 ++++++++++++++++++++++++----- src/utils/request.ts | 13 ++- 4 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 .workbuddy/memory/2026-07-01.md diff --git a/.workbuddy/memory/2026-07-01.md b/.workbuddy/memory/2026-07-01.md new file mode 100644 index 0000000..bdd6a52 --- /dev/null +++ b/.workbuddy/memory/2026-07-01.md @@ -0,0 +1,15 @@ +# 2026-07-01 工作日志 + +## 修复支付页面 request URL 无效问题 +- **文件**: `src/utils/request.ts` +- **问题**: 扫码进入支付页后报错 `request:fail invalid url "/app/subscription/detail-by-no/..."`,原因是核心 `request()` 函数没有调用 `buildUrl()` 拼接 baseUrl,相对路径直接传给微信 `wx.request()` 导致失败 +- **修复**: 在 `request()` 函数中统一加 `buildUrl(options.url)` 处理,确保所有请求方式都能正确拼接完整 URL + +## 小程序码改为体验版 +- **文件**: `src/api/invite/index.ts`(前端) +- **修改**: `generateMiniProgramCode` 函数将 `envVersion` 作为查询参数传给后端,默认值 `'trial'` +- **后端配合**: + - `WxLoginController.java`: `getOrderQRCodeUnlimited` 和 `getOrderQRCode` 方法新增 `@RequestParam(defaultValue = "release") String envVersion` 参数 + - 硬编码的 `"release"` 改为动态读取 `envVersion` 参数 + - `getQRCodeText` 已有此参数,无需修改 +- **注意**: 提审上线前需将前端默认 `envVersion` 改回 `'release'` \ No newline at end of file diff --git a/src/api/passport/wx-login/index.ts b/src/api/passport/wx-login/index.ts index 4213e09..d9f5e4e 100644 --- a/src/api/passport/wx-login/index.ts +++ b/src/api/passport/wx-login/index.ts @@ -90,6 +90,8 @@ export async function loginByOpenId(data: WxLoginParam): Promise<{ /** * 获取微信 OpenId(仅获取,不登录) + * 注意:后端 /wx-login/getOpenId 返回的是 LoginResult { access_token, user }, + * 真正的 openid 在 user.openid 里 */ export async function getOpenId(code: string): Promise<{ success: boolean; @@ -97,22 +99,24 @@ export async function getOpenId(code: string): Promise<{ unionid?: string; session_key?: string; message?: string; + access_token?: string; + user?: WxLoginUserInfo; }> { - const res = await request.post>( + const res = await request.post>( SERVER_API_URL + '/wx-login/getOpenId', { code } ); + console.log('[WxLogin] getOpenId 响应:', res); + if ((res.code === 0 || res.code === 200) && res.data) { return { success: true, - openid: res.data.openid, - unionid: res.data.unionid, - session_key: res.data.session_key + openid: res.data.user?.openid, + unionid: res.data.user?.unionid, + session_key: res.data.user?.session_key, + access_token: res.data.access_token, + user: res.data.user }; } diff --git a/src/passport/pay/index.tsx b/src/passport/pay/index.tsx index 64d5a51..c05240e 100644 --- a/src/passport/pay/index.tsx +++ b/src/passport/pay/index.tsx @@ -4,6 +4,9 @@ import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro' import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import { request } from '@/utils/request' +import { getOpenId, loginByOpenId } from '@/api/passport/wx-login' +import { saveStorageByLoginUser } from '@/utils/server' +import { TenantId } from '@/config/app' /** * 小程序支付页面 @@ -43,42 +46,74 @@ const PayPage: React.FC = () => { const getSubscriptionNo = useCallback(() => { const instance = Taro.getCurrentInstance() const params = instance?.router?.params || {} + console.log('[PayPage] ===== 参数调试开始 =====') + console.log('[PayPage] 原始 params:', JSON.stringify(params)) + console.log('[PayPage] params.scene:', params.scene) + console.log('[PayPage] params.subscriptionNo:', params.subscriptionNo) // 优先读取 scene(小程序码参数),再读取 subscriptionNo(普通链接参数) const scene = params.scene ? decodeURIComponent(params.scene) : '' - return scene || params.subscriptionNo || '' + const subscriptionNo = scene || params.subscriptionNo || '' + console.log('[PayPage] decodeURIComponent(scene):', scene) + console.log('[PayPage] 最终 subscriptionNo:', subscriptionNo) + console.log('[PayPage] ===== 参数调试结束 =====') + return subscriptionNo }, []) // 获取订阅详情 const fetchDetail = useCallback(async () => { const subscriptionNo = getSubscriptionNo() if (!subscriptionNo) { + console.error('[PayPage] subscriptionNo 为空,无法请求') setErrorMsg('缺少订单参数') setLoading(false) return } + console.log('[PayPage] ===== 请求调试开始 =====') + console.log('[PayPage] 请求 URL:', `/app/subscription/detail-by-no/${subscriptionNo}`) + console.log('[PayPage] 当前 token:', Taro.getStorageSync('access_token') || '(无token)') + try { - const res: any = await request({ + // request 默认 returnRaw=false,拦截器已拆包,返回的就是 data 部分(订阅对象) + const data: any = await request({ url: `/app/subscription/detail-by-no/${subscriptionNo}`, - method: 'GET' + method: 'GET', + returnRaw: true // 先用 returnRaw=true 拿到完整响应,方便调试 }) - if (res?.code === 200 || res?.code === 0) { - const data = res.data || res + console.log('[PayPage] 接口完整返回(returnRaw=true):', JSON.stringify(data)) + console.log('[PayPage] data.code:', data?.code) + console.log('[PayPage] data.message:', data?.message) + console.log('[PayPage] data.data:', data?.data) - if (data.status === 'active') { + // 判断业务状态码 + if (data?.code === 0 || data?.code === 200) { + const subscription = data.data || data + console.log('[PayPage] 订阅对象:', JSON.stringify(subscription)) + console.log('[PayPage] 订阅状态:', subscription?.status) + + if (subscription?.status === 'active') { setPaid(true) - setDetail(data) + setDetail(subscription) } else { - setDetail(data) + setDetail(subscription) } } else { - setErrorMsg(res?.message || '获取订单信息失败') + console.error('[PayPage] 业务错误 code:', data?.code, 'message:', data?.message) + setErrorMsg(data?.message || `业务错误(code=${data?.code})`) } } catch (err: any) { + console.error('[PayPage] ===== 请求异常 =====') + console.error('[PayPage] err.name:', err?.name) + console.error('[PayPage] err.type:', err?.type) + console.error('[PayPage] err.code:', err?.code) + console.error('[PayPage] err.message:', err?.message) + console.error('[PayPage] err.data:', err?.data) + console.error('[PayPage] err 完整:', JSON.stringify(err)) setErrorMsg(err?.message || '网络异常,请重试') } finally { setLoading(false) + console.log('[PayPage] ===== 请求调试结束 =====') } }, [getSubscriptionNo]) @@ -92,23 +127,75 @@ const PayPage: React.FC = () => { setPaying(true) try { - // 1. 获取登录凭证(code)并换取 openid + // 1. 获取微信登录凭证(code) const loginRes = await Taro.login() if (!loginRes.code) { throw new Error('获取登录凭证失败') } - // 2. 通过 code 获取 openid(使用现有的 loginByOpenId 接口) - const openid = Taro.getStorageSync('openid') || '' + console.log('[PayPage] Taro.login code:', loginRes.code) - // 3. 请求后端创建 JSAPI 预支付订单 + // 2. 用 code 换取 openid(关键步骤,之前遗漏了) + const openIdRes = await getOpenId(loginRes.code) + console.log('[PayPage] getOpenId 结果:', openIdRes) + + if (!openIdRes.success || !openIdRes.openid) { + throw new Error(openIdRes.message || '获取 openid 失败,请重试') + } + + const openid = openIdRes.openid + // 缓存 openid,后续支付确认等接口可用 + Taro.setStorageSync('openid', openid) + console.log('[PayPage] 获取到 openid:', openid) + + // 3. 尝试自动登录(获取 token,mp-prepay 接口需要登录态) + // 如果用户已在小程序注册过,loginByOpenId 会返回 token + // 如果用户未注册,不影响 openid 已获取,但 mp-prepay 可能需要特殊处理 + try { + const loginResult = await loginByOpenId({ + code: loginRes.code, + tenantId: TenantId + }) + console.log('[PayPage] loginByOpenId 结果:', loginResult) + + if (loginResult.success && loginResult.data) { + // 登录成功,保存 token + saveStorageByLoginUser(loginResult.data.access_token || '', loginResult.data.user) + console.log('[PayPage] 自动登录成功,已保存 token') + } else { + console.warn('[PayPage] 自动登录失败:', loginResult.message) + // 未注册用户:openid 已经有了,但 mp-prepay 需要 userId + // 提示用户先注册/登录 + if (loginResult.message?.includes('未注册') || loginResult.message?.includes('不存在')) { + Taro.showModal({ + title: '需要先登录', + content: '支付前需要先注册账号,是否前往登录?', + confirmText: '去登录', + cancelText: '取消' + }).then(modalRes => { + if (modalRes.confirm) { + Taro.navigateTo({ url: '/passport/login' }) + } + }) + return // 不继续支付流程 + } + } + } catch (loginErr) { + console.warn('[PayPage] 自动登录异常:', loginErr) + // 登录异常但不阻断支付,openid 已拿到,尝试继续 + } + + // 4. 请求后端创建 JSAPI 预支付订单 const prepayRes: any = await request({ url: `/app/subscription/mp-prepay/${detail.id}`, method: 'POST', - data: { openid } + data: { openid }, + returnRaw: true }) - if (prepayRes?.code !== 200 && prepayRes?.code !== 0) { + console.log('[PayPage] mp-prepay 完整返回:', JSON.stringify(prepayRes)) + + if (prepayRes?.code !== 0 && prepayRes?.code !== 200) { throw new Error(prepayRes?.message || '创建支付订单失败') } @@ -118,7 +205,7 @@ const PayPage: React.FC = () => { throw new Error('支付参数不完整') } - // 4. 调用微信支付 + // 5. 调用微信支付 await Taro.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, @@ -127,7 +214,7 @@ const PayPage: React.FC = () => { paySign: payParams.paySign }) - // 5. 支付成功后通知后端确认 + // 6. 支付成功后通知后端确认 try { await request({ url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`, @@ -138,12 +225,14 @@ const PayPage: React.FC = () => { console.warn('支付确认通知失败,等待轮询:', confirmErr) } - // 6. 显示成功状态 + // 7. 显示成功状态 setPaid(true) Taro.showToast({ title: '支付成功', icon: 'success' }) } catch (err: any) { - console.error('支付失败:', err) + console.error('[PayPage] 支付失败:', err) + console.error('[PayPage] err.name:', err?.name, 'err.type:', err?.type) + console.error('[PayPage] err.code:', err?.code, 'err.message:', err?.message) if (err?.errMsg?.includes('cancel')) { Taro.showToast({ title: '支付已取消', icon: 'none' }) diff --git a/src/utils/request.ts b/src/utils/request.ts index 6c8e77d..c0301e0 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -78,6 +78,8 @@ const requestInterceptor = (config: RequestConfig): RequestConfig => { config.header = { ...defaultHeaders, ...config.header }; + console.log('[Request] 发送请求:', { url: config.url, method: config.method, hasToken: !!token, returnRaw: config.returnRaw }); + // 显示加载提示 if (config.showLoading) { Taro.showLoading({ title: '加载中...' }); @@ -95,10 +97,8 @@ const responseInterceptor = (response: any, config: RequestConfig): T => { const { statusCode, data } = response; - // 调试信息(仅开发环境) - if (process.env.NODE_ENV === 'development') { - console.log('API Response:', { statusCode, url: config.url, success: statusCode === 200 }); - } + // 调试信息(所有环境都打印,方便真机排查) + console.log('[Request] 响应:', { statusCode, url: config.url, data: JSON.stringify(data).substring(0, 500) }); // HTTP状态码检查 if (statusCode !== 200) { @@ -139,6 +139,7 @@ const responseInterceptor = (response: any, config: RequestConfig): T => { // 认证错误 if (apiResponse.code === 401 || apiResponse.code === 403) { + console.error('[Request] 认证错误:', { code: apiResponse.code, message: apiResponse.message, url: config.url }); handleAuthError(); throw new RequestError( apiResponse.message || '认证失败', @@ -149,9 +150,7 @@ const responseInterceptor = (response: any, config: RequestConfig): T => { } // 业务错误 - if (process.env.NODE_ENV === 'development') { - console.error('API业务错误:', { code: apiResponse.code, message: apiResponse.message }); - } + console.error('[Request] 业务错误:', { code: apiResponse.code, message: apiResponse.message, url: config.url }); throw new RequestError( apiResponse.message || '请求失败', ErrorType.BUSINESS_ERROR,