草庐IT

Java集成建行龙支付接口(详细)

dacheng_liu 2024-02-02 原文

目录

一、准备工作

二、开始对接

三、总结

一、准备工作

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=&REGINFO=&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位,就会顺利入坑。因为这次是全部

7. 回调验签坑3:需要引入建行提供验签的jar包;

有关Java集成建行龙支付接口(详细)的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby-on-rails - 如何使辅助方法在 Rails 集成测试中可用? - 2

    我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel

  3. ruby-on-rails - 我如何将 Hoptoad 与 DelayedJob 和 DaemonSpawn 集成? - 2

    我一直很高兴地使用DelayedJob习惯用法:foo.send_later(:bar)这会调用DelayedJob进程中对象foo的方法bar。我一直在使用DaemonSpawn在我的服务器上启动DelayedJob进程。但是...如果foo抛出异常,Hoptoad不会捕获它。这是任何这些包中的错误...还是我需要更改某些配置...或者我是否需要在DS或DJ中插入一些异常处理来调用Hoptoad通知程序?回应下面的第一条评论。classDelayedJobWorker 最佳答案 尝试monkeypatchingDelayed::W

  4. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  5. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  6. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  7. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  8. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  9. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  10. jenkins部署1--jenkins+gitee持续集成 - 2

    前置步骤我们都操作完了,这篇开始介绍jenkins的集成。话不多说,看操作1、登录进入jenkins后会让你选择安装插件,选择第一个默认的就行。安装完成后设置账号密码,重新登录。2、配置JDK和Git都需要执行路径,所以需要先把执行路径找到,先进入服务器的docker容器,2.1JDK的路径root@69eef9ee86cf:/usr/bin#echo$JAVA_HOME/usr/local/openjdk-82.2Git的路径root@69eef9ee86cf:/#whichgit/usr/bin/git3、先配置JDK和Git。点击:ManageJenkins>>GlobalToolCon

随机推荐