/** * App 跳转参数处理 * 花开物业 App 通过 launchMiniProgram 的 extraData 传入支付参数和登录凭证, * 小程序通过 referrerInfo.extraData 接收(查询字符串格式)。 * * extraData 示例: * payAmount=10&acct=xxx&custId=xxx&token=xxx&userId=xxx&sign=xxx&... */ const STORAGE_KEY = 'app_launch_payment_data' const APP_QUERY_KEY = 'app_query_params' /** * 需要转为数组的参数字段(逗号分隔字符串 → 数组) */ const ARRAY_PARAMS = ['billIds'] /** * 解析查询字符串为对象,数组字段自动拆分 */ function parseQueryString(str) { if (!str || typeof str !== 'string') return null const params = {} str.split('&').forEach(item => { const idx = item.indexOf('=') if (idx === -1) return const key = item.substring(0, idx) const val = item.substring(idx + 1) const decoded = decodeURIComponent(val || '') // 同名参数合并为数组;已知数组字段按逗号拆分 if (key in params) { if (Array.isArray(params[key])) { params[key].push(decoded) } else { params[key] = [params[key], decoded] } } else { params[key] = ARRAY_PARAMS.includes(key) && decoded.includes(',') ? decoded.split(',').map(v => v.trim()).filter(Boolean) : decoded } }) return params } /** * 归一化 billId/billIds:单个 id 用 billId,多个用 billIds,统一转为 billIds 数组 */ function normalizeBillParams(obj) { if (!obj) return // 先解码再拆分为数组 if (typeof obj.billIds === 'string') { const decoded = decodeURIComponent(obj.billIds) obj.billIds = decoded.includes(',') ? decoded.split(',').map(v => v.trim()).filter(Boolean) : [decoded] } // billId 存在且 billIds 不存在时,包装为数组 if (obj.billId && !obj.billIds) { const decoded = decodeURIComponent( typeof obj.billId === 'string' ? obj.billId : String(obj.billId) ) obj.billIds = decoded.includes(',') ? decoded.split(',').map(v => v.trim()).filter(Boolean) : [decoded] delete obj.billId } } /** * 从启动参数中获取 App 传入的全部数据(支付参数 + token + userId) */ export function captureAppLaunchData(onShowOptions) { let data = null let queryData = null const tryGetData = (options, label) => { if (!options) return null console.log(label, JSON.stringify(options)) // 保存 path query 参数(供接口调用) if (options.query && Object.keys(options.query).length > 0) { queryData = options.query console.log(label + ' path query 参数:', JSON.stringify(queryData)) } // 优先从 query 获取 if (options.query && Object.keys(options.query).length > 0) { return options.query } // referrerInfo.extraData:可能是对象也可能是查询字符串 const extraData = options?.referrerInfo?.extraData if (extraData) { if (typeof extraData === 'string') { console.log(label + ' extraData 是查询字符串,解析中...') return parseQueryString(extraData) } return extraData } return null } // 优先使用本次进入(onLaunch/onShow 传入的 options)的最新参数。 // 热启动再次从 App 进入时,getLaunchOptionsSync 仍返回首次冷启动的旧值, // 若先读它会导致新 token 被旧 token 覆盖,因此这里以本次 options 为准。 data = tryGetData(onShowOptions, '本次进入') // 兜底:本次 options 未取到数据时,依次尝试当前进入参数、冷启动参数。 // 注意:不能以 !onShowOptions 判断,onLaunch/onShow 始终会传入 options 对象, // 那样会误判导致兜底永远不执行,token 无法恢复。 if (!data) { try { data = tryGetData(wx.getEnterOptionsSync?.() || {}, '当前进入(getEnterOptionsSync)') } catch (e) { console.error('getEnterOptionsSync 失败:', e) } } if (!data) { try { data = tryGetData(wx.getLaunchOptionsSync?.() || {}, '冷启动(getLaunchOptionsSync)') } catch (e) { console.error('getLaunchOptionsSync 失败:', e) } } if (data) { // 恢复登录态:只要 App 传入了 token/userId 就恢复,不依赖支付字段名 if (data.token) { uni.setStorageSync('token', data.token) console.log('已恢复 token:', String(data.token).slice(0, 16) + '...') } if (data.userId) { uni.setStorageSync('userId', data.userId) console.log('已恢复 userId') } // 存在支付参数时才作为 App 支付进入处理 if (data.payAmount || data.acct || data.custId || data.billId || data.billIds) { // 归一化 billId/billIds:单个 id 用 billId,多个用 billIds,统一转为 billIds 数组 normalizeBillParams(data) uni.setStorageSync(STORAGE_KEY, data) console.log('已存储 App 支付数据:', JSON.stringify(data)) } } // 归一化 queryData 中的 billId/billIds normalizeBillParams(queryData) // 单独保存参数供接口使用(优先 queryData,fallback 到 data,即 extraData 来源) const queryParams = queryData || data if (queryParams) { uni.setStorageSync(APP_QUERY_KEY, queryParams) console.log('已存储 App 接口参数:', JSON.stringify(queryParams)) } return data } /** 检查是否已登录(token 存在) */ export function isLoggedIn() { return !!uni.getStorageSync('token') } /** 获取 App 传入的支付数据 */ export function getAppPaymentData() { try { return uni.getStorageSync(STORAGE_KEY) || null } catch (e) { return null } } /** 获取 App 传入的 path query 参数(供接口调用) */ export function getAppQueryParams() { try { return uni.getStorageSync(APP_QUERY_KEY) || null } catch (e) { return null } } /** 清除 */ export function clearAppPaymentData() { uni.removeStorageSync(STORAGE_KEY) } /** 清除 path query 参数(在支付完成后调用) */ export function clearAppQueryParams() { uni.removeStorageSync(APP_QUERY_KEY) } /** 组装 paymentItem */ export function buildPaymentItemFromApp(appData) { if (!appData) return null const amount = Number(appData.payAmount || appData.orderAmount || 0).toFixed(2) const displayName = appData.goodName || appData.remark || appData.type || '缴费' return { ...appData, type: displayName, rechargeAmount: amount } } /** * 支付返回结果 * 日照银行支付小程序完成支付后,通过 navigateBackMiniProgram 携带 extraData 返回, * 本小程序在 App.onShow 中通过 referrerInfo.extraData 接收(场景值 1038)。 * 这里只负责捕获/读取/清除原始返回数据,成功/失败判定见 resolvePaymentResult。 */ const PAYMENT_RESULT_KEY = 'payment_return_result' /** * 捕获支付返回结果(在 App.onShow 中调用) * 注意:App 冷/热启动进入时 extraData 是支付参数(payAmount/billIds 等), * 并非支付返回,需跳过,避免覆盖真实结果。 */ export function capturePaymentResult(onShowOptions) { if (!onShowOptions) return const extraData = onShowOptions?.referrerInfo?.extraData if (!extraData) return // 含支付参数字段说明是 App 跳转进入(冷/热启动),不是支付返回,跳过 if (typeof extraData === 'object' && (extraData.payAmount || extraData.billId || extraData.billIds || extraData.acct || extraData.custId)) { return } let parsed = extraData if (typeof extraData === 'string') { try { parsed = JSON.parse(extraData) } catch (e) { parsed = parseQueryString(extraData) } } console.log('捕获到支付返回结果:', JSON.stringify(parsed)) uni.setStorageSync(PAYMENT_RESULT_KEY, parsed) } /** 读取支付返回结果(读取后建议调用 clearPaymentReturnResult 清除) */ export function getPaymentReturnResult() { try { return uni.getStorageSync(PAYMENT_RESULT_KEY) || null } catch (e) { return null } } /** 清除支付返回结果 */ export function clearPaymentReturnResult() { uni.removeStorageSync(PAYMENT_RESULT_KEY) } // 失败/成功信号值(大小写不敏感),字段名与取值以日照银行实际返回为准,如不一致只需调整下方映射 const FAIL_SIGNALS = ['fail', 'failed', 'cancel', 'cancelled', 'error', 'closed', 'notpay', 'payerror', '2', 'false', 'no', '01'] const SUCCESS_SIGNALS = ['success', 'ok', 'paid', 'finish', 'finished', '1', '0000', '0', '200', 'true', 'yes', '00'] const RESULT_FIELDS = ['payResult', 'result', 'status', 'payStatus', 'tradeState', 'returnCode', 'code', 'errCode', 'orderState'] /** * 判定日照银行返回的支付结果:'success' | 'fail' | null(无法判定) */ export function resolvePaymentResult(data) { if (!data || typeof data !== 'object') return null // 布尔字段优先 if (typeof data.success === 'boolean') return data.success ? 'success' : 'fail' if (typeof data.isSuccess === 'boolean') return data.isSuccess ? 'success' : 'fail' for (const key of RESULT_FIELDS) { const v = data[key] if (v === undefined || v === null || v === '') continue const s = String(v).trim().toLowerCase() if (FAIL_SIGNALS.includes(s)) return 'fail' if (SUCCESS_SIGNALS.includes(s)) return 'success' } return null }