Explorar o código

完成linde、apple的第三方登录

npzzwq99 hai 1 semana
pai
achega
da5d4fc159
Modificáronse 3 ficheiros con 163 adicións e 78 borrados
  1. 152 71
      features/passport/social-login.js
  2. 6 6
      pages/passport/lobby.vue
  3. 5 1
      store/modules/user.js

+ 152 - 71
features/passport/social-login.js

@@ -8,19 +8,23 @@
  * 所有接口返回统一格式的会话数据:{ userinfo, statistics, token, ... }
  */
 
-import { post } from '@/core/http/http-client'
+import { get, post } from '@/core/http/http-client'
 import { buildRestPath } from '@/core/config/api-paths'
 import { STORAGE } from '@/core/constants/storage-keys'
 //import { googleLogin as pluginGoogleLogin } from '@/uni_modules/uni-oauth-google'
 
 const ROUTE__LOGIN = buildRestPath('user/third')
+
+const ROUTE__LINE_AUTH_URL = '/line/getAuthUrl'//LINE 授权地址接口
+const ROUTE__LINE_GET_USERINFO = '/line/getuserinfo'//LINE 登录换取用户信息接口
+const FORM_URLENCODED_HEADER = { 'content-type': 'application/x-www-form-urlencoded' }//后端按表单读取 ticket,这里统一声明表单请求头
+const LINE_SUCCESS_PATHS = ['/api/line/loginsuccess', '/line/loginsuccess', '/line/login-success']//LINE 登录成功路径
+const LINE_FAIL_PATHS = ['/api/line/loginfail', '/line/loginfail', '/line/login-fail']//LINE 登录失败路径
 // 谷歌登录配置web client_id 335082713048-el1qosjevi6a8elp80rrq77798msfkij.apps.googleusercontent.com
 //const GOOGLE_SERVER_CLIENT_ID = '335082713048-el1qosjevi6a8elp80rrq77798msfkij.apps.googleusercontent.com'
 /**
  * LINE 登录配置
  */
-const LINE_CHANNEL_ID = '2010891313'
-const LINE_CALLBACK_URL = 'https://backend.amazewayhk.com/auth/line/callback'
 function readClientHandle() {
 	return uni.getStorageSync(STORAGE.chatClient) || null
 }
@@ -36,6 +40,97 @@ function unwrapSession(raw) {
 	return raw
 }
 
+/**
+ * 从后端不同响应结构中提取 LINE 授权地址。
+ * 新流程先 GET /line/getAuthUrl,再把接口返回的授权地址放进 WebView。
+ */
+function pickLineAuthUrl(raw) {
+	if (!raw) return ''
+	if (typeof raw === 'string') return raw.trim()
+	const candidates = [
+		raw.url,
+		raw.auth_url,
+		raw.authUrl,
+		raw.authorize_url,
+		raw.authorizeUrl,
+		raw.redirect_url,
+		raw.redirectUrl,
+		raw.data && raw.data.url,
+		raw.data && raw.data.auth_url,
+		raw.data && raw.data.authUrl
+	]
+	const found = candidates.find(item => typeof item === 'string' && item.trim())
+	return found ? found.trim() : ''
+}
+
+/**
+ * 安全读取 WebView 当前 URL。
+ * 真机上部分阶段 getURL 可能不可用,统一兜底为空字符串,避免调试监听中断登录流程。
+ */
+function readWebviewUrl(wv) {
+	try {
+		return wv && typeof wv.getURL === 'function' ? wv.getURL() : ''
+	} catch (e) {
+		return ''
+	}
+}
+
+/**
+ * 判断当前 WebView URL 是否命中 LINE 登录成功页。
+ * 后端可能带 /api 前缀,成功页命中后前端会提取 ticket 继续换取用户信息。
+ */
+function isLineLoginSuccessUrl(url) {
+	const text = String(url || '').toLowerCase()
+	return LINE_SUCCESS_PATHS.some(path => text.indexOf(path) > -1)
+}
+
+/**
+ * 判断当前 WebView URL 是否命中 LINE 登录失败页。
+ * 失败页通常会带 error_msg,前端识别后关闭 WebView 并提示用户。
+ */
+function isLineLoginFailUrl(url) {
+	const text = String(url || '').toLowerCase()
+	return LINE_FAIL_PATHS.some(path => text.indexOf(path) > -1)
+}
+
+/**
+ * 从 URL 查询参数中读取指定字段。
+ * 优先使用 URL 标准解析,兼容失败时再用正则兜底,主要用于读取 ticket/error_msg。
+ */
+function readUrlQuery(url, key) {
+	const target = String(url || '')
+	try {
+		const parsed = new URL(target)
+		return parsed.searchParams.get(key) || ''
+	} catch (_) {
+		const match = target.match(new RegExp(`[?&]${key}=([^&]+)`))
+		return match ? decodeURIComponent(match[1].replace(/\+/g, '%20')) : ''
+	}
+}
+
+/**
+ * 展开打印 /line/getuserinfo 请求失败信息。
+ * HttpError 会把原始响应放在 payload 中,这里把状态码、请求地址和后端响应都打印出来,便于真机排查。
+ */
+function logLineUserinfoError(err) {
+	// /line/getuserinfo 失败时,HttpError 的后端响应藏在 payload 里;这里展开关键字段方便真机定位。
+	const payload = err && err.payload
+	const responseBody = payload && (payload.res || payload.data)
+	const requestUrl = payload && payload.config && payload.config.url
+	console.log('[LINE 登录] /line/getuserinfo 错误详情:', {
+		name: err && err.name,
+		kind: err && err.kind,
+		message: err && err.message,
+		statusCode: payload && payload.statusCode,
+		errMsg: payload && payload.errMsg,
+		requestUrl,
+		responseBody
+	})
+	try {
+		console.log('[LINE 登录] /line/getuserinfo 后端响应 JSON:', JSON.stringify(responseBody))
+	} catch (_) {}
+}
+
 /**
  * 苹果登录(Sign in with Apple)
  * 仅在 APP 端可用,H5 端不支持。
@@ -133,31 +228,33 @@ export function appleLogin() {
 // }
 
 /**
- * LINE 登录(WebView OAuth 流程)
- * 兼容 iOS 和 Android 端
+ * LINE 登录(后端授权页 WebView 流程)
+ * 兼容 iOS 和 Android 端
  *
- * @returns {Promise<Object>} 会话数据 { userinfo, token, ... }
+ * 注意:LINE 登录方式已改为先调用 GET /line/getAuthUrl 获取授权地址。
+ * 前端不再拼接 access.line.me OAuth 参数,也不再把 code 提交到 user/third。
+ *
+ * @returns {Promise<Object>} /line/getuserinfo 成功后返回 `{ userinfo, statistics }`,页面层沿用普通登录写入流程。
  */
 export function lineLogin() {
-	return new Promise((resolve, reject) => {
+	return new Promise(async (resolve, reject) => {
 		// #ifdef APP-PLUS
 
-		console.log('==================== [LINE Login] 开始 ====================')
-		console.log('[LINE Login] 时间:', new Date().toLocaleString())
-		console.log('[LINE Login] Channel ID:', LINE_CHANNEL_ID)
-		console.log('[LINE Login] Callback URL:', LINE_CALLBACK_URL)
-
-		const authUrl = `https://access.line.me/oauth2/v2.1/authorize?` +
-			`response_type=code` +
-			`&client_id=${LINE_CHANNEL_ID}` +
-			`&redirect_uri=${encodeURIComponent(LINE_CALLBACK_URL)}` +
-			`&state=line_login` +
-			`&scope=profile%20openid` +
-			`&bot_prompt=normal`
+		let authUrl = ''
+		try {
+			// 先向后端获取本次 LINE 授权地址,由后端生成 state、redirect_uri 等参数。
+			const { data } = await get(ROUTE__LINE_AUTH_URL)
+			authUrl = pickLineAuthUrl(data)
+		} catch (err) {
+			reject(err)
+			return
+		}
+		if (!authUrl) {
+			reject(new Error('LINE login: no auth url received'))
+			return
+		}
 
-		console.log('[LINE Login] 授权 URL:', authUrl)
-
-		// 创建 WebView(App 内部窗口,不是外部浏览器)
+		// 使用 /line/getAuthUrl 返回的授权地址打开 WebView,后端负责跳转 LINE 和处理 callback。
 		const wv = plus.webview.create(authUrl, 'line-auth', {
 			top: '44px',
 			left: '0px',
@@ -176,52 +273,50 @@ export function lineLogin() {
 		})
 		let hasResolved = false
 
-		
-		// 监听页面加载
-		wv.addEventListener('loaded', function () {
-			const url = wv.getURL()
-			console.log('[LINE Login] 页面加载:', url)
-
-			// 检查是否跳转到回调 URL 或包含 code 参数
-			if (url.indexOf(LINE_CALLBACK_URL) === 0 || url.indexOf('code=') > -1) {
+		// 监听页面流转;命中后端登录成功页时,提取 ticket 再换取 App 登录态。
+		wv.addEventListener('loaded', async function () {
+			const url = readWebviewUrl(wv)
+			if (isLineLoginFailUrl(url)) {
 				if (hasResolved) return
 				hasResolved = true
-				// 解析 code
-				const code = extractCodeFromUrl(url)
-				// 关闭 WebView
+				// 后端失败页会携带 error_msg,前端识别后关闭 WebView 并把原因抛给页面层。
+				const message = readUrlQuery(url, 'error_msg') || readUrlQuery(url, 'message') || 'LINE login failed'
 				wv.close()
+				reject(new Error(message))
+				return
+			}
+			if (!isLineLoginSuccessUrl(url)) return
+			if (hasResolved) return
 
-				if (!code) {
-					console.error('[LINE Login] ❌ 未获取到 authorization code')
-					console.error('[LINE Login] URL:', url)
-					reject(new Error('LINE login: no code received'))
-					return
-				}
+			hasResolved = true
+			const ticket = readUrlQuery(url, 'ticket')
+			// ticket 属于登录凭证,只用于换取用户信息,不输出到日志。
 
-				console.log('[LINE Login] ✅ 获取到 code:', code)
-				console.log('[LINE Login] 准备调用后端')
-
-				// 发送到后端验证
-				post(ROUTE__LOGIN, {
-					platform: 'line',
-					authorizationCode: code,
-					client_id: readClientHandle()
-				}).then(({ data }) => {
-					console.log('[LINE Login] ✅ 后端接口返回成功')
-					console.log('[LINE Login] 后端返回数据:', JSON.stringify(data, null, 2))
-					console.log('==================== [LINE Login] 完成 ====================')
-					resolve(unwrapSession(data))
-				}).catch(err => {
-					console.error('[LINE Login] ❌ 后端请求失败', err)
-					reject(err)
+			if (!ticket) {
+				wv.close()
+				reject(new Error('LINE login: missing ticket'))
+				return
+			}
+
+			try {
+				// 后端接口按表单参数读取 ticket;data 保持对象形态,避免影响项目 Sign 签名计算。
+				const { data } = await post(ROUTE__LINE_GET_USERINFO, { ticket }, {
+					header: FORM_URLENCODED_HEADER
 				})
+				// 用户信息里包含 token 等敏感字段,成功时不打印响应内容。
+				wv.close()
+				resolve(unwrapSession(data))
+			} catch (err) {
+				logLineUserinfoError(err)
+				wv.close()
+				reject(err)
 			}
 		})
 
 		// 监听错误
 		wv.addEventListener('error', function (error) {
-			console.log('[LINE Login] ❌ WebView 错误:', JSON.stringify(error))
-			console.log('[LINE Login] 错误详情:', error)
+			// WebView 加载错误保留日志,便于定位授权页无法打开等异常。
+			console.log('[LINE 登录][WebView] 加载错误:', error)
 			if (!hasResolved) {
 				hasResolved = true
 				wv.close()
@@ -239,17 +334,3 @@ export function lineLogin() {
 		// #endif
 	})
 }
-
-/**
- * 从 URL 中提取 authorization code
- */
-function extractCodeFromUrl(url) {
-	try {
-		const urlObj = new URL(url)
-		return urlObj.searchParams.get('code')
-	} catch (e) {
-		// 手动解析
-		const match = url.match(/[?&]code=([^&]+)/)
-		return match ? match[1] : null
-	}
-}

+ 6 - 6
pages/passport/lobby.vue

@@ -1,5 +1,5 @@
 <template>
-	<view class="pp-lobby">
+	<view class="pp-lobby" :style="appSystemFontScaleStyle">
 		<!-- 返回按钮 -->
 		<view class="pp-lobby__back" @tap="goBack">
 			<text class="wlIcon wlIcon-fanhui1"></text>
@@ -259,7 +259,7 @@ export default {
 }
 
 .pp-lobby__app-name {
-	font-size: 48rpx;
+	font-size: calc(48rpx * var(--app-font-scale, 1));
 	font-weight: 800;
 	color: #060606;
 	margin-top: 24rpx;
@@ -300,7 +300,7 @@ export default {
 }
 
 .pp-lobby_text {
-	font-size: 32rpx;
+	font-size: calc(32rpx * var(--app-font-scale, 1));
 	font-weight: 500;
 	color: #000000;
 	letter-spacing: 0.5rpx;
@@ -330,14 +330,14 @@ export default {
 }
 
 .pp-lobby__other-link {
-	font-size: 28rpx;
+	font-size: calc(28rpx * var(--app-font-scale, 1));
 	color: rgba(255, 255, 255, 0.9);
 	text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
 	padding: 12rpx 24rpx;
 }
 
 .pp-lobby__divider {
-	font-size: 30rpx;
+	font-size: calc(30rpx * var(--app-font-scale, 1));
 	color: rgba(255, 255, 255, 0.5);
 }
-</style>
+</style>

+ 5 - 1
store/modules/user.js

@@ -89,7 +89,11 @@ export default {
 			if (profile.token && !state.token) state.token = profile.token
 			uni.setStorageSync(STORAGE.user, state)
 			const stats = payload && (payload.statistics || (payload.data && payload.data.statistics))
-			if (stats) dispatch('stats/merge', stats, { root: true })
+			if (stats) {
+				// 后端登录接口会返回统计数据;同时同步新旧两个统计模块,保证登录后页面角标立即刷新。
+				dispatch('stats/merge', stats, { root: true })
+				commit('statistics/edit', stats, { root: true })
+			}
 		}
 	}
 }