fix(login): needUserInfo 授权改用 wx.getUserInfo(后端文档约定),暂缓手机号授权流程

This commit is contained in:
ozh 2026-09-16 15:28:49 +08:00
parent 026578dfbd
commit 87e4031fff
4 changed files with 36 additions and 99 deletions

View File

@ -63,27 +63,6 @@
margin-top: 32px;
}
// 手机号授权用原生 Button(open-type=getPhoneNumber),抹平默认样式与 .submit 对齐
.submitBtn {
height: 96px;
margin-top: 32px;
background: $color-primary-grad;
border-radius: 48px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 32px;
font-weight: 600;
line-height: 1;
padding: 0;
border: none;
&::after {
border: none;
}
}
.submitText {
color: #ffffff;
font-size: 32px;

View File

@ -1,15 +1,9 @@
import { useState } from 'react'
import { View, Text, Button } from '@tarojs/components'
import { View, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import { useAppDispatch, useAppSelector } from '@/store/hooks'
import { setAuth } from '@/store/authSlice'
import {
performWxLogin,
completeUserInfoLogin,
completeMobileLogin,
getLoginSubState,
type LoginSubState
} from './wxLoginFlow'
import { performWxLogin, completeUserInfoLogin, getLoginSubState } from './wxLoginFlow'
import { useToast } from '@/hooks/useToast'
import { ROUTES } from '@/constants/routes'
import { reLaunch } from '@/utils/navigation'
@ -22,8 +16,8 @@ export default function Login() {
const toast = useToast()
const token = useAppSelector((s) => s.auth.token)
const [loading, setLoading] = useState(false)
// 注册中间态:needUserInfo(待头像昵称授权)→ noMobile(待手机号授权)→ null(初始/完成)
const [subState, setSubState] = useState<LoginSubState | null>(null)
// 注册中间态:needUserInfo(待用户资料授权)→ null(初始/完成)
const [needAuth, setNeedAuth] = useState(false)
const [openId, setOpenId] = useState('')
// 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5)
@ -31,25 +25,27 @@ export default function Login() {
if (token && !loading) reLaunch(ROUTES.HOME)
})
// 完整登录态校验与落地;未完成时推进 subState
// 完整登录态校验与落地;未完成时推进授权态
const applyResult = async (result: Awaited<ReturnType<typeof performWxLogin>>) => {
const state = getLoginSubState(result)
if (state !== 'complete') {
console.log('[login-page] 注册未完成,推进到', state)
// openId 兜底链:noMobile 阶段后端把 openId 放在 bindKey 字段返回(联调实测);
// 其余阶段从本地缓存(needUserInfo 时已写入)兜底
if (state === 'noMobile') {
// 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续
throw new Error('后端要求绑定手机号,该流程暂未开通')
}
// openId 兜底链:响应 openId → 本地缓存(needUserInfo 首次已写入)→ 当前 state
const openIdNext =
result.openId ?? (state === 'noMobile' ? result.bindKey : undefined) ??
storage.get<string>(WX_OPENID_STORAGE_KEY) ?? openId
result.openId ?? storage.get<string>(WX_OPENID_STORAGE_KEY) ?? openId
if (!openIdNext) throw new Error('响应缺少 openId,无法继续授权')
setOpenId(openIdNext)
setSubState(state)
setNeedAuth(true)
setLoading(false)
return
}
if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证')
console.log('[login-page] 登录完成,token 已落地')
setSubState(null)
setNeedAuth(false)
dispatch(setAuth({ token: result.code, userInfo: result.userInfo }))
await reLaunch(ROUTES.HOME)
}
@ -67,50 +63,20 @@ export default function Login() {
}
}
// needUserInfo:getUserProfile 授权头像昵称
// needUserInfo:getUserInfo 授权用户资料(后端文档约定的授权接口)
const handleAuthUserInfo = async () => {
if (loading || !openId) return
setLoading(true)
try {
applyResult(await completeUserInfoLogin(openId))
} catch (e) {
// 授权失败/取消:保留待授权态,用户可重试(getUserProfile 每次都需用户确认)
// 授权失败/取消:保留待授权态,用户可重试
console.error('[login-page] completeUserInfoLogin 失败', e)
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
setLoading(false)
}
}
// noMobile:手机号授权按钮回调(Button open-type=getPhoneNumber)
const handleGetPhoneNumber = async (e: {
detail: { iv: string; encryptedData: string; code?: string; errMsg?: string }
}) => {
console.log('[login-page] 手机号授权回调', {
errMsg: e.detail.errMsg,
// 两种手机号回传形态:新版 code(动态令牌)或旧版 iv/encryptedData(加密数据)
hasCode: Boolean(e.detail.code),
hasEncrypted: Boolean(e.detail.encryptedData)
})
if (loading || !openId) return
setLoading(true)
try {
applyResult(await completeMobileLogin(openId, e.detail))
} catch (err) {
console.error('[login-page] completeMobileLogin 失败', err)
toast.error(err instanceof Error && err.message ? err.message : '授权失败')
setLoading(false)
}
}
const submitText = () => {
if (loading) {
return subState === 'noMobile' ? '绑定中...' : subState === 'needUserInfo' ? '授权中...' : '登录中...'
}
if (subState === 'needUserInfo') return '授权头像昵称,继续'
if (subState === 'noMobile') return '授权手机号,完成注册'
return '微信一键登录'
}
return (
<View className={styles.page}>
<View className={styles.hero}>
@ -121,23 +87,14 @@ export default function Login() {
<Text className={styles.slogan}>一起守护家人</Text>
</View>
<View className={styles.form}>
{subState === 'noMobile' ? (
// 手机号授权必须用原生 Button open-type,微信弹官方授权框
<Button
className={styles.submitBtn}
openType="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
>
{submitText()}
</Button>
) : (
<View
className={styles.submit}
onClick={subState === 'needUserInfo' ? handleAuthUserInfo : handleWxLogin}
>
<Text className={styles.submitText}>{submitText()}</Text>
</View>
)}
<View
className={styles.submit}
onClick={needAuth ? handleAuthUserInfo : handleWxLogin}
>
<Text className={styles.submitText}>
{loading ? '登录中...' : needAuth ? '授权用户信息,继续' : '微信一键登录'}
</Text>
</View>
</View>
</View>
)

View File

@ -62,11 +62,12 @@ async function reloginWith(
return result
}
// needUserInfo 二次授权:getUserProfile 拿 iv/encryptedData → 重调登录接口补用户资料
// needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料
// 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认)
export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLoginResponse> {
let profile: { iv: string; encryptedData: string }
try {
const r = await Taro.getUserProfile({ desc: '用于完善会员资料' })
const r = await Taro.getUserInfo({ withCredentials: true })
profile = { iv: r.iv, encryptedData: r.encryptedData }
} catch (e) {
throw new Error(e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg || '授权失败')
@ -75,8 +76,7 @@ export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLo
}
// noMobile 三次授权:手机号授权按钮回调(open-type=getPhoneNumber)→ 重调登录接口
// detail 两种形态:新版含 code(动态令牌,后端可直连微信换取手机号);
// 旧版含 iv/encryptedData(需与 session_key 解密,依赖同一 code 会话)
// 暂缓接入(后端读取字段未定,等联调确认);保留实现以便接续
export async function completeMobileLogin(
openId: string,
detail: { iv: string; encryptedData: string; code?: string; errMsg?: string }

View File

@ -2,12 +2,12 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
const mockLogin = vi.hoisted(() => vi.fn())
const mockGetAccountInfo = vi.hoisted(() => vi.fn())
const mockGetUserProfile = vi.hoisted(() => vi.fn())
const mockGetUserInfo = vi.hoisted(() => vi.fn())
vi.mock('@tarojs/taro', () => ({
default: {
login: mockLogin,
getAccountInfoSync: mockGetAccountInfo,
getUserProfile: mockGetUserProfile
getUserInfo: mockGetUserInfo
}
}))
@ -51,7 +51,7 @@ beforeEach(() => {
mem.clear()
mockLogin.mockReset()
mockGetAccountInfo.mockReset()
mockGetUserProfile.mockReset()
mockGetUserInfo.mockReset()
mockWxMiniappLogin.mockReset()
mockLogin.mockResolvedValue({ code: 'wx-code-1' })
mockGetAccountInfo.mockReturnValue({ miniProgram: { appId: 'wx-appid-live' } })
@ -136,11 +136,12 @@ describe('needUserInfo 首次登录分支', () => {
})
describe('completeUserInfoLogin 二次授权', () => {
it('getUserProfile → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => {
mockGetUserProfile.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => {
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' }))
const res = await completeUserInfoLogin('o-need')
expect(res.code).toBe('tk-abc')
expect(mockGetUserInfo).toHaveBeenCalledWith({ withCredentials: true })
expect(mockWxMiniappLogin).toHaveBeenCalledWith({
appId: 'wx-appid-live',
code: 'wx-code-1',
@ -151,13 +152,13 @@ describe('completeUserInfoLogin 二次授权', () => {
})
it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => {
mockGetUserProfile.mockRejectedValue({ errMsg: 'getUserProfile:fail auth deny' })
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserProfile:fail auth deny')
mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' })
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny')
expect(mockWxMiniappLogin).not.toHaveBeenCalled()
})
it('授权成功但换 code 失败:抛错且不发起登录请求', async () => {
mockGetUserProfile.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
mockLogin.mockRejectedValue({ errMsg: 'login:fail' })
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('获取微信登录凭证失败,请重试')
expect(mockWxMiniappLogin).not.toHaveBeenCalled()