diff --git a/server/api/auth/wechat/login.post.ts b/server/api/auth/wechat/login.post.ts index 8bb82d5..617028c 100644 --- a/server/api/auth/wechat/login.post.ts +++ b/server/api/auth/wechat/login.post.ts @@ -44,27 +44,86 @@ export default defineEventHandler(async (event) => { } // 1. 用 code 换取 openid - let session: Jscode2SessionResult + let session: any try { - session = await $fetch( - 'https://api.weixin.qq.com/sns/jscode2session', - { - query: { appid, secret, js_code: code, grant_type: 'authorization_code' }, - timeout: 8000 - } - ) + session = await $fetch('https://api.weixin.qq.com/sns/jscode2session', { + query: { appid, secret, js_code: code, grant_type: 'authorization_code' }, + timeout: 8000, + /* + * ★★★ 关键修复(线上「微信登录一直 401」的真正根因)★★★ + * + * ofetch 按响应的 **content-type** 决定要不要解析 JSON。 + * 微信的 jscode2session 返回的 content-type 不是 application/json, + * 于是 ofetch **原样返回字符串**,`session` 就成了 string 而不是对象 —— + * 结果 session.openid / errcode / errmsg **全是 undefined**, + * 代码走到 `if (!session.openid)` 抛 401,日志打印「undefined undefined」, + * 用户看到「微信登录失败:未知错误(undefined)」。 + * + * 实测(容器内直接跑 ofetch): + * GOT type=string "{\"errcode\":40029,\"errmsg\":\"invalid code\"}" + * + * 显式声明 responseType: 'json' 后才会解析成对象。 + */ + responseType: 'json' + }) } catch (e: any) { console.error('[WeChatMP] jscode2session 请求失败:', e?.message) throw createError({ statusCode: 502, message: '微信登录服务暂时不可用,请稍后重试' }) } + // 兜底:万一某天 responseType 仍没生效(或上游改了 content-type),手动解析一次, + // 不要再让「字符串被当对象用」这种问题静默成 undefined。 + if (typeof session === 'string') { + try { + session = JSON.parse(session) + } catch { + console.error('[WeChatMP] 响应无法解析为 JSON:', String(session).slice(0, 300)) + session = null + } + } + + /* + * ⚠️ 诊断日志(这次是踩坑加的)。 + * + * 线上曾出现 `[WeChatMP] jscode2session 返回异常: undefined undefined` —— + * errcode / errmsg **双 undefined**。这不是「appid 错了」: + * 微信出错时会返回 { errcode, errmsg },两个都取不到说明**响应体根本不是微信的 JSON**。 + * 最可能是 $fetch 拿回了一段非 JSON 文本(被代理/防火墙拦截返回的 HTML、 + * 或重定向页),此时 ofetch 解析失败会把原始字符串返回,于是所有字段都是 undefined。 + * + * 所以这里把**响应的真实类型和内容**打出来,否则下次还是只能看到 undefined。 + * appid 打前 6 位后 4 位,用于和微信公众平台里的小程序 AppID 核对(两者必须完全一致, + * 且**不能**填公众号的 AppID —— 那套凭据不通用)。 + */ + const appidMasked = + appid.length > 10 ? appid.slice(0, 6) + '****' + appid.slice(-4) : appid.slice(0, 3) + '****' + console.error( + '[WeChatMP] 诊断: appid=' + appidMasked + + ' secretLen=' + String(secret).length + + ' 响应类型=' + typeof session + + ' 是否数组=' + Array.isArray(session) + + ' 顶层键=' + (session && typeof session === 'object' ? Object.keys(session).join(',') : '(非对象)') + ) + console.error( + '[WeChatMP] 诊断: 原始响应=' + + (typeof session === 'string' + ? session.slice(0, 500) + : JSON.stringify(session)?.slice(0, 500)) + ) + // 微信接口失败时仍返回 HTTP 200,必须检查 openid - if (!session.openid) { - console.error('[WeChatMP] jscode2session 返回异常:', session.errcode, session.errmsg) - throw createError({ - statusCode: 401, - message: `微信登录失败:${session.errmsg || '未知错误(' + session.errcode + ')'}` - }) + if (!session || typeof session !== 'object' || !session.openid) { + // 区分「微信明确报错」和「响应体不是微信的 JSON」——两者排查方向完全不同 + const notJson = !session || typeof session !== 'object' + const errcode = session && session.errcode + const errmsg = session && session.errmsg + console.error('[WeChatMP] jscode2session 返回异常:', errcode, errmsg, notJson ? '(响应非 JSON)' : '') + + const message = notJson + ? '微信登录失败:服务器访问微信接口时未收到合法响应(可能被网络策略拦截),请查看服务端日志' + : `微信登录失败:${errmsg || '未知错误(' + (errcode ?? '无错误码') + ')'}` + + throw createError({ statusCode: 401, message }) } const openid = session.openid