const { API_BASE_URL, MINI_PROGRAM_APP_ID, MINI_PROGRAM_ENV, WEB_VIEW_ENTRY_URL, WEB_VIEW_SOURCE_QUERY, } = require('../../config'); const MINI_PROGRAM_CLIENT_TYPE = 'mini_program'; const MINI_PROGRAM_CLIENT_RUNTIME = 'wechat_mini_program'; const CLIENT_INSTANCE_STORAGE_KEY = 'genarrative:mini-program-client-instance-id'; function isConfiguredEntryUrl(value) { const trimmed = String(value || '').trim(); return /^https:\/\/[^/]+/i.test(trimmed); } function trimTrailingSlash(value) { return String(value || '').trim().replace(/\/+$/u, ''); } function isConfiguredApiBaseUrl(value) { return /^https:\/\/[^/]+/i.test(String(value || '').trim()); } function appendQuery(url, query) { const pairs = Object.keys(query) .filter((key) => query[key]) .map( (key) => `${encodeURIComponent(key)}=${encodeURIComponent(String(query[key]))}`, ); if (pairs.length === 0) { return url; } return `${url}${url.includes('?') ? '&' : '?'}${pairs.join('&')}`; } function appendHashParams(url, params) { const pairs = Object.keys(params) .filter((key) => params[key]) .map( (key) => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`, ); if (pairs.length === 0) { return url; } const hashIndex = url.indexOf('#'); const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url; const rawHash = hashIndex >= 0 ? url.slice(hashIndex + 1) : ''; const separator = rawHash ? '&' : ''; return `${baseUrl}#${rawHash}${separator}${pairs.join('&')}`; } function resolveWebViewUrl(authResult) { const entryUrl = String(WEB_VIEW_ENTRY_URL || '').trim(); if (!isConfiguredEntryUrl(entryUrl)) { return ''; } const sourcedUrl = appendQuery(entryUrl, WEB_VIEW_SOURCE_QUERY); if (!authResult || !authResult.token) { return sourcedUrl; } return appendHashParams(sourcedUrl, { auth_provider: 'wechat', auth_token: authResult.token, auth_binding_status: authResult.bindingStatus, }); } function getClientInstanceId() { const stored = wx.getStorageSync(CLIENT_INSTANCE_STORAGE_KEY); if (stored) { return String(stored); } const nextId = `wxmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; wx.setStorageSync(CLIENT_INSTANCE_STORAGE_KEY, nextId); return nextId; } function resolveClientPlatform() { const info = wx.getSystemInfoSync(); const platform = String(info.platform || '').toLowerCase(); if (platform === 'ios') { return 'ios'; } if (platform === 'android') { return 'android'; } return 'unknown'; } function wxLogin() { return new Promise((resolve, reject) => { wx.login({ success(result) { if (result.code) { resolve(result.code); return; } reject(new Error('微信登录未返回 code')); }, fail(error) { reject(new Error(error.errMsg || '微信登录失败')); }, }); }); } function requestMiniProgramLogin(code) { return new Promise((resolve, reject) => { const apiBaseUrl = trimTrailingSlash(API_BASE_URL); if (!isConfiguredApiBaseUrl(apiBaseUrl)) { reject(new Error('请先配置 API_BASE_URL')); return; } wx.request({ url: `${apiBaseUrl}/api/auth/wechat/miniprogram-login`, method: 'POST', data: { code }, header: { 'content-type': 'application/json', 'x-client-type': MINI_PROGRAM_CLIENT_TYPE, 'x-client-runtime': MINI_PROGRAM_CLIENT_RUNTIME, 'x-client-platform': resolveClientPlatform(), 'x-client-instance-id': getClientInstanceId(), 'x-mini-program-app-id': MINI_PROGRAM_APP_ID, 'x-mini-program-env': MINI_PROGRAM_ENV, }, success(response) { if (response.statusCode >= 200 && response.statusCode < 300) { resolve(response.data); return; } const message = response.data && response.data.error && response.data.error.message ? response.data.error.message : `微信登录失败:${response.statusCode}`; reject(new Error(message)); }, fail(error) { reject(new Error(error.errMsg || '微信登录请求失败')); }, }); }); } function requestMiniProgramBindPhone(authToken, wechatPhoneCode) { return new Promise((resolve, reject) => { const apiBaseUrl = trimTrailingSlash(API_BASE_URL); if (!isConfiguredApiBaseUrl(apiBaseUrl)) { reject(new Error('请先配置 API_BASE_URL')); return; } wx.request({ url: `${apiBaseUrl}/api/auth/wechat/bind-phone`, method: 'POST', data: { wechatPhoneCode }, header: { authorization: `Bearer ${authToken}`, 'content-type': 'application/json', 'x-client-type': MINI_PROGRAM_CLIENT_TYPE, 'x-client-runtime': MINI_PROGRAM_CLIENT_RUNTIME, 'x-client-platform': resolveClientPlatform(), 'x-client-instance-id': getClientInstanceId(), 'x-mini-program-app-id': MINI_PROGRAM_APP_ID, 'x-mini-program-env': MINI_PROGRAM_ENV, }, success(response) { if (response.statusCode >= 200 && response.statusCode < 300) { resolve(response.data); return; } const message = response.data && response.data.error && response.data.error.message ? response.data.error.message : `绑定手机号失败:${response.statusCode}`; reject(new Error(message)); }, fail(error) { reject(new Error(error.errMsg || '绑定手机号请求失败')); }, }); }); } async function resolveAuthResult() { const code = await wxLogin(); const response = await requestMiniProgramLogin(code); if (!response || !response.token) { throw new Error('服务器未返回登录态'); } return { token: response.token, bindingStatus: response.bindingStatus || 'pending_bind_phone', }; } Page({ data: { authResult: null, bindingPhone: false, errorMessage: '', loading: true, phoneBindingRequired: false, webViewUrl: '', }, async onLoad() { // 中文注释:web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。 if (!isConfiguredEntryUrl(WEB_VIEW_ENTRY_URL)) { this.setData({ errorMessage: '请先在 miniprogram/config.js 填写 WEB_VIEW_ENTRY_URL。', loading: false, webViewUrl: '', }); return; } if (!isConfiguredApiBaseUrl(API_BASE_URL)) { this.setData({ errorMessage: '请先在 miniprogram/config.js 填写 API_BASE_URL。', loading: false, webViewUrl: '', }); return; } try { const authResult = await resolveAuthResult(); if (authResult.bindingStatus === 'pending_bind_phone') { this.setData({ authResult, errorMessage: '', loading: false, phoneBindingRequired: true, webViewUrl: '', }); return; } this.setData({ authResult, errorMessage: '', loading: false, phoneBindingRequired: false, webViewUrl: resolveWebViewUrl(authResult), }); } catch (error) { this.setData({ authResult: null, errorMessage: error && error.message ? error.message : '微信登录失败,请稍后重试。', loading: false, phoneBindingRequired: false, webViewUrl: '', }); } }, async handleGetPhoneNumber(event) { if (!this.data.authResult || !this.data.authResult.token) { this.handleRetryLogin(); return; } const detail = event.detail || {}; if (!detail.code) { this.setData({ errorMessage: detail.errMsg || '需要授权手机号后才能完成绑定。', }); return; } this.setData({ bindingPhone: true, errorMessage: '', }); try { const response = await requestMiniProgramBindPhone( this.data.authResult.token, detail.code, ); if (!response || !response.token) { throw new Error('服务器未返回绑定后的登录态'); } const nextAuthResult = { token: response.token, bindingStatus: 'active', }; this.setData({ authResult: nextAuthResult, bindingPhone: false, errorMessage: '', loading: false, phoneBindingRequired: false, webViewUrl: resolveWebViewUrl(nextAuthResult), }); } catch (error) { this.setData({ bindingPhone: false, errorMessage: error && error.message ? error.message : '绑定手机号失败,请稍后重试。', }); } }, handleRetryLogin() { this.setData({ authResult: null, bindingPhone: false, errorMessage: '', loading: true, phoneBindingRequired: false, webViewUrl: '', }); this.onLoad(); }, handleWebViewLoad(event) { console.info('[web-view] loaded', event.detail); }, handleWebViewError(event) { console.error('[web-view] load failed', event.detail); }, handleWebViewMessage(event) { // 中文注释:H5 如需和小程序壳通信,可通过 wx.miniProgram.postMessage 发送轻量消息。 console.info('[web-view] message', event.detail); }, });