257 lines
9.1 KiB
JavaScript
257 lines
9.1 KiB
JavaScript
/**
|
||
* 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
|
||
}
|