目录
一、准备工作
二、开始对接
三、总结
一、准备工作
1. 获取建行龙支付对接文档(注意:建行会给指定邮箱发送16个rar的压缩包)都下载完才能获取到完整文档,解压完可以看到名为“建行龙支付接入指南V1.32”的文件夹,里面的内容为6个文件夹1个pdf文档。
2. 获取各种资料
1).微信商户编号
2).商户柜台编号
3).建行商户编号
4).终端号
5).分行代码
6).商户公钥
3. 开通权限
注意:需要联系分管贵公司的建行工作人员,开通服务器实时反馈和退款的权限。
二、开始对接
这里我使用的是SpringBoot框架进行对接
1. 配置application.yml文件
jh:
merchantId: 商户编号
posId: 柜台编号
branchId: 分行代码
subAppId: 小程序APPID(这里参考贵公司的支付渠道,我方需使用微信小程序支付)
tradeType: 支付类型(这里参考贵公司的支付渠道,我方需使用微信小程序支付)
pub: 公钥串
url: 接口地址
operatorCode: 商户操作员编号
pwd: 操作员密码
wlptServerIp: 外联平台ip地址
wlptPort: 外联平台端口号
2. 统一下单接口对接
2.1 下单接口参数实体类
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.math.BigDecimal;
/**
* 建设银行下单接口参数
* @author snkj
* @create 2022-10-24 10:23
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class JhPlaceOrderInfo {
/**
* 商户代码
* 必填
*/
@JSONField(name="MERCHANTID")
private String MERCHANTID;
/**
* 商户柜台代码
* 必填
*/
@JSONField(name="POSID")
private String POSID;
/**
* 分行代码
* 必填
*/
@JSONField(name="BRANCHID")
private String BRANCHID;
/**
* 订单号(最长30位)
* 必填
*/
@JSONField(name="ORDERID")
private String ORDERID;
/**
* 付款金额
* 必填
*/
@JSONField(name="PAYMENT")
private String PAYMENT;
/**
* 币种
* 必填
* 缺省为 01-人民币
*/
@JSONField(name="CURCODE")
private String CURCODE;
/**
* 备注信息1
* 一般作为商户自定义备注信
* 息使用,可在对账单中显示
*/
@JSONField(name="REMARK1")
private String REMARK1;
/**
* 备注信息2
* 一般作为商户自定义备注信
* 息使用,可在对账单中显示
*/
@JSONField(name="REMARK2")
private String REMARK2;
/**
* 交易码
* 由建行统一分配为 530590
* 必填
*/
@JSONField(name="TXCODE")
private String TXCODE;
/**
* MAC 校验域
* 采用标准 MD5 算法,由商户实现
* 必填
*/
@JSONField(name="MAC")
private String MAC;
/**
* 接口类型
* 分行业务人员在 P2 员工渠道后台设置防钓鱼的开关。
* 1- 防钓鱼接口
* 必填
*/
@JSONField(name="TYPE")
private String TYPE;
/**
* 公钥后 30 位
* 商户从建行商户服务平台下载,截取后 30 位。
* 仅作为源串参加 MD5 摘要,不作为参数传递
* 必填
*/
@JSONField(name="PUB")
private String PUB;
/**
* 网关类型
* 默认送 0
* 必填
*/
@JSONField(name="GATEWAY")
private String GATEWAY;
/**
* 客户端 IP
* 客户在商户系统中的 IP,即客户登陆(访问)商户系统时使用的 ip)
*/
@JSONField(name="CLIENTIP")
private String CLIENTIP;
/**
* 客户注册信息
* 客户在商户系统中注册的信息,中文需使用 escape 编码
*/
@JSONField(name="REGINFO")
private String REGINFO;
/**
* 商品信息
* 客户购买的商品中文需使用 escape 编码
*/
@JSONField(name="PROINFO")
private String PROINFO;
/**
* 商户 URL
* 商户送空值即可;具体请看 REFERER 设置说明
*/
@JSONField(name="EFERER")
private String EFERER;
/**
* 订单超时时间
* 格式:
* YYYYMMDDHHMMSS如:
* 20120214143005
* 银行系统时间> TIMEOUT
* 时拒绝交易,若送空值则不
* 判断超时。
* 当该字段有值时参与 MAC
* 校验,否则不参与 MAC 校
* 验。
*/
@JSONField(name="TIMEOUT")
private String TIMEOUT;
/**
* 交易类型
* JSAPI-- 公 众 号 支 付 、
* MINIPRO--小程序
* 必填
*/
@JSONField(name="TRADE_TYPE")
private String TRADE_TYPE;
/**
* 小程序/公众号的 APPID
* 当前调起支付的小程序/公众号 APPID
* 必填
*/
@JSONField(name="SUB_APPID")
private String SUB_APPID;
/**
* 用户子标识
* 用户在小程序/公众号 appid
* 下的唯一标识,小程序通过
* wx.login 获取,接口文档地
* 址 :
* https://developers.weixin.qq.com/miniprogram/dev/api/apilogin.html?t=20161122
* 必填
*/
@JSONField(name="SUB_OPENID")
private String SUB_OPENID;
/**
* 渠道商号
* 对于商户自定义的渠道商号当该字段有值时参与 MAC校验,否则不参与 MAC 校验。
*/
@JSONField(name="WX_CHANNELID")
private String WX_CHANNELID;
/**
* 返回信息位图
* 共 20 位,商户通知是否返回某
* 个字段的位图,0 或空-不返回,
* 1-返回。
* 第 1 位:是否返回 OPENID 和
* SUB_OPENID
* 第 2 位:保留位,默认送 0
* 第 3 位:保留位,默认送 0
* 第 4 位:是否返回支付详细信息
* 字段
* 示例:10000000000000000000
*/
@JSONField(name="RETURN_FIELD")
private String RETURN_FIELD;
/**
* 实名支付
* 实名支付功能,包含类型、
* 证件号、姓名三个子域(如果本字段
* 出现,那么本字
* 段包含的三个子域均需出现。详见下
* 文说明5)USERPARAM字段说明)。
* 当该字段有值时参与MAC校验,否则不
* 参与MAC校验。
* 暂未上线,请忽略
*/
@JSONField(name="USERPARAM")
private String USERPARAM;
}
2.2 下单接口Service以及实现
/**
* 建行支付service
* @author snkj
* @create 2022-11-02 18:10
*/
public interface IJhPayService {
/**
* 建行统一下单
* @param jhPlaceOrderInfo
*/
public Map<String,Object> unifiedPlaceOrder(JhPlaceOrderInfo jhPlaceOrderInfo);
}
/**
* 建行支付service实现
*
* @author snkj
* @create 2022-11-02 18:11
*/
@Service
public class JhPayServiceImpl implements IJhPayService {
// 获取application.yml的配置信息
@Value("${jh.merchantId}")
private String merchantId;
@Value("${jh.posId}")
private String posId;
@Value("${jh.branchId}")
private String branchId;
@Value("${jh.subAppId}")
private String subAppId;
@Value("${jh.tradeType}")
private String tradeType;
@Value("${jh.pub}")
private String pub;
@Value("${jh.url}")
private String url;
@Value("${jh.operatorCode}")
private String operatorCode;
@Value("${jh.pwd}")
private String pwd;
@Value("${jh.wlptServerIp}")
private String wlptServerIp;
@Value("${jh.wlptPort}")
private String wlptPort;
/**
* 建行统一下单
*
* @param jhPlaceOrderInfo
*/
@Override
public Map<String, Object> unifiedPlaceOrder(JhPlaceOrderInfo jhPlaceOrderInfo) {
Map<String, Object> map = new HashMap<>();
// -----------封装请求参数-----------
// 生成订单号
OrderNoUtils idWorker = new OrderNoUtils(0, 0);
long orderId = idWorker.nextId();
jhPlaceOrderInfo.setORDERID(String.valueOf(orderId));
jhPlaceOrderInfo.setPAYMENT("支付金额");
jhPlaceOrderInfo.setSUB_OPENID("小程序/微信公众号,支付人的openId");
// 注意,这里要对中文进行编码,工具类参考下方2.4部分
jhPlaceOrderInfo.setPROINFO(EscapeUtils.escape("设备租用押金") + jhPlaceOrderInfo.getPAYMENT());
// 1. 截取公钥后30位
String pubSub = pub.substring(pub.length() - 30);
jhPlaceOrderInfo.setMERCHANTID(merchantId);
jhPlaceOrderInfo.setPOSID(posId);
jhPlaceOrderInfo.setBRANCHID(branchId);
jhPlaceOrderInfo.setTRADE_TYPE(tradeType);
jhPlaceOrderInfo.setTIMEOUT(DateUtils.addMinute(15));
jhPlaceOrderInfo.setSUB_APPID(subAppId);
jhPlaceOrderInfo.setCURCODE("01");
jhPlaceOrderInfo.setTXCODE("530590");
jhPlaceOrderInfo.setTYPE("1");
jhPlaceOrderInfo.setGATEWAY("0");
jhPlaceOrderInfo.setPUB(pubSub);
// 2. 获取加密后的mac,这里需要注意,参与MAC的参数是固定排序格式,需参考文档手动排序并MD5加密
String mac = getMac(jhPlaceOrderInfo);
jhPlaceOrderInfo.setMAC(mac);
// 3. 获取请求参数&连接
String paramsStr = getParamsStr(JSON.parseObject(JSON.toJSONString(jhPlaceOrderInfo), Map.class));
String result = HttpUtil.post(url, paramsStr);
if (StringUtils.isEmpty(result)) {
// 表示返回为空,自行处理
}
JSONObject jsonObject = JSON.parseObject(result);
if (!jsonObject.getString("SUCCESS").equals("true")) {
// SUCCESS返回状态码不为true时,表示通信失败,自行处理
}
// 获取接口返回的payUrl
String payUrl = jsonObject.getString("PAYURL");
if (StringUtils.isEmpty(payUrl)) {
// 如果PAYURL为空,自行处理
}
// 返回结果参考{ "SUCCESS":"true", "PAYURL":"https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_08_EPAY?BRANCHID=120000000&TXCODE=530590&SUB_APPID=wx8b28b84282cce9fc&CCB_IBSVersion=V6&CURCODE=01&GATEWAY=0&PROINFO=%25u79DF%25u7528%25u8BBE%25u5907%25u62BC%25u91D11.00&MERCHANTID=105000789993067&ORDERID=575469756870557696&RETURN_FIELD=10000000000000000000&POSID=071114078&PAYMENT=1.00&TRADE_TYPE=MINIPRO&MAC=5e4a3586bfbeb6f1e86b1ba9ea12d19e&SUB_OPENID=oz46x5GAP3LnbSJVozyozYN-63Tw&TYPE=1&TIMEOUT=20221107164541&QRCODE=1&CHANNEL=1"}
// PAYURL不为空时,对于小程序支付而言,需手动发送一下get请求,获取小程序支付所需的参数,具体参数请参考相关文档
// 小程序:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=7_7&index=5
// 公众号:https://pay.weixin.qq.com/wiki/doc/api/jsapi_sl.php?chapter=7_7&index=6
String payUrlResult = HttpUtil.get(payUrl);
if (StringUtils.isEmpty(payUrlResult)) {
// 请求异常,自行处理
}
JSONObject payUrlJson = JSON.parseObject(payUrlResult);
if (!payUrlJson.getString("ERRCODE").equals("000000")) {
// ERRCODE为错误码,000000 表示交易成功,非 000000 表示交易失败,错误信息可以查看 ERRMSG 字段
}
map.put("appId", payUrlJson.getString("appId"));
map.put("timeStamp", payUrlJson.getString("timeStamp"));
map.put("nonceStr", payUrlJson.getString("nonceStr"));
map.put("package", payUrlJson.getString("package"));
map.put("signType", payUrlJson.getString("signType"));
map.put("paySign", payUrlJson.getString("paySign"));
return map;
}
/**
* 生成mac并md5加密
*
* @param jhPlaceOrderInfo
* @return
*/
private String getMac(JhPlaceOrderInfo jhPlaceOrderInfo) {
String postParams = "MERCHANTID=" + jhPlaceOrderInfo.getMERCHANTID() + "&POSID=" + jhPlaceOrderInfo.getPOSID() + "" +
"&BRANCHID=" + jhPlaceOrderInfo.getBRANCHID() + "&ORDERID=" + jhPlaceOrderInfo.getORDERID() + "&PAYMENT=" + jhPlaceOrderInfo.getPAYMENT() + "" +
"&CURCODE=01&TXCODE=530590&REMARK1=&REMARK2=&TYPE=1&PUB=" + jhPlaceOrderInfo.getPUB() + "&GATEWAY=0&CLIENTIP=®INFO=&PROINFO=" + jhPlaceOrderInfo.getPROINFO() + "&REFERER=" +
"&TIMEOUT=" + jhPlaceOrderInfo.getTIMEOUT() + "&TRADE_TYPE=" + jhPlaceOrderInfo.getTRADE_TYPE() + "" +
"&SUB_APPID=" + jhPlaceOrderInfo.getSUB_APPID() + "&SUB_OPENID=" + jhPlaceOrderInfo.getSUB_OPENID() + "";
return MD5Utils.string2MD5(postParams);
}
/**
* 生成提交参数
*
* @param params
* @return
*/
private String getParamsStr(Map params) {
StringBuffer toBeMacStr = new StringBuffer();
Set<Map.Entry<String, Object>> entries = params.entrySet();
Iterator iterator = entries.iterator();
while (iterator.hasNext()) {
Object itset = iterator.next();
Map.Entry entry = (Map.Entry) itset;
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (StringUtils.isNotEmpty(value)) {
if (!key.equals("PUB")) {
toBeMacStr.append("&" + key + "=" + value);
}
}
}
return toBeMacStr.toString();
}
}
2.3 服务器通知回调实体类以及Controller
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* @author snkj
* @create 2022-11-08 19:05
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class JhNotifyInfo {
/**
* 商户柜台代码
*/
@JSONField(name="POSID")
private String POSID;
/**
* 分行代码
*/
@JSONField(name="BRANCHID")
private String BRANCHID;
/**
* 订单号(最长30位)
*/
@JSONField(name="ORDERID")
private String ORDERID;
/**
* 付款金额
*/
@JSONField(name="PAYMENT")
private String PAYMENT;
/**
* 币种
* 缺省为 01-人民币
*/
@JSONField(name="CURCODE")
private String CURCODE;
/**
* 备注信息1
* 一般作为商户自定义备注信
* 息使用,可在对账单中显示
*/
@JSONField(name="REMARK1")
private String REMARK1;
/**
* 备注信息2
* 一般作为商户自定义备注信
* 息使用,可在对账单中显示
*/
@JSONField(name="REMARK2")
private String REMARK2;
/**
* 账户类型
* 服务器通知中有此字段返回且参与验签
* AL:代表支付宝支付
* WX:代表微信支付
* 其他:代表建行支付或跨行付
*/
@JSONField(name="ACC_TYPE")
private String ACC_TYPE;
/**
* 成功-Y,失败-N
*/
@JSONField(name="SUCCESS")
private String SUCCESS;
/**
* 接口类型
* 分行业务人员在 P2 员工渠道后台设置防钓鱼的开关。
* 1- 防钓鱼接口
*/
@JSONField(name="TYPE")
private String TYPE;
/**
* Referer信息
* 分行业务人员在P2员工渠道后台设置防钓鱼开关。
* 1.开关关闭时,无此字段返回且不参与验签。
* 2.开关打开时,有此字段返回且参与验签。
*/
@JSONField(name="REFERER")
private String REFERER;
/**
* 客户端IP
* 客户在商户系统中的IP,即客户登陆(访问)商户系统时使用的IP)
* 分行业务人员在P2员工渠道后台设置防钓鱼开关。
* 1.开关关闭时,无此字段返回且不参与验签。
* 2.开关打开时,有此字段返回且参与验签。
*/
@JSONField(name="CLIENTIP")
private String CLIENTIP;
/**
* 系统记账日期
* 商户登陆商户后台设置返回记账日期的开关
* 1.开关关闭时,无此字段返回且不参与验签。
* 2.开关打开时,有此字段返回且参与验签。参数值格式为YYYYMMDD(如20100907)。
*/
@JSONField(name="ACCDATE")
private String ACCDATE;
/**
* 分期期数
* 从商户传送的信息中获得;
* 当分期期数为空或无此字段上送时,无此字段返回且不参与验签,否则有此字段返回且参与验签。
*/
@JSONField(name="INSTALLNUM")
private String INSTALLNUM;
/**
* 错误信息
* 该值默认返回为空,商户无需处理,仅需参与验签即可。当有分期期数返回时,则有ERRMSG字段返回且参与验签,否则无此字段返回且不参与验签。
*/
@JSONField(name="ERRMSG")
private String ERRMSG;
/**
* 支付账户信息
* 分行业务人员在P2员工渠道后台设置防钓鱼开关和返回账户信息的开关。
* 1.开关关闭时,无此字段返回且不参与验签。
* 2.开关打开但支付失败时,无此字段返回且不参与验签。
* 3.开关打开且支付成功时,有此字段返回且参与验签。参数值格式如下:“姓名|账号加密后的密文”。
* 解密方法请参考“商户通知验签包“文件夹下的《USERMSG》压缩包
*/
@JSONField(name="USRMSG")
private String USRMSG;
/**
* 客户加密信息
* 分行业务人员在P2员工渠道后台设置防钓鱼开关和客户信息加密返回的开关。
* 1.开关关闭时,无此字段返回且不参与验签
* 2.开关打开时,有此字段返回且参数验签。参数值格式如下:“证件号密文|手机号密文”。该字段不可解密。
*/
@JSONField(name="USRINFO")
private String USRINFO;
/**
* 实付金额
* 优惠之后的实际支付金额。
* 目前只针对白名单商户返回,无此字段返回且不参与验签,有此字段返回且参与验签。
*/
@JSONField(name="DISCOUNT")
private String DISCOUNT;
/**
* 返回客户的积分使用情况,格式如下:
* {“APnt_Hpn_Num”:”积分发生数量”,”APntCmpt_Amt”:”积分抵扣金额”}
* 当综合积分字段为空或无此字段上送时,无此字段返回且不参与验签,否则有此字段返回且参与验签。
*/
@JSONField(name="ZHJF")
private String ZHJF;
/**
* 客户识别号
* 提交建行的参数RETURN_FIELD打开对应开关才返回该字段。
* 客户识别码, 微信、支付宝、龙支付时返回。
* 有该字段返回时(无论返回值是空还是其他),需参与验签,否则无需参与验签。
*/
@JSONField(name="OPENID")
private String OPENID;
/**
* 用户子标识
* 提交建行的参数RETURN_FIELD打开对应开关才返回该字段。
* 微信支付专有字段。
* 子商户appid下用户唯一标识,如需返回则请求时需要传sub_appid。
* 有该字段返回时(无论返回值是空还是其他),需参与验签,否则无需参与验签。
*/
@JSONField(name="SUB_OPENID")
private String SUB_OPENID;
/**
* 支付详细信息
* 支付详细信息。当RETURN_FIELD字段第四位上送1时返回。
* 字段说明见下方[支付详细信息字段说明]
* 格式如下:
* {“TYPE“:"ALIPAY",“PAY_CHANNEL“:"BANKCARD",“DEBIT_CREDIT_TYPE“:"DEBIT_CARD",“THIRD_TRADE_NO“:"2018010521001004890523646975"}
* 为防止特殊字符,建行会将该参数值用utf-8编码进行urlencode,因此商户需先decode之后才能拿到明文。
* 编码之后为:
* %7B%22TYPE%22%3A%22ALIPAY%22%2C%22PAY_CHANNEL%22%3A%22BANKCARD%22%2C%22DEBIT_CREDIT_TYPE%22%3A%22DEBIT_CARD%22%2C%22THIRD_TRADE_NO%22%3A%222018010521001004890523646975%22%7D
* 有该字段返回时(无论返回值是空还是其他),需参与验签,否则无需参与验签,参与签名的是encode之后的参数值。
*/
@JSONField(name="PAYMENT_DETAILS")
private String PAYMENT_DETAILS;
/**
* 数字签名
*/
@JSONField(name="SIGN")
private String SIGN;
}
/**
* @author snkj
* @create 2022-11-02 18:14
*/
@Slf4j
@RestController
@RequestMapping("/pay")
public class ApiPayController {
// 获取application.yml中的配置
@Value("${jh.merchantId}")
private String merchantId;
@Value("${jh.posId}")
private String posId;
@Value("${jh.branchId}")
private String branchId;
@Value("${jh.subAppId}")
private String subAppId;
@Value("${jh.tradeType}")
private String tradeType;
@Value("${jh.pub}")
private String pub;
/**
* 建行支付通知回调接口
*
* @return
*/
@PostMapping("/jh/notify")
public String pay(JhNotifyInfo jhNotifyInfo) {
log.info("建行回调通知参数[{}]", JSON.toJSONString(jhNotifyInfo));
RSASig rsaSig = new RSASig();
rsaSig.setPublicKey(pub);
String src = "POSID=" + jhNotifyInfo.getPOSID() + "&BRANCHID=" + jhNotifyInfo.getBRANCHID() + "&ORDERID=" + jhNotifyInfo.getORDERID() +
"&PAYMENT=" + jhNotifyInfo.getPAYMENT() + "&CURCODE=" + jhNotifyInfo.getCURCODE() + "&REMARK1=" + jhNotifyInfo.getREMARK1() + "&REMARK2=" + jhNotifyInfo.getREMARK2() + "&ACC_TYPE=" + jhNotifyInfo.getACC_TYPE() +
"&SUCCESS=" + jhNotifyInfo.getSUCCESS() + "&TYPE=" + jhNotifyInfo.getTYPE() + "&REFERER=" + jhNotifyInfo.getREFERER() + "&CLIENTIP=" + jhNotifyInfo.getCLIENTIP();
// 校验签名
boolean verifySigature = rsaSig.verifySigature(jhNotifyInfo.getSIGN(), src);
if (verifySigature) {
// 验签通过,业务逻辑自行处理
// 这里需要调用建行龙支付提供的查询订单接口以保证订单确实支付成功
log.info("验签通过");
return "SUCCESS";
} else {
// 验签失败
log.info("验签失败");
return "FAIL";
}
}
}
<dependency>
<groupId>netpay.merchant.crypto</groupId>
<artifactId>netpay</artifactId>
<version>0.0.1</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/netpay.jar</systemPath>
</dependency>
2.4 部分工具类
/**
* 时间工具类
*
* @author snkj
*/
public class DateUtils {
public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
public static final String parseDateToStr(final String format, final Date date) {
return new SimpleDateFormat(format).format(date);
}
/**
* 当前时间加分钟
* @param minute
* @return
*/
public static String addMinute(int minute){
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE, minute);
return parseDateToStr(YYYYMMDDHHMMSS,nowTime.getTime());
}
}
/**
* 对中文进行escape编码
* @author snkj
* @create 2022-11-02 19:07
*/
public class EscapeUtils {
private final static String[] hex = { "00", "01", "02", "03", "04", "05",
"06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", "10",
"11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B",
"1C", "1D", "1E", "1F", "20", "21", "22", "23", "24", "25", "26",
"27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F", "30", "31",
"32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C",
"3D", "3E", "3F", "40", "41", "42", "43", "44", "45", "46", "47",
"48", "49", "4A", "4B", "4C", "4D", "4E", "4F", "50", "51", "52",
"53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D",
"5E", "5F", "60", "61", "62", "63", "64", "65", "66", "67", "68",
"69", "6A", "6B", "6C", "6D", "6E", "6F", "70", "71", "72", "73",
"74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E",
"7F", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
"8A", "8B", "8C", "8D", "8E", "8F", "90", "91", "92", "93", "94",
"95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F",
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA",
"AB", "AC", "AD", "AE", "AF", "B0", "B1", "B2", "B3", "B4", "B5",
"B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF", "C0",
"C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB",
"CC", "CD", "CE", "CF", "D0", "D1", "D2", "D3", "D4", "D5", "D6",
"D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF", "E0", "E1",
"E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC",
"ED", "EE", "EF", "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7",
"F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF" };
private final static byte[] val = { 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00, 0x01,
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F,
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F };
/** */
/**
* 编码
* @param s
* @return
*/
public static String escape(String s) {
StringBuffer sbuf = new StringBuffer();
int len = s.length();
for (int i = 0; i < len; i++) {
int ch = s.charAt(i);
if ('A' <= ch && ch <= 'Z') {
sbuf.append((char) ch);
} else if ('a' <= ch && ch <= 'z') {
sbuf.append((char) ch);
} else if ('0' <= ch && ch <= '9') {
sbuf.append((char) ch);
} else if (ch == '-' || ch == '_' || ch == '.' || ch == '!'
|| ch == '~' || ch == '*' || ch == '\'' || ch == '('
|| ch == ')') {
sbuf.append((char) ch);
} else if (ch <= 0x007F) {
sbuf.append('%');
sbuf.append(hex[ch]);
} else {
sbuf.append('%');
sbuf.append('u');
sbuf.append(hex[(ch >>> 8)]);
sbuf.append(hex[(0x00FF & ch)]);
}
}
return sbuf.toString();
}
/**
* 解码 说明:本方法保证 不论参数s是否经过escape()编码,均能得到正确的“解码”结果
*
* @param s
* @return
*/
public static String unescape(String s) {
StringBuffer sbuf = new StringBuffer();
int i = 0;
int len = s.length();
while (i < len) {
int ch = s.charAt(i);
if ('A' <= ch && ch <= 'Z') {
sbuf.append((char) ch);
} else if ('a' <= ch && ch <= 'z') {
sbuf.append((char) ch);
} else if ('0' <= ch && ch <= '9') {
sbuf.append((char) ch);
} else if (ch == '-' || ch == '_' || ch == '.' || ch == '!'
|| ch == '~' || ch == '*' || ch == '\'' || ch == '('
|| ch == ')') {
sbuf.append((char) ch);
} else if (ch == '%') {
int cint = 0;
if ('u' != s.charAt(i + 1)) {
cint = (cint << 4) | val[s.charAt(i + 1)];
cint = (cint << 4) | val[s.charAt(i + 2)];
i += 2;
} else {
cint = (cint << 4) | val[s.charAt(i + 2)];
cint = (cint << 4) | val[s.charAt(i + 3)];
cint = (cint << 4) | val[s.charAt(i + 4)];
cint = (cint << 4) | val[s.charAt(i + 5)];
i += 5;
}
sbuf.append((char) cint);
} else {
sbuf.append((char) ch);
}
i++;
}
return sbuf.toString();
}
}
import java.util.HashSet;
import java.util.Set;
/**
* 订单号工具类
* @author snkj
* @create 2022-09-25 16:14
*/
public class OrderNoUtils {
// ==============================Fields===========================================
/**
* 开始时间截 (2018-07-03)
*/
private final long twepoch = 1530607760000L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public OrderNoUtils(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return (((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence);
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/**
* 测试
*/
public static void main(String[] args) {
OrderNoUtils idWorker = new OrderNoUtils(0, 0);
Set set = new HashSet();
long id = idWorker.nextId();
System.out.println(id);
set.add(id);
}
}
其他工具类采用Hutool,请自行百度并引入
三、总结
我方暂时只需要微信小程序之后,故以上案例均为小程序支付,后续会持续更新其他几个支付类型。
列举几个坑,各位看官注意一下
1. 生成MAC签名摘要时,需要商户的柜台公钥后30位;
2. REMARK1和REMARK2可以传递两个备注,但长度不能超过30位,并且要求对中文使用js的escape函数进行编码(参考上方的后端escape编码工具类);
3. PROINFO也需要对中文使用js的escape函数进行编码(参考上方的后端escape编码工具类);
4. 在根据参数拼接MAC签名串时,要注意别把Null拼进去,就是说,要提前将Null => 空值
5. 回调验签坑1:文档中对于参数有返回值的意思是:包括空值,但不包括Null。再翻译一下:就算返回值是个空值,也算有返回值,但如果是Null就不算有返回值,就不参与验签;
6. 回调验签坑2:在验签时还需要商户柜台公钥,如果还像上面那样只截取后面的30位,就会顺利入坑。因为这次是全部;文章来源:https://www.toymoban.com/news/detail-490017.html
7. 回调验签坑3:需要引入建行提供验签的jar包;文章来源地址https://www.toymoban.com/news/detail-490017.html
到了这里,关于Java集成建行龙支付接口(详细)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!