From 537647ec740cf88374da08f622d4c8dc0724fe7f Mon Sep 17 00:00:00 2001 From: Billy <641833868@qq.com> Date: Fri, 17 Oct 2025 17:34:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pages/ccblife/index.vue | 427 +++++++++++++++++ frontend/sheep/platform/index.js | 18 +- frontend/sheep/platform/pay.js | 84 +++- .../sheep/platform/provider/ccblife/api.js | 78 ++++ .../sheep/platform/provider/ccblife/index.js | 440 ++++++++++++++++++ 5 files changed, 1044 insertions(+), 3 deletions(-) create mode 100644 frontend/pages/ccblife/index.vue create mode 100644 frontend/sheep/platform/provider/ccblife/api.js create mode 100644 frontend/sheep/platform/provider/ccblife/index.js diff --git a/frontend/pages/ccblife/index.vue b/frontend/pages/ccblife/index.vue new file mode 100644 index 0000000..c0d9b6b --- /dev/null +++ b/frontend/pages/ccblife/index.vue @@ -0,0 +1,427 @@ + + + + + \ No newline at end of file diff --git a/frontend/sheep/platform/index.js b/frontend/sheep/platform/index.js index 36a54fc..2c0d0c8 100644 --- a/frontend/sheep/platform/index.js +++ b/frontend/sheep/platform/index.js @@ -15,6 +15,7 @@ import { isWxBrowser } from '@/sheep/helper/utils'; // #endif import wechat from './provider/wechat/index.js'; import apple from './provider/apple'; +import ccblife from './provider/ccblife/index.js'; import share from './share'; import Pay from './pay'; @@ -28,7 +29,14 @@ let platform = ''; let isWechatInstalled = true; // #ifdef H5 -if (isWxBrowser()) { +// 先检测建行生活环境 +ccblife.detectEnvironment(); + +if (ccblife.isInCcbApp) { + name = 'CcbLife'; + provider = 'ccb'; + platform = 'ccblife'; +} else if (isWxBrowser()) { name = 'WechatOfficialAccount'; provider = 'wechat'; platform = 'officialAccount'; @@ -65,13 +73,19 @@ const load = () => { if (provider === 'wechat') { wechat.load(); } + if (provider === 'ccb') { + ccblife.init(); + // 处理URL跳转登录 + ccblife.handleUrlLogin(); + } }; -// 使用厂商独占sdk name = 'wechat' | 'alipay' | 'apple' +// 使用厂商独占sdk name = 'wechat' | 'alipay' | 'apple' | 'ccb' const useProvider = (_provider = '') => { if (_provider === '') _provider = provider; if (_provider === 'wechat') return wechat; if (_provider === 'apple') return apple; + if (_provider === 'ccb') return ccblife; }; // 支付服务转发 diff --git a/frontend/sheep/platform/pay.js b/frontend/sheep/platform/pay.js index f1fb545..00ceb1d 100644 --- a/frontend/sheep/platform/pay.js +++ b/frontend/sheep/platform/pay.js @@ -5,6 +5,8 @@ import $wxsdk from '@/sheep/libs/sdk-h5-weixin'; import { getRootUrl } from '@/sheep/helper'; +import CcbLifePlatform from './provider/ccblife/index'; +import ccbApi from './provider/ccblife/api'; /** * 支付 @@ -31,6 +33,9 @@ export default class SheepPay { alipay: () => { this.redirectPay(); // 现在公众号可以直接跳转支付宝页面 }, + ccb: () => { + this.ccbPay(); // 建行支付 + }, money: () => { this.moneyPay(); }, @@ -45,6 +50,9 @@ export default class SheepPay { alipay: () => { this.copyPayLink(); }, + ccb: () => { + sheep.$helper.toast('小程序暂不支持建行支付'); + }, money: () => { this.moneyPay(); }, @@ -59,6 +67,9 @@ export default class SheepPay { alipay: () => { this.alipay(); }, + ccb: () => { + this.ccbPay(); // 建行支付 + }, money: () => { this.moneyPay(); }, @@ -68,11 +79,19 @@ export default class SheepPay { }, H5: { wechat: () => { - this.wechatWapPay(); + // 如果在建行App内,使用建行支付 + if (CcbLifePlatform.isInCcbApp) { + this.ccbPay(); + } else { + this.wechatWapPay(); + } }, alipay: () => { this.redirectPay(); }, + ccb: () => { + this.ccbPay(); // 建行支付 + }, money: () => { this.moneyPay(); }, @@ -268,6 +287,69 @@ export default class SheepPay { } } + // 建行生活支付 + async ccbPay() { + let that = this; + + // 检查是否在建行App内 + if (!CcbLifePlatform.isInCcbApp) { + sheep.$helper.toast('请在建行生活App内使用建行支付'); + return; + } + + // 获取订单ID(从订单号查询) + const orderInfo = await sheep.$api.order.detail(this.orderSN); + if (!orderInfo || orderInfo.code !== 1) { + sheep.$helper.toast('获取订单信息失败'); + return; + } + + // 调用后端生成支付串 + const paymentResult = await ccbApi.createPayment(orderInfo.data.id); + + if (paymentResult.code !== 1) { + sheep.$helper.toast(paymentResult.msg || '生成支付串失败'); + return; + } + + // 调起建行支付 + try { + const result = await CcbLifePlatform.payment({ + payment_string: paymentResult.data.payment_string, + order_id: orderInfo.data.id, + order_sn: this.orderSN + }); + + if (result.code === 0) { + // 支付成功,通知后端 + const callbackResult = await ccbApi.paymentCallback({ + order_id: orderInfo.data.id, + trans_id: result.data?.trans_id || '', + pay_time: new Date().getTime() + }); + + if (callbackResult.code === 1) { + that.payResult('success'); + } else { + sheep.$helper.toast('支付确认失败,请联系客服'); + that.payResult('fail'); + } + } else { + // 支付失败或取消 + if (result.msg && result.msg.includes('取消')) { + sheep.$helper.toast('支付已取消'); + } else { + sheep.$helper.toast(result.msg || '支付失败'); + that.payResult('fail'); + } + } + } catch (error) { + console.error('[建行支付] 错误:', error); + sheep.$helper.toast('支付失败'); + that.payResult('fail'); + } + } + // 支付结果跳转,success:成功,fail:失败 payResult(resultType) { sheep.$router.redirect('/pages/pay/result', { diff --git a/frontend/sheep/platform/provider/ccblife/api.js b/frontend/sheep/platform/provider/ccblife/api.js new file mode 100644 index 0000000..d89c170 --- /dev/null +++ b/frontend/sheep/platform/provider/ccblife/api.js @@ -0,0 +1,78 @@ +/** + * 建行生活 API 接口 + * + * @author Billy + * @date 2025-01-17 + */ + +import request from '@/sheep/request'; + +const ccbApi = { + /** + * URL跳转登录 + * 建行App会携带加密参数跳转到此地址 + */ + login: (params) => { + return request({ + url: '/ccblife/login', + method: 'GET', + params: params, + custom: { + showLoading: true, + auth: false + } + }); + }, + + /** + * 自动登录(JSBridge方式) + * H5在建行App内打开时,通过JSBridge获取用户信息后调用 + */ + autoLogin: (data) => { + return request({ + url: '/ccblife/autoLogin', + method: 'POST', + data: data, + custom: { + showLoading: true, + auth: false + } + }); + }, + + /** + * 生成支付串 + * 用于调起建行收银台 + */ + createPayment: (orderId) => { + return request({ + url: '/ccbpayment/createPayment', + method: 'POST', + data: { + order_id: orderId + }, + custom: { + showLoading: true, + auth: true + } + }); + }, + + /** + * 支付回调 + * 支付完成后通知后端 + */ + paymentCallback: (data) => { + return request({ + url: '/ccbpayment/callback', + method: 'POST', + data: data, + custom: { + showLoading: false, + auth: true + } + }); + } +}; + +export default ccbApi; \ No newline at end of file diff --git a/frontend/sheep/platform/provider/ccblife/index.js b/frontend/sheep/platform/provider/ccblife/index.js new file mode 100644 index 0000000..397db03 --- /dev/null +++ b/frontend/sheep/platform/provider/ccblife/index.js @@ -0,0 +1,440 @@ +/** + * 建行生活平台集成模块 + * + * @author Billy + * @date 2025-01-17 + */ + +import sheep from '@/sheep'; +import ccbApi from './api'; + +// 建行生活配置 +const config = { + // 是否开启调试 + debug: true, + // API基础路径 + apiBaseUrl: '/addons/shopro', + // 超时时间 + timeout: 10000, +}; + +// 建行生活平台对象 +const CcbLifePlatform = { + // 平台标识 + platform: 'ccblife', + + // 平台名称 + name: '建行生活', + + // 是否在建行生活环境中 + isInCcbApp: false, + + // JSBridge 对象 + bridge: null, + + // 就绪状态 + isReady: false, + + // 就绪回调队列 + readyCallbacks: [], + + /** + * 初始化 + */ + init() { + // 检测环境 + this.detectEnvironment(); + + // 如果在建行App内,初始化JSBridge + if (this.isInCcbApp) { + this.setupBridge(); + // 自动登录 + this.autoLogin(); + } + + console.log('[CcbLife] 初始化完成, 是否在建行App内:', this.isInCcbApp); + }, + + /** + * 检测运行环境 + */ + detectEnvironment() { + // #ifdef H5 + const ua = navigator.userAgent.toLowerCase(); + + // 检查User-Agent + if (ua.indexOf('ccblife') > -1 || ua.indexOf('ccb') > -1) { + this.isInCcbApp = true; + return; + } + + // 检查URL参数 + const urlParams = this.getUrlParams(); + if (urlParams.ccbParamSJ || urlParams.from === 'ccblife') { + this.isInCcbApp = true; + return; + } + + // 检查JSBridge + if (window.WebViewJavascriptBridge || window.mbspay) { + this.isInCcbApp = true; + return; + } + // #endif + + // #ifdef APP-PLUS + // 在APP中也可能通过插件方式集成建行SDK + // TODO: 检测建行SDK插件 + // #endif + + // #ifdef MP-WEIXIN + // 小程序环境不支持建行生活 + this.isInCcbApp = false; + // #endif + }, + + /** + * 设置JSBridge + */ + setupBridge() { + // #ifdef H5 + const self = this; + + // iOS WebViewJavascriptBridge + if (window.WebViewJavascriptBridge) { + self.bridge = window.WebViewJavascriptBridge; + self.onBridgeReady(); + } else { + document.addEventListener('WebViewJavascriptBridgeReady', function() { + self.bridge = window.WebViewJavascriptBridge; + self.onBridgeReady(); + }, false); + } + + // Android 直接通过window对象 + if (window.mbspay && !self.bridge) { + self.bridge = window.mbspay; + self.onBridgeReady(); + } + + // 设置超时检查 + setTimeout(() => { + if (!self.isReady) { + console.warn('[CcbLife] JSBridge 未就绪'); + } + }, 3000); + // #endif + }, + + /** + * Bridge就绪回调 + */ + onBridgeReady() { + this.isReady = true; + console.log('[CcbLife] JSBridge 已就绪'); + + // 执行所有等待的回调 + this.readyCallbacks.forEach(callback => callback()); + this.readyCallbacks = []; + }, + + /** + * 等待Bridge就绪 + */ + ready(callback) { + if (this.isReady) { + callback(); + } else { + this.readyCallbacks.push(callback); + } + }, + + /** + * 获取URL参数 + */ + getUrlParams() { + const params = {}; + // #ifdef H5 + const search = window.location.search.substring(1); + if (search) { + const pairs = search.split('&'); + pairs.forEach(pair => { + const parts = pair.split('='); + if (parts.length === 2) { + params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); + } + }); + } + // #endif + return params; + }, + + /** + * 获取建行用户信息 + */ + async getUserInfo() { + return new Promise((resolve, reject) => { + if (!this.isInCcbApp) { + reject({ + code: -1, + msg: '不在建行生活App内' + }); + return; + } + + this.ready(() => { + this.callNative('getUserInfo', {}, (result) => { + if (result && result.userid) { + resolve({ + code: 0, + data: { + ccb_user_id: result.userid, + mobile: result.mobile || '', + nickname: result.nickname || '', + avatar: result.avatar || '' + } + }); + } else { + reject({ + code: -1, + msg: '获取用户信息失败' + }); + } + }); + }); + }); + }, + + /** + * 自动登录 + */ + async autoLogin() { + try { + // 检查是否已登录 + const token = uni.getStorageSync('token'); + if (token) { + console.log('[CcbLife] 用户已登录'); + return; + } + + // 获取用户信息 + const userResult = await this.getUserInfo(); + if (userResult.code !== 0) { + throw new Error(userResult.msg); + } + + // 调用后端登录接口 + const loginResult = await ccbApi.autoLogin(userResult.data); + + if (loginResult.code === 1) { + // 保存Token和用户信息 + uni.setStorageSync('token', loginResult.data.token); + uni.setStorageSync('userInfo', loginResult.data.userInfo); + + // 更新Shopro用户状态 + sheep.$store('user').getInfo(); + + console.log('[CcbLife] 自动登录成功'); + + // 触发登录成功事件 + uni.$emit('ccb:login:success', loginResult.data); + } else { + throw new Error(loginResult.msg); + } + } catch (error) { + console.error('[CcbLife] 自动登录失败:', error); + } + }, + + /** + * 调起建行支付 + */ + async payment(options) { + return new Promise((resolve, reject) => { + if (!this.isInCcbApp) { + reject({ + code: -1, + msg: '不在建行生活App内' + }); + return; + } + + // 必需参数检查 + if (!options.payment_string) { + reject({ + code: -1, + msg: '缺少支付串参数' + }); + return; + } + + this.ready(() => { + // #ifdef H5 + // 区分iOS和Android + if (this.isIOS()) { + // iOS使用URL Scheme + this.paymentForIOS(options, resolve, reject); + } else { + // Android使用JSBridge + this.paymentForAndroid(options, resolve, reject); + } + // #endif + + // #ifdef APP-PLUS + // APP中调用原生插件 + this.paymentForApp(options, resolve, reject); + // #endif + }); + }); + }, + + /** + * iOS支付 + */ + paymentForIOS(options, resolve, reject) { + // #ifdef H5 + const paymentUrl = 'comccbpay://pay?' + options.payment_string; + + // 尝试打开支付页面 + window.location.href = paymentUrl; + + // 设置回调检查 + setTimeout(() => { + resolve({ + code: 0, + msg: '已调起支付,请在建行App内完成支付' + }); + }, 1000); + // #endif + }, + + /** + * Android支付 + */ + paymentForAndroid(options, resolve, reject) { + this.callNative('payment', { + payment_string: options.payment_string + }, (result) => { + if (result && result.success) { + resolve({ + code: 0, + data: result + }); + } else { + reject({ + code: -1, + msg: result ? result.error : '支付失败' + }); + } + }); + }, + + /** + * APP支付 + */ + paymentForApp(options, resolve, reject) { + // #ifdef APP-PLUS + // TODO: 调用建行SDK插件 + uni.showToast({ + title: 'APP支付暂未实现', + icon: 'none' + }); + reject({ + code: -1, + msg: 'APP支付暂未实现' + }); + // #endif + }, + + /** + * 调用原生方法 + */ + callNative(method, params, callback) { + // #ifdef H5 + try { + if (this.isIOS() && this.bridge && this.bridge.callHandler) { + // iOS WebViewJavascriptBridge + this.bridge.callHandler(method, params, callback); + } else if (window.mbspay && window.mbspay[method]) { + // Android直接调用 + const result = window.mbspay[method](JSON.stringify(params)); + if (callback) { + callback(typeof result === 'string' ? JSON.parse(result) : result); + } + } else { + console.warn('[CcbLife] 原生方法不存在:', method); + if (callback) { + callback({ + success: false, + error: '原生方法不存在' + }); + } + } + } catch (e) { + console.error('[CcbLife] 调用原生方法失败:', e); + if (callback) { + callback({ + success: false, + error: e.message + }); + } + } + // #endif + }, + + /** + * 判断是否iOS + */ + isIOS() { + // #ifdef H5 + return /iPhone|iPad|iPod/i.test(navigator.userAgent); + // #endif + // #ifndef H5 + return uni.getSystemInfoSync().platform === 'ios'; + // #endif + }, + + /** + * 处理URL跳转登录 + */ + async handleUrlLogin() { + const params = this.getUrlParams(); + + // 如果有ccbParamSJ参数,说明是从建行跳转过来的 + if (params.ccbParamSJ) { + try { + // 调用后端解密并登录 + const result = await ccbApi.login(params); + + if (result.code === 1) { + // 保存Token和用户信息 + uni.setStorageSync('token', result.data.token); + uni.setStorageSync('userInfo', result.data.user_info); + + // 更新用户状态 + sheep.$store('user').getInfo(); + + // 跳转到指定页面 + const redirectUrl = result.data.redirect_url || '/pages/index/index'; + uni.reLaunch({ + url: redirectUrl + }); + } else { + uni.showToast({ + title: result.msg || '登录失败', + icon: 'none' + }); + } + } catch (error) { + console.error('[CcbLife] URL登录失败:', error); + uni.showToast({ + title: '登录失败', + icon: 'none' + }); + } + } + } +}; + +// 导出平台对象 +export default CcbLifePlatform; \ No newline at end of file