feat(pay): 优化小程序支付流程并增强日志调试
- 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象 - 支付页面新增大量日志,详尽打印参数、请求和响应信息 - 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试 - 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录 - 增加自动登录失败交互,提示未注册用户先登录 - 支付相关请求均使用 returnRaw 方便错误排查 - request 请求模块添加统一请求和响应日志,增强调试能力 - request 认证错误和业务错误时加入详细日志输出
This commit is contained in:
15
.workbuddy/memory/2026-07-01.md
Normal file
15
.workbuddy/memory/2026-07-01.md
Normal file
@@ -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'`
|
||||
@@ -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<ApiResult<{
|
||||
openid: string;
|
||||
unionid?: string;
|
||||
session_key?: string;
|
||||
}>>(
|
||||
const res = await request.post<ApiResult<WxLoginResult>>(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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 = <T>(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 = <T>(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 = <T>(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,
|
||||
|
||||
Reference in New Issue
Block a user