From b4e76a58ab165ce8a079aba84cbb5af28fe89a9e Mon Sep 17 00:00:00 2001 From: Billy <641833868@qq.com> Date: Tue, 21 Oct 2025 13:45:10 +0800 Subject: [PATCH] 1 --- ...程序API使用说明_v1.1_20230511.html | 599 ------------- ...in跳转链接解密可参考此demo2.java | 44 - doc/建行支付对接修复报告.md | 538 ------------ doc/建行支付架构修复报告.md | 825 ------------------ 4 files changed, 2006 deletions(-) delete mode 100644 doc/CCBLife小程序API使用说明_v1.1_20230511.html delete mode 100644 doc/UrlMain跳转链接解密可参考此demo2.java delete mode 100644 doc/建行支付对接修复报告.md delete mode 100644 doc/建行支付架构修复报告.md diff --git a/doc/CCBLife小程序API使用说明_v1.1_20230511.html b/doc/CCBLife小程序API使用说明_v1.1_20230511.html deleted file mode 100644 index b1f76f2..0000000 --- a/doc/CCBLife小程序API使用说明_v1.1_20230511.html +++ /dev/null @@ -1,599 +0,0 @@ - - - - - -CCBLife小程序API使用说明 - -
-

CCBLife小程序API使用说明_v1.1_20230511

文档修订记录

版本日期修订说明
1.02023.02.14同步在线文档接口说明
1.12023.05.11新增实名认证api

文档目录

1. 文档说明

本文档所描述API适用于建行生活App端内运行的JUMP小程序。

2. 接口说明

回调函数统一格式:

回调结果参数(Object res)

属性类型说明最低版本
dataobject返回内容-
statestring状态码-
msgstring状态信息|报错信息-

响应内容封装在data的Json对象里

2.1 login

用途说明

登录|获取用户信息。提供客户端认证模式与服务端认证模式两种模式。若建行生活处于未登录状态会跳转建行生活APP的登录页进行登录(行内单点登录使用)。

请求参数

属性类型默认值必填说明最低版本
typenumber-登录类型-
PLATFORM_IDstring--服务方ID-
Opn_Chnl_IDstring-合作方渠道编号-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

属性类型说明最低版本
encryptedDatastring"userid=xxx&mobile=xxx&PreAhr_ID=xxx"的加密字符串。(userid:建行生活用户编号,mobile:手机号,PreAhr_ID:用户中心预授权编码)-

注意

2.2 ccblife_login

用途说明

登录|获取用户信息。提供客户端认证模式与服务端认证模式两种模式。若建行生活处于未登录状态会跳转建行生活APP的登录页进行登录。

请求参数

属性类型默认值必填说明最低版本
typenumber-登录类型-
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

属性类型说明最低版本
encryptedDatastring"unionid=xxx&phone=xxx&locationCityCode=xxx"的加密字符串。(unionid:建行生活用户编号,phone:手机号,locationCityCode:用户选择城市码)-

注意

2.3 checkSession

用途说明

检查登录态是否过期。

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
isVaildboolean登录态是否有效-

2.4 getUserInfo

用途说明

获取用户信息。目前能返回的信息均为登录态敏感信息,加密。

请求参数

属性类型默认值必填说明最低版本
withCredentialsbooleantrue是否带上登录态信息。-
loginTypenumber-当前登录类型-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

loginType的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

state的合法值:

说明最低版本
0获取成功-
1获取失败-
2获取失败:未授权-

data:

属性类型说明最低版本
userInfoObject用户信息,不包含敏感数据-
encryptedDatastring包括敏感数据在内的完整用户信息的加密数据-
signaturestring用户数据签名-
saltstring签名使用的字符串-
ivstring加密算法的初始向量-

encryptedData 解密:

属性类型说明最低版本
unionidstring建行生活平台帐号的唯一标识-
openidstring用户在当前小程序的唯一标识-
cityCodeString用户选择城市编码 
locationCityCodestring用户当前定位城市编码-
registerCityCodestring用户归属城市编码,即用户注册地-
phonestring用户手机号-

注意:

2.5 authorize

用途说明

提前向用户发起授权请求。

请求参数

属性类型默认值必填说明最低版本
scopestring-需要获取权限的 scope-
successfunction-接口调用成功的回调函数(授权成功)-
failfunction-接口调用失败的回调函数(授权失败)-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

scope说明:

scope对应接口说明最低版本
scope.userInfogetUserInfo、login用户信息-
scope.camerascanCode摄像头-

2.6 requestPayment

用途说明

调用建行生活收银台。

请求参数

属性类型默认值必填说明最低版本
payInfostring-支付参数-

payInfo参数内容:

属性类型可为空必填说明最低版本
MERCHANTIDchar(15)YF商户代码;由建行统一分配-
POSIDchar(9)YF柜台代码;由建行统一分配-
BRANCHIDchar(9)YF分行代码;由建行统一分配-
POSID19char(19)NF商户19位终端号;由建行统一分配,使用微信支付时上送。仅作为参数传递,不参与MAC校验-
PLATMCTIDchar(19)YF外部平台商户号;当使用外部商户号时,建行商户号、柜台号、分行号及终端号无需上送。当该字段有值时参与MAC校验,否则不参与MAC校验-
ORDERIDchar(30)YT订单号;由商户提供,最长30位-
PAYMENTnumber(16,2)YT付款金额;由商户提供,最长30位-
CURCODEchar(2)YT币种;缺省为01-人民币(只支持人民币支付)-
TXCODEchar(6)YT交易码;由建行统一分配为520100-
REMARK1char(30)NT备注1;网银不处理,直接传到城综网,该字段只支持送数字和英文-
REMARK2char(30)NT备注2;上送YS开头的服务方编号,与PLATFORMID保持一致-
TYPEchar(1)YT接口类型;1- 防钓鱼接口-
GATEWAYchar(100)YT网关类型;默认送0-
CLIENTIPchar(40)NT客户端IP;客户在商户系统中的IP-
REGINFOchar(256)NT客户注册信息;客户在商户系统中注册的信息,中文需使用escape编码-
PROINFOchar(256)NT商品信息;客户购买的商品,中文需使用escape编码-
REFERERchar(100)NT商户URL;商户送空值即可-
INSTALLNUMchar(2)NF分期期数;信用卡支付分期期数,一般为 3、6、12 等,必须为大于 1 的整数。 仅当分期支付时上送该字段,无此字段上送时,则视为普通支付。-
THIRDAPPINFOchar(40)YT客户端标识;通过建行生活APP下单场景,订单中客户端标识固定设为comccbpay1234567890cloudmerchant-
TIMEOUTchar(14)NF订单超时时间;格式:YYYYMMDDHHMMSS(如:20120214143005)
银行系统时间> TIMEOUT时拒绝交易,若送空值则不判断超时。
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
USERIDchar(100)NF在中国建设银行App环境需提供。 当该字段有值时参与MAC校验,否则不参与MAC校验-
TOKENchar(100)NF在中国建设银行App环境需提供。 当该字段有值时参与MAC校验,否则不参与MAC校验-
PAYSUCCESSURLchar(100)NF在中国建设银行App环境考虑,如需指定支付成功页面需提供,需对URL编码,生产环境必须为HTTPS。未提供则默认跳转到建行生活的支付成功页面 当该字段有值时参与MAC校验,否则不参与MAC校验-
PAYBITMAPchar(10)NF支付位图;默认为空,只需要展示龙支付时请送0100000000
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
POINTAVYIDvarchar(6)NF积分二级活动编号;默认为空,特定场景使用。龙支付积分二级活动上送 010051-
DCEPDEPACCNOvarchar(32)NF数字人民币收款钱包编号;默认为空,特定场景使用。数字人民币商户绑定的收款钱包编号-
COUPONAVYIDvarchar(32)NF有价券活动编号;默认为空,特定场景使用。-
ONLY_CREDIT_PAY_FLAGvarchar(1)NF限制信用卡支付标志;默认为空,特定场景使用。当有价券活动编号不为空时生效,送Y限制仅信用卡能支付,送N或空不作限制-
FIXEDPOINTVALvarchar(16)NF固定抵扣积分值;默认为空,特定场景使用。上送该值时,若用户不满足积分使用条件将拒绝支付-
EXTENDPARAMSvarchar(256)NF积分二级活动编号;默认为空,特定场景使用。上送约定JSON格式字符串-
PLATFORMPUBvarchar(256)YF服务方公钥;仅作为源串参加MD5摘要,不作为参数传递-
MACchar(32)TTMD5加密串;采用标准MD5算法,对以上字段进行MAC加密(32位小写),由商户实现。-
PLATFORMIDchar(16)YT服务方编号;仅作为参数传递,不参与MAC校验-
ENCPUBvarchar(512)YF商户公钥密文;使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文。
若商户已经上架建行生活并同步公钥,可以不再上送商户公钥。仅作为参数传递,不参与MAC校验
-
SCNIDchar(32)NF场景编号;默认为空,埋点使用。特色场景的唯一标识。仅作为参数传递,不参与MAC校验-
SCN_PLTFRM_IDchar(32)NF场景平台编号;默认为空,埋点使用。场景平台唯一标识。仅作为参数传递,不参与MAC校验 

注意:

2.7 ccblife_requestPayment

用途说明

调用建行生活收银台。

请求参数

属性类型默认值必填说明最低版本
miniIdstring-本小程序id-
successPagestring-成功页面的路径,不设置则跳转建行生活APP的支付成功页面-
payInfoobject-支付参数-

payInfo参数内容

属性类型可为空必填说明最低版本
MERCHANTIDchar(15)商户代码;由建行统一分配-
POSIDchar(9)柜台代码;由建行统一分配-
BRANCHIDchar(9)分行代码;由建行统一分配-
POSID19char(19)商户19位终端号;由建行统一分配,仅作为参数传递,不参与MAC校验-
ORDERIDchar(30)订单号;由商户提供,最长30位-
PAYMENTnumber(16,2)付款金额;由商户提供,最长30位-
CURCODEchar(2)币种;缺省为01-人民币(只支持人民币支付)-
TXCODEchar(6)交易码;由建行统一分配为520100-
REMARK1char(30)备注1;网银不处理,直接传到城综网,该字段只支持送数字和英文-
REMARK2char(30)备注2;上送YS开头的服务方编号-
TYPEchar(1)接口类型;1- 防钓鱼接口-
GATEWAYchar(100)网关类型;默认送0-
CLIENTIPchar(40)客户端IP;客户在商户系统中的IP-
REGINFOchar(256)客户注册信息;客户在商户系统中注册的信息,中文需使用escape编码-
PROINFOchar(256)商品信息;客户购买的商品,中文需使用escape编码-
REFERERchar(100)商户URL;商户送空值即可-
THIRDAPPINFOchar(40)客户端标识;通过建行生活APP下单场景,订单中客户端标识固定设为comccbpay1234567890cloudmerchant-
TIMEOUTchar(14)订单超时时间;格式:YYYYMMDDHHMMSS(如:20120214143005)
银行系统时间> TIMEOUT时拒绝交易,若送空值则不判断超时。
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
PAYBITMAPchar(10)支付位图;默认为空,只需要展示龙支付时请送0100000000
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
PLATFORMPUBvarchar(256)服务方公钥;仅作为源串参加MD5摘要,不作为参数传递-
MACchar(32)MD5加密串;采用标准MD5算法,对以上字段进行MAC加密(32位小写),由商户实现。-
PLATFORMIDchar(16)服务方编号;仅作为参数传递,不参与MAC校验-
ENCPUBvarchar(512)商户公钥密文;使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文。
若商户已经上架建行生活并同步公钥,可以不再上送商户公钥。仅作为参数传递,不参与MAC校验
-

注意:

2.8 navigateTo

用途说明

跳转建行生活页面 | 外部H5页|建信小程序 | 微信小程序。

请求参数

属性类型默认值必填说明最低版本
typenumber-跳转页面类型-
toPagestring-跳转路径,类型为小程序本值为空时调起首页。-
isNewViewbooleanfalse是否打开新WebView,当type为1或2时有效。值为false时用进入小程序前入口所在页面的webview打开,若不存在则用新webview打开。-
isShowHeaderbooleanfalse是否展示通用标题栏,当type为1或2时有效-
headerNamestring-标题栏名称,isShowHeader为true时有效-
headerRightTypenumber0展示标题栏时右边按钮的类型-
paramstring-跳转携带的参数,type非0时拼接到最终URL后-
miniIdstring-跳转小程序id,type为3或4时有效-
miniVersionnumber0微信小程序版本,type为4时有效 
successfunction-接口调用的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0建行生活原生页面-
1建行生活H5页面-
2外部H5页面-
3Jump小程序-
4微信小程序-

headerRightType 的合法值:

说明最低版本
0关闭按钮-
1分享按钮-

miniVersion 的合法值:

说明最低版本
0发布版-
1预览版-
2测试版-

响应内容

state 的合法值:

说明最低版本
0跳转成功-
1跳转失败-

2.9 scanCode

用途说明

建行生活App的扫码功能,扫描建行生活提供的业务二维码(扫码支付等)需要用本接口而非jump.scanCode

请求参数

属性类型默认值必填说明最低版本
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
contentstring扫码结果-

state 的合法值:

说明最低版本
0扫码成功-
1非有效业务二维码,无法解析-

2.10 openPayCode

用途说明

打开支付码。

请求参数

属性类型默认值必填说明最低版本
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

state 的合法值:

说明最低版本
0打开成功-
1打开失败-

2.11 callMap

用途说明

调起手机内的地图App。支持苹果地图|高德地图|百度地图。

请求参数

属性类型默认值必填说明最低版本
paramsobject-参数-
needNavigationbooleanfalse是否需要导航-
addressstring-商户地址-
lgtnumber-商户纬度-
lttnumber-商户经度-
cityNamestring-城市名称-
business_namestring-商户名称-
self_lgtnumber-客户维度-
self_lttnumber-客户经度-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

注意:

响应内容

state 的合法值:

说明最低版本
0调起地图成功-
1调起地图失败-

2.12 startFaceScan

用途说明

刷脸认证|人脸校验

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring-服务方ID-
namestring--姓名-
cardTypestring-卡片类型(身份证)-
cardNumstring-身份证号码-
phoneNumstring-手机号-
showErrorstring-报错弹窗:1-显示 0-不显示-
scanOnlystring-仅刷脸:1-只刷脸,不发校验刷脸流水的交易-
Stm_Chnl_IDstring-渠道号,默认为建行生活渠道-
Stm_Chnl_Txn_CDstring-渠道交易码,默认为建行生活渠道交易码-
txCodestring-安全交易码,默认为建行生活安全交易码-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
successstring刷脸认证是否成功:0-失败,1-成功-
Comm_Auth_FieldsstringUUID-
Apl_Aply_TrcNostring全局流水号-

2.13 userStatus

用途说明

获取用户状态信息

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
userTypestring用户类型:00游客/未登录 01钱包用户 02已注册未开钱包-
isLoginstring登录状态:0:未登录 1:已登录-

2.14 share

用途说明

分享。

请求参数

属性类型默认值必填说明最低版本
share_idstring-分享id,存在分享ID时先调接口获取分享内容-
textstring--分享的描述-
titlestring--标题-
urlstring--链接-
imagestring--图片链接-
typestring--0--分享链接,1--分享微信朋友,2--分享朋友圈-
base64Picstring--图片base64格式化-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
typestring0: 取消分享 1:分享到微信 2:分享到朋友圈-

2.15 checkUser

用途说明

校验用户身份。

请求参数

属性类型默认值必填说明最低版本
platformIdstring-服务方编号,非空-
sceneIdstring--场景ID-
checkTypestring--校验类型 1-校验平台支付密码 2-校验平台登录密码-
checkScopestring--验密有效范围,0-App内有效(默认值) 1-同类场景有效 2-场景内有效 3-场景内同功能有效 4-一次性有效-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
tokenstring唯一验密流水号,每次重新生成-
codestring校验结果状态码
0:校验完成
1:已在其他功能场景校验通过,且在有效期内
-1:用户取消校验
-2:校验失败,当前场景未配置校验类型、用户状态异常等原因
-

注意:

2.15 RealNameAuthorization

用途说明

实名认证。

请求参数

属性类型默认值必填说明最低版本
platformIdstring-服务方编号,非空-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
successstring实名结果 0-失败,1-成功-
- - \ No newline at end of file diff --git a/doc/UrlMain跳转链接解密可参考此demo2.java b/doc/UrlMain跳转链接解密可参考此demo2.java deleted file mode 100644 index 1fdd5cc..0000000 --- a/doc/UrlMain跳转链接解密可参考此demo2.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.filedemo.util.fuwufang; - -import com.example.filedemo.util.RSAUtil; -import sun.misc.BASE64Decoder; -import sun.misc.BASE64Encoder; - -public class UrlMain { - public static void main(String[] args) throws Exception { - String msg = "BGCOLOR=&userid=YSM202111170063936&mobile=18242028306&cityid=330100&userCityId=330100&orderid=&PLATFLOWNO=0000A2UNK1639016304462982&openid=&lgt=113.3295774824442<t=23.12339638654285&Usr_Name=&USERID=YSM202111170063936&MOBILE=18242028306&CITYID=330100&USERCITYID=330100&ORDERID=&OPENID=&LGT=113.3295774824442<T=23.12339638654285"; - - //String enc_msg = "SDB0dllqYmxFS2xHRmlqa1ZaOFk0OHBXY0I5TitoREdJaVB3K1pjM2M3dy9jek4zN016ZUoxZENTNTVLWVFFV3VSYzlYOVlXRkpBcQpWRUgwaDJUMG04V2lmNHJyS3krdG5QUDJHalhEQlNma21oR3JrV0lsbFRibC9vbWJONGxqeVk1TXZQWjVWc2t5N2ZVRlZTYlNlYjIzCnJ5cFN4dTRNSDUrTjFRTU5NVFE9Cg%3D%3D"; - // 公钥 - String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClMNB2rs4PMyxHdV+HeISWBbe55WQkmSYQQvFq8M4MMczhYihhp1Z9p723wD8cv9m/PQQcQZuNIehGGIIbZnMZFkqwDYUODH0DF8N5o7BiUhw/XUr3nl49/hsjlE6L7k/7jYzxZ+r3CXhz7qVXZNW6tD2RM+AI4qomQr0p1VNxhQIDAQAB"; - // 私钥 - String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKUw0Hauzg8zLEd1X4d4hJYFt7nlZCSZJhBC8WrwzgwxzOFiKGGnVn2nvbfAPxy/2b89BBxBm40h6EYYghtmcxkWSrANhQ4MfQMXw3mjsGJSHD9dSveeXj3+GyOUTovuT/uNjPFn6vcJeHPupVdk1bq0PZEz4AjiqiZCvSnVU3GFAgMBAAECgYAyTZQdoAulu0qPlCF8CmotmR4ioMUHFA/wQcJsc1n7gqrGM3LikeeXqh3ut79ATPfM8ZKv3Ba3Oo0V017DY0ZG7j2stXxFhm2ln/q6nfaDsfx5ae22kIdNFCrDfwYByBiVsZPNCrj+8qDb/DPiVveEpsj7hn6thZY8QnjwEi0O3QJBAOia3cqup/rLMTYwtl43OREyMDt3qWS+aRQz1jQJlQSONV76qsZpZZUVxQEglvf6+afRCyn1mAqNa2dek6gbHTMCQQC1zijBYb6b4kghbKg/ZC37A79kBuRKtl/yIMYtFLWrtIntv047HavVPHZLEl++44Hk+9rfzNw1J12uXigGVoZnAkBGh6745jzJLxOc+uhRaS1EqZM2dPJIOfRiy9UHsmAdIYHNavSddRf4PMGfteIRD2jkGd7oui+AA6Gtll/veUlBAkAwybEwK/3NsUywA4um70hTiy7qNds/nW9j952W7W7PNDSrY2IoBQ9eusn33WdqP31VKK0Uz9HsRbMjHstY4BFTAkEAisda+CJkO/Epdj693ewIr4GbGORGSVB2pCjLGPqhuvu37d/T9+9T85BoeaMwm31aVNGOPIUCSPOMelKRUoj3Gw=="; - - // 公钥加密得到密文并使用base64处理 - String enc_msg = RSAUtil.encrypt(msg, publicKey); - //enc_msg = ""; - //enc_msg = "TVpoZ040QTdVcTZMem11cGdTVjdWeFEvWU5kZWZqcFVBK3JSS0hGK0NsSFo0Ly82RnV3blVvT1hrMXNlZ25odXlkVytvRkdGT0xoYwpRSlhjYWI4eWJkUWh3UTgzUU1MQzBzYWtManZCOFNhR29VWDhiMHZoRXhWdkVIQ3BTcnlVRUQwUU9zQzVodXoxQTk3djRIeXNlMFR5CjFnWm1zaTRLZm9sWHM1TndJcXMvQ3lVdE9MTjNqZGZoajNHTXQvSHN1c01PVzFBekwxbTFTWVk2VGhuL3lvODA4NGRRaGN2aC9KVU0KdUdWZUNtdEJJbytHVVJaOXNCZW5BaUZVTEFWTHJINHhRK0pWUnpRRUZvRlhhNVVaRVQ0cDJTME1TWXBTZ1R4eStOeFloTER1S280OQpsV3RONFVKVkJiTVY1dUJrdEthMEF6MEROczA1bndDTnJtL3FReFdQMmFtV3pRT24ybHl6MVRCMXhjZGpaVVpDc3prOVZoM3FtS05SCmNVTnV2M0x6cDB2SzIzOVFyM205R0taWGl6TFdKcktQcE9UaDkrODQyY0s3L1ZmTDh6S2NBZG1QN21QMXhKZE5NVlEzZFZnaW9XWlQKd2xWWVc4N2FLZ0xLNlZIaEhJNTVGV3RNaFYrTzdNbUZlSzBWckhaS3NHSDJIUGg5ZUQyVkFVWVNWaU9HdisyUDRFczJ5V0lCNjkxNgpjMStRWjc5RXdwRmwrQjBJUG0yLy9rbDVXSXVYaDdXY1FPUzBBMm4yMnYxWnlMK29vdVNUTWNGMm9TNWx1RjRzRVc5VDkrb0tjRjU5CjBVOGVvY1E0R2Q5dkxwTnVpSG1CNUpvWFF6NG1EZGFBdG13eS9zMHFXVTgyQjlWZEVETlNPZGx0NnNoQXBTZ01ObGtGS0hyQllHMD0K"; - - BASE64Encoder encoder = new BASE64Encoder(); - enc_msg = encoder.encode(enc_msg.getBytes("UTF-8")); - enc_msg = enc_msg.replaceAll("\r\n", "").replaceAll("\r", "").replaceAll("\n", ""); - - - System.out.println("公钥加密得到密文并使用base64处理:"); - //enc_msg = "TVpoZ040QTdVcTZMem11cGdTVjdWeFEvWU5kZWZqcFVBK3JSS0hGK0NsSFo0Ly82RnV3blVvT1hrMXNlZ25odXlkVytvRkdGT0xoYwpRSlhjYWI4eWJkUWh3UTgzUU1MQzBzYWtManZCOFNhR29VWDhiMHZoRXhWdkVIQ3BTcnlVRUQwUU9zQzVodXoxQTk3djRIeXNlMFR5CjFnWm1zaTRLZm9sWHM1TndJcXMvQ3lVdE9MTjNqZGZoajNHTXQvSHN1c01PVzFBekwxbTFTWVk2VGhuL3lvODA4NGRRaGN2aC9KVU0KdUdWZUNtdEJJbytHVVJaOXNCZW5BaUZVTEFWTHJINHhRK0pWUnpRRUZvRlhhNVVaRVQ0cDJTME1TWXBTZ1R4eStOeFloTER1S280OQpsV3RONFVKVkJiTVY1dUJrdEthMEF6MEROczA1bndDTnJtL3FReFdQMmFtV3pRT24ybHl6MVRCMXhjZGpaVVpDc3prOVZoM3FtS05SCmNVTnV2M0x6cDB2SzIzOVFyM205R0taWGl6TFdKcktQcE9UaDkrODQyY0s3L1ZmTDh6S2NBZG1QN21QMXhKZE5NVlEzZFZnaW9XWlQKd2xWWVc4N2FLZ0xLNlZIaEhJNTVGV3RNaFYrTzdNbUZlSzBWckhaS3NHSDJIUGg5ZUQyVkFVWVNWaU9HdisyUDRFczJ5V0lCNjkxNgpjMStRWjc5RXdwRmwrQjBJUG0yLy9rbDVXSXVYaDdXY1FPUzBBMm4yMnYxWnlMK29vdVNUTWNGMm9TNWx1RjRzRVc5VDkrb0tjRjU5CjBVOGVvY1E0R2Q5dkxwTnVpSG1CNUpvWFF6NG1EZGFBdG13eS9zMHFXVTgyQjlWZEVETlNPZGx0NnNoQXBTZ01ObGtGS0hyQllHMD0K"; - System.out.println(enc_msg); - - //enc_msg = "UVRreXJPVm1GRlgrYldkWFNjd1pwM0dTMWxuNkJYMThUZEs1U1dLQWU2cFdkV0JoUXBFeU1nci90L1J1YWpTSks0RXo2a250cXJGK0hoclRXQ3I5Nk8vUEw4aWFKS3J5SllpUm9jTE1NMVdEcWsyakIvMWxkWXE1WGx4Qk9lenR3aTI0alV4MVV4dTBZY0ZWaUIvdGFRd0xIaWdzdE1nT1pEYnlqcnhKdUdGdkpJVG9hNkJDbGM4RXpnRWN3bzZiQnBDR3BXSCtkQk5LZE5yN0dDYnAzRTltUGRxOEh4Y01NNFBiNXdyeFBrZlpCMkl2NXpGRnNmOStUTHEzajVQT2JTa2t6dXR2VGhVS2VKN1dYZ01Vdzhvbk9rYzE2S3Q3VTg3dEFJVlpJYTY2RDdTMGd5ZWNrN01oVE5KM2tkYXNhUmVtbEQ2cys0WkNtb2NqYWVWbVpuT09yNGtsS3Z6U2VZVE5sOWNpMXRCblFBV0M0VzE5dVN0RXp6OGxFY21idHBqZHVSbUxGODNyNm83YWZ6N1dDbGMwUDEyakMxODRxZDNNUUpRQ0l0OE1OZThzNTZsNVJ1blJnRmNGNGJEb2UyTU94QUUzb05Rd3JCMldRemcxNE9mRFp2UVdlMW9JVUNMU0cyZGc1OUNUN09KdG5lZndDaEJQUGNmc2tBVE8%3D"; - //enc_msg = "T2tpcTVMeC9uVCt6VXNSRWxaT09VVGh1YlZtMkpXQXhzcWErZkxsQ0pUWktMQVZNdTFTYm1VZnN6aVVGaTNnbUE0VEx6LzJLL3pkVmpzY0pHSDlzUmJ6MFRWTUM3QkZ4ZXV5bVJZMW43bGVNOG1wRVhheGNpTEIyVzNMV0lmakh0d0o0QTRUNWtwMnhUOVprRXFhZ1RKUEZ3RUgwSmdqem9CRHpjMzZNWkxlRS9DUzBCR0RoQzdTODFweXBMaktuUWdhK0RJNUFOQUdrQnhjeHcrQWFGeUdNRmRVMWVaMU9GWUtYVjRzeUJVZnZ6dk1UN2ZmODIvLzZBa1VRMFN3a2p5TmliRjg4VkJCODJGckRCOC9TRW1CWVJnWWtRVklhWEFPZXo1aXlSR0laam1KN3Z6bXFKTDZSVzVGWTFPYms2YWJaU1FnVnZwNXoxbStHdG1KdkRYczJxeE01Unk2N0RtNlhpOGRyRERvVW83YUdzbW5Tamp5VzNUSVE0WS9iSzVyMEo5UndwcjUvTTFYMGg1T3d4MWJoRWVVTUJVZlMzV1BZTVNwMVR4WFVsRkFjTk8yQk9wZ3lvcWJYcmRFV0c2RmFIUXNxYS82ay80SmpseVpCbDd6cUUwYU9SM3lZMXY0ZG9iVHlDb0JENkNhcFp1SWs0NFlibDRaMFdTRmFlRE1lcndiZmdUcU1nWmFNL0RjWmVWN2V1akVGNytaWjNLTExZdmU2VlV3bVlJbmM2bHg2N2FwdG5UM0hic3BWei8rTnlHek1FRWRmQlpGTVFnbDhSeTBoeTlDcGRxRng2dUhrdm5wRHJrenZkUVAzWm55bkRzZHgwdlBoUW9XeEQyQWRDVi9UdVdIOTIxeG52b0NVa3U2UCtkSFJyUm9kd1BVSjBWOURiYnc9"; - // base64逆处理并用私钥解密 - BASE64Decoder decoder = new BASE64Decoder(); - enc_msg = new String(decoder.decodeBuffer(enc_msg),"UTF-8"); - String dec_msg = RSAUtil.decrypt(enc_msg, privateKey); - - System.out.println("base64逆处理并用私钥解密:"); - System.out.println(dec_msg); - - - - } -} diff --git a/doc/建行支付对接修复报告.md b/doc/建行支付对接修复报告.md deleted file mode 100644 index ea968f9..0000000 --- a/doc/建行支付对接修复报告.md +++ /dev/null @@ -1,538 +0,0 @@ -# 建行支付对接修复报告 - -**项目**: Shopro商城建行支付集成 -**修复时间**: 2025-01-20 -**文档版本**: v2.0 (修订版) -**建行接口版本**: v2.20 (2025-07-25) - ---- - -## ⚠️ 重要修订说明 - -本报告v2.0版本修正了v1.0中关于"建行平台公钥"的**严重错误理解**: - -- ❌ **错误**: 文档中不存在"建行平台公钥"这个概念 -- ✅ **正确**: 应该是"建行生活支付验签公钥"(需联系建行生活技术支持获取) - ---- - -## 📋 修复概览 - -本次对建行支付对接代码进行了**5项严重错误修复**和**1项性能优化**,基于建行官方Java示例代码和接口文档v2.20规范。 - -### 修复文件清单 - -| 文件路径 | 修复项 | 风险等级 | -|---------|--------|---------| -| `addons/shopro/library/ccblife/CcbPaymentService.php` | MAC签名算法、SIGN验签逻辑 | 🔴 致命 | -| `addons/shopro/library/ccblife/CcbEncryption.php` | ENCPUB生成、RSA分段加密 | 🔴 致命 | -| `addons/shopro/controller/Ccbpayment.php` | 防重复支付、notify返回格式 | 🟡 严重 | - ---- - -## 🔴 致命错误修复 - -### 1. 支付串MAC签名算法错误 - -**位置**: `CcbPaymentService.php:148-153` - -#### 修复前 ❌ -```php -// 错误: 使用私钥签名 -$mac = md5($signString . $this->config['private_key']); -``` - -#### 修复后 ✅ -```php -// 正确: 使用服务方公钥参与MD5计算(建行v2.2规范) -$platformPubKey = $this->config['public_key']; // 服务方公钥 -$mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); -``` - -#### 技术说明 -根据建行文档v2.2版本和官方MD5Util.java示例: -- **PLATFORMPUB字段**: 仅参与MD5摘要计算,不作为HTTP参数传递 -- **签名格式**: `MD5(参数串 + &PLATFORMPUB= + 服务方公钥内容)` -- **输出格式**: 32位**大写**MD5字符串 (对照MD5Util.java第30行: `toUpperCase()`) - -**影响**: 修复前建行会拒绝所有支付请求,因签名验证100%失败。 - ---- - -### 2. ENCPUB字段生成逻辑错误 - -**位置**: `CcbEncryption.php:387-420` - -#### 修复前 ❌ -```php -// 错误: 加密整个商户公钥 -return $this->rsaEncrypt($this->publicKey); -``` - -#### 修复后 ✅ -```php -// 正确: 只加密商户公钥后30位 -$publicKeyContent = str_replace([ - '-----BEGIN PUBLIC KEY-----', - '-----END PUBLIC KEY-----', - "\r", "\n", " " -], '', $this->publicKey); - -$last30Chars = substr($publicKeyContent, -30); -return $this->rsaEncrypt($last30Chars); -``` - -#### 技术说明 -建行文档明确要求: -> "使用服务方公钥对**商户公钥后30位**进行RSA加密并base64后的密文" - -**影响**: 修复前ENCPUB字段内容错误,可能导致建行无法验证商户公钥。 - ---- - -### 3. 异步通知SIGN验签逻辑优化 - -**位置**: `CcbPaymentService.php:467-570` - -#### 修复前 ❌ -```php -// 错误: 使用MD5验签 -$expectedSign = md5($signStr . $this->config['private_key']); -return strtolower($signature) === strtolower($expectedSign); -``` - -#### 修复后 ✅ -```php -// 智能验签方案: 如果配置了验签公钥则使用RSA,否则降级为POSID验证 -$ccbVerifyPublicKey = $this->config['ccb_payment_verify_public_key'] ?? ''; - -if (empty($ccbVerifyPublicKey)) { - // 降级方案: POSID验证 - return ($params['POSID'] ?? '') === $this->config['pos_id']; -} - -// 完整方案: RSA验签(尝试SHA256和SHA1) -$signBinary = hex2bin($params['SIGN']); -$pubKey = openssl_pkey_get_public($ccbVerifyPublicKey); - -$result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA256); -if ($result !== 1) { - $result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA1); -} - -return $result === 1; -``` - -#### 技术说明 -根据建行文档7.2.3章节: -- **SIGN字段**: 256个十六进制字符 (2048位RSA签名) -- **验签密钥**: "建行生活分配的服务商支付验签公钥" (NT_TYPE=YS时) -- **验签算法**: RSA-SHA256或SHA1 (文档未明确,代码会自动尝试) -- **获取方式**: 联系建行生活平台技术支持 - -#### 降级方案说明 -由于建行未提供验签公钥和示例代码,代码实现了两级验证: - -1. **优先**: 如果配置了`ccb_payment_verify_public_key`,使用RSA验签 -2. **降级**: 如果未配置,验证POSID和订单号是否匹配 - -**建议**: 尽快联系建行技术支持获取验签公钥,补全配置后获得完整安全保障。 - -**影响**: 修复后验签逻辑更健壮,未配置公钥时也能正常运行(安全性降低但不会中断业务)。 - ---- - -## 🟡 严重问题修复 - -### 4. 订单状态更新缺少防重复逻辑 - -**位置**: `Ccbpayment.php:170-197` - -#### 修复前 ❌ -```php -// 直接更新,没有并发控制 -$order->status = 'paid'; -$order->save(); -``` - -#### 修复后 ✅ -```php -// 使用原子性更新,防止并发重复支付 -$affectedRows = Db::name('shopro_order') - ->where('id', $order->id) - ->where('status', 'unpaid') // 只更新未支付的订单 - ->update([ - 'status' => 'paid', - 'paid_time' => time() * 1000, - 'updatetime' => time() - ]); - -if ($affectedRows === 0) { - // 订单已支付或状态异常 - throw new Exception('订单状态异常,无法更新为已支付'); -} -``` - -**影响**: 修复前在高并发场景下可能出现重复支付或状态覆盖。 - ---- - -### 5. notify接口返回格式不规范 - -**位置**: `Ccbpayment.php:271-283` - -#### 修复前 ❌ -```php -// ThinkPHP框架会追加额外内容 -echo $result; // 'SUCCESS' 或 'FAIL' -``` - -#### 修复后 ✅ -```php -// 直接exit,确保只返回纯文本 -exit(strtoupper($result)); // 'SUCCESS' 或 'FAIL' -``` - -#### 技术说明 -建行要求异步通知响应: -- **HTTP 200** 状态码 -- **纯文本** 响应体: `SUCCESS` 或 `FAIL` -- **不允许**任何额外字符(HTML/JSON等) - -**影响**: 修复前ThinkPHP框架可能追加调试信息,导致建行认为通知失败并重复推送。 - ---- - -## ⚡ 性能优化 - -### 6. RSA加密分段大小动态计算 - -**位置**: `CcbEncryption.php:102-129` - -#### 优化前 ⚠️ -```php -// 写死1024位RSA的chunk size -$chunkSize = 117; // 1024位RSA密钥,每次最多加密117字节 -``` - -#### 优化后 ✅ -```php -// 动态获取RSA密钥大小 -$keyDetails = openssl_pkey_get_details($pubKeyId); -$keySize = $keyDetails['bits'] / 8; // 1024位=128字节, 2048位=256字节 -$chunkSize = $keySize - 11; // PKCS1填充需要预留11字节 -``` - -**优势**: -- 自动适配1024位/2048位/4096位RSA密钥 -- 减少不必要的分段次数,提升加密性能 -- 避免密钥升级后的兼容性问题 - ---- - -## 🔐 建行接口签名规则总结 - -### 支付串生成流程 - -```mermaid -graph LR - A[34个参数] --> B[按ASCII排序ksort] - B --> C[http_build_query拼接] - C --> D[追加&PLATFORMPUB=服务方公钥] - D --> E[MD5签名,32位小写] - E --> F[ENCPUB=RSA加密商户公钥后30位] - F --> G[最终支付串=参数+MAC+PLATFORMID+ENCPUB] -``` - -### 异步通知验签流程 - -```mermaid -graph LR - A[接收SIGN字段] --> B[hex2bin转二进制] - B --> C[移除SIGN,剩余参数ksort排序] - C --> D[拼接签名原串] - D --> E[使用建行公钥RSA-SHA256验签] - E --> F{验签结果} - F -->|成功| G[返回SUCCESS] - F -->|失败| H[返回FAIL] -``` - ---- - -## ✅ 验证检查清单 - -修复完成后,请逐项检查以下配置: - -### 1. 配置文件检查 - -**文件**: `addons/shopro/config/ccblife.php` - -```php -return [ - // 建行商户信息 - 'merchant_id' => 'YOUR_MERCHANT_ID', - 'pos_id' => 'YOUR_POS_ID', - 'branch_id' => 'YOUR_BRANCH_ID', - 'service_id' => 'YOUR_SERVICE_ID', - - // ✅ 服务方公钥(用于MAC签名) - 'public_key' => '-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----', - - // ✅ 服务方私钥(用于解密) - 'private_key' => '-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... ------END PRIVATE KEY-----', - - // ✅ 建行平台公钥(用于SIGN验签) - 新增必填! - 'platform_public_key' => '-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----', - - // 建行收银台URL - 'cashier_url' => 'https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain', -]; -``` - -### 2. 密钥格式验证 - -运行以下PHP脚本验证密钥格式: - -```php -generatePaymentString($orderId); - -// 验证点: -// 1. MAC长度为32位 -// 2. ENCPUB字段存在且不为空 -// 3. 支付串包含所有34个必需参数 -``` - -#### TC2: 异步通知验签测试 -```php -// 模拟建行回调数据 -$params = [ - 'ORDERID' => 'test123', - 'PAYMENT' => '100.00', - 'SUCCESS' => 'Y', - 'SIGN' => '256字符十六进制字符串...' -]; - -$result = $service->handleNotify($params); -// 预期: 返回'success'或'fail' -``` - -#### TC3: 并发支付测试 -使用Apache Bench进行并发测试: -```bash -ab -n 100 -c 10 http://your-domain/api/ccbpayment/callback -``` -验证订单状态不会重复更新。 - ---- - -## ⚠️ 上线前必读 - -### 1. 建行生活支付验签公钥获取(重要!) - -**关键**: 需要向建行生活技术支持索要**"建行生活支付验签公钥"**,用于异步通知SIGN验签。 - -#### 为什么需要这个公钥? - -- 建行用自己的私钥对异步通知进行RSA签名(生成SIGN字段) -- 你需要用建行的公钥来验证SIGN,确保通知是建行发送的 -- 这个公钥**不是**你自己生成的公钥,是建行生活平台分配给你的 - -#### 如何获取? - -1. 联系建行生活平台运营人员或技术支持 -2. 说明需要获取"建行生活支付验签公钥"(NT_TYPE=YS的验签公钥) -3. 提供你的商户号和服务方编号 -4. 获取后配置到`.env`文件中 - -```ini -# .env文件 -ccb_payment_verify_public_key="-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----" -``` - -#### 未配置的影响 - -- 异步通知验签会降级为POSID验证 -- 安全性降低,无法完全确认通知来源 -- 但不会中断业务,系统仍可正常运行 - -### 2. 验证密钥格式 - -运行以下PHP脚本验证密钥配置是否正确: - -```php ->Backend: POST /createPayment(生成支付串) - Backend->>Backend: 保存订单(未支付状态) - - Note over Backend,CCBBackend: 步骤2: 推送订单 ✅ 推送未支付订单! - Backend->>Merchant: 调用订单推送接口(A3341TP01) - Merchant-->>Backend: 返回推送结果 - Backend-->>H5: {payment_string} - - Note over H5,CCBApp: 步骤3: 调起建行收银台 - H5->>CCBApp: JSBridge.ccbpay(支付串) - activate CCBBackend - CCBApp->>CCBBackend: 校验登录 - CCBApp->>CCBApp: 调用支付组件 - H5->>CCBApp: 确认支付、输入密码 - CCBApp->>CCBBackend: 发送支付请求 - Note right of CCBBackend: 用户在建行APP中完成支付 - - Note over CCBBackend,Backend: 步骤10-12: 建行异步通知 - CCBBackend->>Merchant: 返回支付成功通知 - Merchant->>Backend: 推送服务器通知(notify) - Backend->>Backend: 验证SIGN签名 - Backend->>Backend: 原子更新本地订单状态为paid - deactivate CCBBackend - - Note over Backend,Merchant: 步骤13: 更新订单状态 ✅ 更新为已支付! - Backend->>Merchant: 调用订单更新接口(A3341TP02) - Merchant-->>Backend: 返回更新结果 - Backend-->>Merchant: 返回SUCCESS - - Note over H5,Backend: 步骤15-16: 前端轮询查询状态 (未收到通知时) - loop 每2秒轮询(最多60秒) - H5->>Backend: GET /queryPaymentStatus - Backend-->>H5: {status: 'paid'或'unpaid'} - alt status == 'paid' - H5->>H5: 跳转到支付成功页 - end - end -``` - -**关键流程说明**: -1. **步骤2**: 生成支付串后**立即推送未支付订单**到建行外联系统(A3341TP01) -2. **步骤13**: 收到支付成功通知后**更新订单状态为已支付**(A3341TP02) -3. **步骤15**: 前端轮询查询订单状态(用于未收到通知的降级方案) - -### 修复前的错误实现 - -```mermaid -sequenceDiagram - participant H5 as 前端H5页面 - participant Callback as callback接口 - participant Notify as notify接口 - participant CCBApp as 建行APP(黑盒) - - Note over H5,Callback: ❌ 错误1: 前端callback通知支付成功 - H5->>Callback: POST callback(order_id, trans_id) ❌ 可伪造! - Callback->>Callback: verifyPayment()主动查询建行? - Callback->>Callback: ❌ 更新订单为已支付 - Callback->>Callback: ❌ 推送订单到外联 - Callback-->>H5: 返回success - - Note over CCBApp,Notify: ❌ 错误2: 建行异步通知被边缘化 - CCBApp-->>Notify: POST notify(ORDERID, SIGN等) - Notify->>Notify: ❌ 再次更新订单? - Notify->>Notify: ❌ 再次推送订单? - Notify-->>CCBApp: 返回SUCCESS - - Note over H5,Callback: ⚠️ 严重问题: 两条并行路径! - rect rgb(255, 200, 200) - Note right of Callback: 路径A: 前端callback触发更新
路径B: 建行notify触发更新
可能导致重复处理或竞态条件! - end -``` - ---- - -## 🔥 严重安全漏洞详解 - -### 漏洞1: 前端callback可伪造支付成功 - -**问题代码** (`frontend/sheep/platform/pay.js:325-336`): - -```javascript -// ❌ 错误: 前端主动调用callback通知后端支付成功 -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'); // ❌ 跳转到成功页 - } -} -``` - -**攻击方式**: - -1. 用户在浏览器控制台执行: -```javascript -ccbApi.paymentCallback({ - order_id: 12345, - trans_id: 'fake_trans_id', - pay_time: Date.now() -}) -``` - -2. 后端callback()接口收到请求,**没有验证签名**,直接更新订单为已支付! - -3. 攻击者不花一分钱就能白嫖商品 🔥 - -**风险等级**: 🔴 **致命** - 可直接导致商户资金损失 - ---- - -### 漏洞2: callback()与notify()双通道竞态条件 - -**问题代码** (`addons/shopro/controller/Ccbpayment.php`): - -```php -// ❌ callback()中更新订单状态 -public function callback() -{ - // ...省略代码... - - // 更新订单状态 - $affectedRows = Db::name('shopro_order') - ->where('id', $order->id) - ->where('status', 'unpaid') - ->update(['status' => 'paid']); - - // 推送订单到建行 - $this->pushOrderToCcb($order); // ❌ 在callback中推送 -} - -// ❌ notify()中也更新订单状态 -public function notify() -{ - // ...省略代码... - - $this->paymentService->handleNotify($params); // ❌ 内部再次更新订单 - - // handleNotify()内部还会调用pushOrder() ❌ 重复推送! -} -``` - -**竞态条件场景**: - -1. 用户在建行APP完成支付 -2. 建行异步通知服务器 → 触发`notify()` → 更新订单为paid -3. **同时**前端H5页面返回 → 调用`callback()` → 再次尝试更新订单 -4. 如果notify还未完成,callback会成功更新 → **订单被更新两次** -5. pushOrderToCcb()被调用两次 → **外联系统收到重复订单** - -**风险等级**: 🔴 **致命** - 可能导致订单状态异常或重复扣款 - ---- - -### 漏洞3: 订单推送时机错误 - -**问题**: callback()在前端触发时就推送订单,但此时: -- 建行可能还未真正扣款 -- callback可能是攻击者伪造的请求 -- 订单状态可能还未真正更新为paid - -**正确时机**: 只在`notify()`收到建行异步通知并验签成功后推送! - ---- - -## ✅ 修复方案 - -### 修复1: callback()改造为纯查询接口 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:129-164`): - -```php -/** - * 查询订单支付状态 (前端轮询用) - * - * ⚠️ 重要: 本接口只查询订单状态,不执行任何业务逻辑! - * 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。 - */ -public function queryPaymentStatus() -{ - try { - $orderId = $this->request->get('order_id', 0); - - if (empty($orderId)) { - $this->error('订单ID不能为空'); - } - - // ✅ 只查询,不更新! - $order = OrderModel::where('id', $orderId) - ->where('user_id', $this->auth->id) - ->field('id, order_sn, status, paid_time, ccb_pay_flow_id') - ->find(); - - if (!$order) { - $this->error('订单不存在'); - } - - // ✅ 返回订单状态(只读操作,绝不修改数据!) - $this->success('查询成功', [ - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'status' => $order->status, - 'is_paid' => in_array($order->status, ['paid', 'completed', 'success']), - 'paid_time' => $order->paid_time, - 'pay_flow_id' => $order->ccb_pay_flow_id, - ]); - - } catch (Exception $e) { - Log::error('[建行支付] 查询订单状态失败 error:' . $e->getMessage()); - $this->error('查询失败: ' . $e->getMessage()); - } -} - -/** - * ⚠️ 已废弃: 支付回调 (前端调用) - * @deprecated 2025-01-20 该方法存在严重安全漏洞,已被queryPaymentStatus()替代 - */ -public function callback() -{ - // 向后兼容:直接调用查询接口 - Log::warning('[建行支付] callback()已废弃,请前端改用queryPaymentStatus()接口'); - - $_GET['order_id'] = $this->request->post('order_id', 0); - return $this->queryPaymentStatus(); -} -``` - -**修复要点**: -- ✅ 只查询,不更新任何数据 -- ✅ 只返回订单状态,前端根据状态判断是否跳转 -- ✅ 支持用户权限验证(`where('user_id', $this->auth->id)`) -- ✅ 向后兼容旧版callback接口 - ---- - -### 修复2: notify()成为唯一的支付确认通道 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:200-258`): - -```php -/** - * 建行支付通知 (建行服务器回调) - * - * ✅ 正确流程: - * 1. 验证签名 - * 2. 更新订单状态(由handleNotify()完成) - * 3. 推送订单到建行外联系统(本方法完成) - * 4. 返回SUCCESS给建行 - */ -public function notify() -{ - try { - // 1-5. 解析和验证参数 - $rawData = file_get_contents('php://input'); - Log::info('[建行通知] 收到异步通知: ' . $rawData); - - $params = $this->request->post(); - if (empty($params) && $rawData) { - parse_str($rawData, $params); - } - - if (empty($params['ORDERID'])) { - Log::error('[建行通知] 缺少ORDERID参数'); - exit('FAIL'); - } - - // 6. 调用服务层处理通知(返回订单ID) - $result = $this->paymentService->handleNotify($params); - - // 7. ✅ 处理成功后推送订单到建行外联系统 - if ($result['status'] === 'success' && !empty($result['order_id'])) { - // ⚠️ 只有新支付才推送,已支付的订单跳过推送 - if ($result['already_paid'] === false) { - try { - $this->pushOrderToCcb($result['order_id']); - Log::info('[建行通知] 订单推送成功 order_id:' . $result['order_id']); - } catch (Exception $e) { - // ⚠️ 推送失败不影响支付成功,记录日志后续补推 - Log::error('[建行通知] 订单推送失败 order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); - } - } else { - Log::info('[建行通知] 订单已支付且已推送,跳过推送'); - } - } - - // 8. 返回处理结果 - $response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL'; - Log::info('[建行通知] 处理完成,返回: ' . $response); - - exit($response); // 直接退出,确保只输出SUCCESS/FAIL - - } catch (Exception $e) { - Log::error('[建行通知] 处理失败 error:' . $e->getMessage()); - exit('FAIL'); - } -} -``` - -**修复要点**: -- ✅ 验证SIGN签名(由handleNotify()完成) -- ✅ 原子更新订单状态(由handleNotify()完成) -- ✅ 推送订单到外联系统(本方法完成,在订单状态更新成功后) -- ✅ 幂等性保护(已支付的订单跳过推送) -- ✅ 推送失败不影响支付成功(记录日志后续补推) - ---- - -### 修复3: createPayment()推送未支付订单 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:101-118`): - -```php -// 5. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单) -// ⚠️ 注意:此时推送的是未支付状态的订单 -try { - $pushResult = $this->orderService->pushOrder($orderId); - - if ($pushResult['status']) { - Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); - } else { - // ⚠️ 推送失败不阻塞支付流程,只记录日志 - Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); - } -} catch (Exception $e) { - // ⚠️ 推送异常不阻塞支付流程 - Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); -} - -// 6. 返回支付串 -$this->success('支付串生成成功', $result['data']); -``` - -**修复要点**: -- ✅ 生成支付串后立即调用`pushOrder()`推送未支付订单(A3341TP01) -- ✅ 推送失败不阻塞支付流程,用户仍可继续支付 -- ✅ 记录推送结果日志,失败的可后续补推 - ---- - -### 修复4: notify()更新订单状态为已支付 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:227-248`): - -```php -// 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态) -if ($result['status'] === 'success' && !empty($result['order_id'])) { - // ⚠️ 只有新支付才更新,已支付的订单跳过更新 - if ($result['already_paid'] === false) { - try { - // 调用订单更新接口,将订单状态从未支付更新为已支付 - $updateResult = $this->orderService->updateOrderStatus($result['order_id']); - - if ($updateResult['status']) { - Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']); - } else { - // ⚠️ 更新失败不影响本地支付状态,记录日志后续补推 - Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']); - } - } catch (Exception $e) { - // ⚠️ 更新异常不影响支付成功,记录日志后续补推 - Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); - } - } else { - Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']); - } -} -``` - -**修复要点**: -- ✅ 收到支付成功通知后调用`updateOrderStatus()`(A3341TP02) -- ✅ 将订单状态从"未支付"更新为"已支付" -- ✅ 更新失败不影响本地支付状态(本地订单已标记为paid) -- ✅ 幂等性保护(已支付的订单跳过更新) - ---- - -### 修复5: handleNotify()返回订单ID - -**修复后** (`addons/shopro/library/ccblife/CcbPaymentService.php:349-403`): - -```php -/** - * 处理异步通知 - * - * ⚠️ 注意:这是唯一可信的支付确认来源! - * 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统 - * - * @param array $params 通知参数 - * @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string] - */ -public function handleNotify($params) -{ - try { - // 1. 验证签名 - if (!$this->verifyNotifySignature($params)) { - throw new \Exception('签名验证失败'); - } - - // 2. 查询订单 - $payFlowId = $params['ORDERID'] ?? ''; - $userOrderId = $params['USER_ORDERID'] ?? ''; - - if (!empty($userOrderId)) { - $order = Order::where('order_sn', $userOrderId)->find(); - } else { - $order = Order::where('ccb_pay_flow_id', $payFlowId)->find(); - } - - if (!$order) { - throw new \Exception('订单不存在'); - } - - // 3. ✅ 幂等性检查: 如果订单已支付,直接返回成功 - if ($order['status'] == 'paid') { - Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id); - return [ - 'status' => 'success', - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'already_paid' => true, // ✅ 标记为已支付 - ]; - } - - // 4. 更新订单状态 - $this->updateOrderPaymentStatus($order, $params); - - Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id); - - // 5. ✅ 返回订单ID供控制器推送到外联系统 - return [ - 'status' => 'success', - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'already_paid' => false, // ✅ 新支付 - ]; - - } catch (\Exception $e) { - Log::error('[建行通知] 处理失败: ' . $e->getMessage()); - return [ - 'status' => 'fail', - 'message' => $e->getMessage(), - ]; - } -} -``` - -**修复要点**: -- ✅ 返回订单ID供控制器推送 -- ✅ 返回already_paid标志防止重复推送 -- ✅ 幂等性保护(已支付的订单直接返回成功) - ---- - -### ~~修复6: pushOrderToCcb()增加幂等性检查~~ - -**⚠️ 已废弃**: 根据建行流程图,`pushOrderToCcb()`方法已废弃。 - -**正确流程**: -1. `createPayment()` → 调用`orderService->pushOrder()` → 推送未支付订单(A3341TP01) -2. `notify()` → 调用`orderService->updateOrderStatus()` → 更新为已支付(A3341TP02) - ---- - -**⚠️ 已废弃**: 根据建行流程图,订单推送和更新的幂等性由`CcbOrderService`内部保证。 - ---- - -### 修复6: 前端改为轮询查询 - -**修复后** (`frontend/sheep/platform/pay.js:325-386`): - -```javascript -// 建行生活支付 -async ccbPay() { - // ...省略订单信息获取和支付串生成... - - // 调起建行支付 - const result = await CcbLifePlatform.payment({ - payment_string: paymentResult.data.payment_string, - order_id: orderId, - order_sn: this.orderSN - }); - - if (result.code === 0) { - // ✅ 支付调起成功,开始轮询查询订单状态 - console.log('[建行支付] 支付调起成功,开始轮询查询订单状态'); - - uni.showLoading({ - title: '支付确认中...', - mask: true - }); - - // ✅ 轮询查询订单状态(最多30次,每次间隔2秒,总共60秒) - let pollCount = 0; - const MAX_POLL_COUNT = 30; - const POLL_INTERVAL = 2000; - - const pollPaymentStatus = async () => { - pollCount++; - - try { - const statusResult = await ccbApi.queryPaymentStatus(orderId); - - if (statusResult.code === 1 && statusResult.data.is_paid) { - // ✅ 支付成功 - uni.hideLoading(); - console.log('[建行支付] 订单已支付'); - that.payResult('success'); - return; - } - - // 未支付,继续轮询 - if (pollCount < MAX_POLL_COUNT) { - setTimeout(pollPaymentStatus, POLL_INTERVAL); - } else { - // 超时 - uni.hideLoading(); - uni.showModal({ - title: '提示', - content: '支付确认超时,请稍后在订单列表中查看支付状态', - showCancel: false, - confirmText: '知道了', - success: () => { - sheep.$router.redirect('/pages/order/list'); - } - }); - } - } catch (error) { - console.error('[建行支付] 查询状态失败:', error); - - // 继续轮询(网络错误不中断) - if (pollCount < MAX_POLL_COUNT) { - setTimeout(pollPaymentStatus, POLL_INTERVAL); - } else { - uni.hideLoading(); - sheep.$helper.toast('支付状态查询失败,请稍后在订单列表中查看'); - that.payResult('fail'); - } - } - }; - - // 延迟1秒后开始轮询(给建行异步通知留点时间) - setTimeout(pollPaymentStatus, 1000); - } -} -``` - -**修复要点**: -- ✅ 轮询查询订单状态(每2秒一次) -- ✅ 最多轮询30次(总共60秒) -- ✅ 网络错误不中断轮询 -- ✅ 超时友好提示用户去订单列表查看 - ---- - -## 📊 修复前后对比 - -| 对比项 | 修复前(错误) | 修复后(正确) | -|-------|------------|------------| -| **支付确认来源** | 前端callback + 建行notify (双通道) ❌ | 只依赖建行notify (单通道) ✅ | -| **前端职责** | 调用callback通知后端支付成功 ❌ | 轮询查询订单状态 ✅ | -| **安全性** | 可伪造前端请求触发支付成功 🔴 | 只信任建行签名验证 ✅ | -| **订单推送时机** | 支付成功后推送 ❌ | **创建订单时推送未支付状态** ✅ | -| **订单更新时机** | 未更新到建行 ❌ | **支付成功后更新为已支付** ✅ | -| **竞态风险** | callback和notify可能同时执行 🔴 | 只有notify会更新订单 ✅ | -| **幂等性** | 无幂等保护 ❌ | 支持重复调用,已推送/已更新的跳过 ✅ | -| **符合建行规范** | 否 ❌ | **完全符合流程图** ✅ | -| **订单状态一致性** | 可能重复更新或状态异常 🔴 | 原子更新,状态一致 ✅ | -| **建行订单同步** | 不同步或错误时机同步 ❌ | **按流程图正确同步** ✅ | - ---- - -## ✅ 验证清单 - -修复完成后,请逐项验证: - -### 1. 后端验证 - -```bash -# 验证queryPaymentStatus接口 -curl -X GET "http://your-domain/addons/shopro/ccbpayment/queryPaymentStatus?order_id=123" \ - -H "Authorization: Bearer YOUR_TOKEN" - -# 预期返回: -{ - "code": 1, - "msg": "查询成功", - "data": { - "order_id": 123, - "order_sn": "202501200001", - "status": "unpaid", - "is_paid": false, - "paid_time": null, - "pay_flow_id": "" - } -} -``` - -### 2. 安全测试 - -**测试1: 验证callback()已不能触发支付成功** - -```bash -# 尝试伪造callback请求 -curl -X POST "http://your-domain/addons/shopro/ccbpayment/callback" \ - -d "order_id=123&trans_id=fake_trans&pay_time=123456789" \ - -H "Authorization: Bearer YOUR_TOKEN" - -# ✅ 预期: 只返回订单状态,不会更新订单为已支付 -``` - -**测试2: 验证notify()是否幂等** - -```bash -# 模拟建行重复发送通知 -curl -X POST "http://your-domain/addons/shopro/ccbpayment/notify" \ - -d "ORDERID=PAY20250120001&SUCCESS=Y&SIGN=..." - -# ✅ 预期: -# - 第1次调用: 更新订单+推送外联,返回SUCCESS -# - 第2次调用: 跳过处理,直接返回SUCCESS -# - 日志中应有"订单已支付,跳过处理" -``` - -### 3. 前端验证 - -1. 在建行APP中发起支付 -2. 观察浏览器控制台: - - 应该看到"支付调起成功,开始轮询查询订单状态" - - 每2秒调用一次`queryPaymentStatus`接口 - - 收到`is_paid=true`后跳转到成功页 - -3. 网络中断测试: - - 支付完成后断开网络 - - 前端应继续轮询(虽然失败) - - 60秒后提示超时,引导用户去订单列表查看 - -### 4. 日志验证 - -```bash -# 查看创建支付串日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行支付' - -# ✅ 正常流程应该看到: -# [建行支付] 订单推送成功 order_id:123 ← 步骤2: 推送未支付订单 - -# 查看notify日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知' - -# ✅ 正常流程应该看到: -# [建行通知] 收到异步通知: ORDERID=... -# [建行通知] 解析参数: {...} -# [建行通知] 订单状态更新成功 order_id:123 ← 步骤13: 更新为已支付 -# [建行通知] 处理完成,返回: SUCCESS - -# 查看幂等性日志(重复通知时) -# [建行通知] 订单已支付,跳过处理 order_id:123 -# [建行通知] 订单已支付且已更新,跳过更新 order_id:123 -``` - ---- - -## 📝 数据库字段说明 - -确保订单表包含以下字段: - -```sql -ALTER TABLE `fa_shopro_order` -ADD COLUMN `ccb_pay_flow_id` VARCHAR(64) DEFAULT '' COMMENT '建行支付流水号', -ADD COLUMN `ccb_sync_status` TINYINT(1) DEFAULT 0 COMMENT '建行同步状态:0-未同步 1-已同步 2-失败', -ADD COLUMN `ccb_sync_time` INT(10) DEFAULT 0 COMMENT '建行同步时间', -ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因'; -``` - -**字段说明**: -- `ccb_sync_status`: 0=未同步 / 1=已同步 / 2=失败 -- `ccb_sync_error`: 推送失败时记录错误原因,供后续补推 - ---- - -## 🚀 部署步骤 - -### 1. 备份现有代码 - -```bash -# 备份控制器 -cp addons/shopro/controller/Ccbpayment.php addons/shopro/controller/Ccbpayment.php.bak - -# 备份服务类 -cp addons/shopro/library/ccblife/CcbPaymentService.php addons/shopro/library/ccblife/CcbPaymentService.php.bak - -# 备份前端代码 -cp frontend/sheep/platform/pay.js frontend/sheep/platform/pay.js.bak -cp frontend/sheep/platform/provider/ccblife/api.js frontend/sheep/platform/provider/ccblife/api.js.bak -``` - -### 2. 部署后端代码 - -```bash -# 上传修复后的文件 -# - addons/shopro/controller/Ccbpayment.php -# - addons/shopro/library/ccblife/CcbPaymentService.php - -# 清除缓存 -php think clear -``` - -### 3. 部署前端代码 - -```bash -cd frontend - -# 上传修复后的文件 -# - sheep/platform/pay.js -# - sheep/platform/provider/ccblife/api.js - -# 重新打包发布 -``` - -### 4. 数据库迁移(如果字段缺失) - -```sql --- 检查字段是否存在 -SHOW COLUMNS FROM `fa_shopro_order` LIKE 'ccb_sync_error'; - --- 如果不存在,添加字段 -ALTER TABLE `fa_shopro_order` -ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因'; -``` - -### 5. 监控上线 - -```bash -# 实时监控notify日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行' - -# 监控查询接口调用 -tail -f runtime/log/$(date +%Y%m)/*.log | grep 'queryPaymentStatus' -``` - ---- - -## ⚠️ 回滚方案 - -如遇紧急问题,可立即回滚: - -```bash -# 回滚后端 -mv addons/shopro/controller/Ccbpayment.php.bak addons/shopro/controller/Ccbpayment.php -mv addons/shopro/library/ccblife/CcbPaymentService.php.bak addons/shopro/library/ccblife/CcbPaymentService.php - -# 清除缓存 -php think clear - -# 回滚前端(重新发布旧版本代码) -``` - ---- - -## 📞 技术支持 - -**开发者**: Billy -**修复日期**: 2025-01-20 -**建行文档版本**: v2.20 (2025-07-25) - -如有疑问,请查阅: -- 建行接入文档: `/doc/建行相关App服务方接入文档v2.20_20250725.html` -- 本修复报告: `/doc/建行支付架构修复报告.md` -- 加密修复报告: `/doc/建行支付对接修复报告.md` - ---- - -## 📝 变更历史 - -| 版本 | 日期 | 修改内容 | -|-----|------|---------| -| v1.0 | 2025-01-20 | 初始版本,完成严重安全漏洞和架构偏离修复 | -| v2.0 | 2025-01-20 | **重大修正**: 根据建行流程图修正订单同步时机 - 推送未支付订单在createPayment,更新已支付订单在notify | - ---- - -**修复完成,已做好生产环境部署准备!** ✅