一、依赖
<dependency>
<groupId>io.gitee.hwcgetit</groupId>
<artifactId>saobei-open-sdk</artifactId>
<version>1.1.8</version>
</dependency>
二、入参(dto)
package com.saobei.entity.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* @author yfl
* @date 2023/02/10
* <p>
* * * 小程序支付 入参
*/
@Data
@Accessors(chain = true)
public class SaoBeiMiniPayDto implements Serializable {
/**
* 支付方式: 010-微信、020-支付宝、060-qq钱包、080-京东钱包、090-口碑、100-翼支付、110-银联二维码、140-和包支付(仅限和包通道)、000-自动识别类型
*/
@NotNull(message = "支付方式不能为空")
private String payType;
/**
* 收银机、POS机等设备的公网IP(IPV4格式 ,人行侧风控主要依据,请真实填写)
*/
private String terminalIp;
/**
* 商户侧门店或者终端编号,请先与运营同事确认是否在扫呗平台配置过映射,若没有配置映射,请勿传值,否则支付会报错。
*/
private String deviceNo;
/**
* 终端流水号,填写商户系统的支付订单号,不可重复
*/
@NotNull(message = "订单号不能为空")
private String terminalTrace;
/**
* 终端交易时间,yyyyMMddHHmmss,全局统一时间格式
*/
@JsonFormat(pattern = "yyyyMMddHHmmss")
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
@NotNull(message = "交易时间不能为空")
private String terminalTime;
/**
* 金额,单位分
*/
@NotNull(message = "金额不能为空 (单位:分)")
private String totalFee;
/**
* 微信侧商户公众号appid
*/
private String subAppId;
/**
* 用户标识(微信openid,支付宝userid),pay_type为010及020时必填
*/
private String openid;
/**
* 订单描述,显示在订单详情页面,禁止使用+,空格,/,?,%,#,&,=这几类特殊符号
*/
private String orderBody;
/**
* 外部系统通知地址
*/
private String notifyUrl;
/**
* 附加数据,原样返回
*/
private String attach;
/**
* 订单包含的商品列表信息,Json格式。
* [{"goods_id":"test001","goods_name":"技术单品1","price":"150","quantity":"2"},{"goods_id":"test002","goods_name":"技术单品2","price":"150","quantity":"2"}]
*/
private String goodsDetail;
/**
* 微信侧订单优惠标记,代金券或立减优惠功能的参数(字段值:cs和bld)
*/
private String goodsTag;
/**
* 优惠券串码
*/
private String couponNo;
/**
* 优惠券凭证
*/
private String couponCredential;
/**
* 微信门店编号和支付宝外部自定义门店编号,透传。微信对应scene_info(场景信息)中的门店id。 支付宝自定义门店编号不能随便传,在确认门店编号存在的情况下传值,否则影响支付
*/
private String customStoreId;
/**
* 支付宝官方门店编号
*/
private String officialStoreId;
/**
* 点餐场景类型: qr_order(店内扫码点餐)、pre_order(预点到店自提)、home_delivery (外送到家)、direct_payment(直接付款)、other(其他)
*/
private String foodOrderType;
/**
* 商户号
*/
private String merchantNo;
/**
* 终端号
*/
private String terminalId;
/**
* 支付密钥
*/
private String accessToken;
/**
* 签名
*/
private String keySign;
}
三、支付常量
package com.saobei.entity.constants;
/**
* @author yfl
* @date 2023/02/09
* <p>
* * * 扫呗常量
*/
public interface SaoBeiConstants {
/**
* 版本号 (当前版本:202) 根据调用的SDK版本来决定版本号
*/
String PAY_VER = "202";
/**
* 机构秘钥
*/
String KEY = "your key";
/**
* 支付秘钥
*/
String ACCESS_TOKEN = "your token";
/**
* 机构号
*/
String INST_NO = "your inst_no";
/**
* 商户号
*/
String MERCHANT_NO = "your metchant_no";
/**
* 终端号
*/
String TERMINAL_ID = "your_id";
}
根据业务需求来定义常量,考虑有多个商户的情况下可以将支付的关键信息动态处理。单个商户的话可以内置,但是要注意代码安全哦。
四、枚举
package com.saobei.entity.enums;
import lombok.Getter;
/**
* @author yfl
* @date 2023/02/09
* <p>
* * * 扫呗请求接口类型枚举类
*/
@Getter
public enum ServerIdEnum {
ONE("010"),
TWO("011"),
THREE("012"),
FOUR("015"),
FIVE("020"),
SIX("030"),
SEVEN("040"),
EIGHT("041"),
NINE("031"),
TEN("016");
/*
* 接口类型:
* 付款码支付-010、扫码支付-011、公众号预支付-012、小程序支付接口-015、支付查询-020
* 退款申请-030、撤销交易-040、关闭订单-041、退款订单查询-031、聚合码支付-016
*/
private final String type;
ServerIdEnum(String type) {
this.type = type;
}
}
五、Controller层
package com.saobei.controller;
import com.alibaba.fastjson.JSON;
import com.balledu.common.core.domain.ResultData;
import com.balledu.common.core.utils.QrcodeUtil;
import com.saobei.entity.dto.*;
import com.saobei.entity.enums.ServerIdEnum;
import com.saobei.open.sdk.DefaultSaobeiApiClient;
import com.saobei.open.sdk.SaobeiApiClient;
import com.saobei.open.sdk.model.requst.trade.*;
import com.saobei.open.sdk.model.response.trade.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.saobei.entity.constants.SaoBeiConstants.PAY_VER;
/**
* @author yfl
* @date 2023/02/09
* <p>
* 扫呗支付
*/
@RestController
@RequestMapping("/saobei")
@Slf4j
public class SaoBeiController {
/**
* 小程序支付
* ********
*
* @param dto 入参
* @return 返回结果
*/
@PostMapping("/minipay")
public ResultData minipay(@Validated @RequestBody SaoBeiMiniPayDto dto) {
try {
SaobeiApiClient<SaobeiMiniPayRequest, SaobeiMiniPayResponse> client = new DefaultSaobeiApiClient<>(dto.getAccessToken());
SaobeiMiniPayRequest request = new SaobeiMiniPayRequest();
request.setPay_ver(PAY_VER);
request.setPay_type(dto.getPayType());
request.setService_id(ServerIdEnum.FOUR.getType());
request.setMerchant_no(dto.getMerchantNo());
request.setTerminal_id(dto.getTerminalId());
request.setTerminal_trace(dto.getTerminalTrace());
request.setTerminal_time(dto.getTerminalTime());
request.setOpen_id(dto.getOpenid());
request.setSub_appid(dto.getSubAppId());
request.setTotal_fee(dto.getTotalFee());
request.setOrder_body(dto.getOrderBody() == null ? "场馆通订单" : dto.getOrderBody());
log.info("请求报文: {}" + JSON.toJSONString(request));
SaobeiMiniPayResponse response = client.execute(request);
log.info("返回报文: {}" + JSON.toJSONString(response));
return ResultData.createSuccessJsonResult(response);
} catch (Exception e) {
log.error("调用遭遇异常,原因:{}" + e.getMessage() + e);
return ResultData.createFailJsonResult("调用遭遇异常,原因:" + e.getMessage() + e);
}
}
}
Java内部调用扫呗SDK不用自己去验签签名(keySign),其他语言可能需要此操作。
注意:
● 该接口为预支付接口,还未进行支付,故该接口不返回优惠字段。优惠字段会在支付完后,通过回调接口返回,或者通过查询接口获取。
● 请求完这个接口得到的返回参数直接去调微信小程序/支付宝小程序的js支付(参见下方描述),参数值不用做变化
获取用户标识及调起js支付文档:
1、微信小程序
● 获取openid: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
● 调js支付: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=5
2、支付宝小程序
● 获取userid: https://opendocs.alipay.com/mini/05dxgc?pathHash=1a3ecb13
● 调js支付: https://opendocs.alipay.com/mini/api/openapi-pay
六、返回结果(vo)
参数名称 | 类型 | 长度 | 必填 | 说明 |
return_code | String | 2 | Y | 响应码: 01 成功,62 失败,响应码仅代表通信状态,不代表业务结果 |
return _msg | String | 128 | Y | 返回信息提示,“预支付请求成功”“预支付请求失败”等 |
key _sign | String | 32 | Y | 签名串 |
result_code | String | 2 | N | 业务结果:01 成功 02 失败 |
pay_type | String | 3 | N | 支付方式:010 微信 020 支付宝 |
merchant_name | String | 40 | N | 商户名称 |
merchant_no | String | 15 | N | 商户号 |
terminal_id | String | 8 | N | 终端号 |
device_no | String | 32 | N | 商户终端设备号(商户自定义,如门店编号).必须在平台已配苦过 |
terminal_trace | String | 32 | N | 终端流水号,商户系统的订单号,系统原样返回 |
terminal_time | String | 14 | N | 终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 |
total_fee | String | 12 | N | 金额,单位分 |
out_trade_no | String | 32 | N | 平台唯一订单号 |
appld | String | 16 | N | 微信小程序支付返回字段,公众号id |
time_stamp | String | 32 | N | 微信小程序支付返回字段,时间戳,示例: 1414561699标准北京时间,时区为东八区,自1970年1月1日 0点0分(秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。 |
nonce_str | String | 32 | N | 微信小程序支付返回字段,随机字符串 |
package _str | String | 128 | N | 微信小程序支付返回字段,订单详情扩展字符串。 |
sign_type | String | 32 | N | 微信小程序支付返回字段,签名方式,示例: MD5.RSA |
pay_sign | String | 512 | N | 微信小程序支付返回字段,签名 |
ali_trade_no | String | 32 | N | 支付宝小程序支付返回字段用于调起支付宝小程序 |
● 注意:
● 请求完这个接口得到的返回参数直接去调微信小程序/支付宝小程序,参数值不用做变化。
1)先判断协议字段(return_code)返回,再判断业务(result_code)返回,最后判断交易状态。
2)小程序和公众号支付时,当return_code和result_code两个值都返回01时,才能取值去请求微信支付。否则会由于取不到参数,微信支付报缺少参数错误。
七、支付中遇到的问题
小程序支付成功后,不点击完成,不会进入到success回调,怎么解决?
(不知道大家有没有遇见过这个问题,解决办法其实有很多种,反正我是踩了一次这个坑,虽然已经解决了,印象很深刻,貌似好像只是针对于扫呗支付我才遇见过,其他的支付还没有踩坑,大家可以思考一下!)