wechat pay score api

This commit is contained in:
xiafang
2020-12-15 19:20:46 +08:00
parent bbdbe44b0b
commit a3b595a8dd
12 changed files with 447 additions and 29 deletions

View File

@@ -109,12 +109,44 @@ public enum WechatPayV3Type {
*/
COMBINE_CLOSE(HttpMethod.POST, "%s/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close"),
/**
* 商户预授权API.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_PERMISSIONS(HttpMethod.POST, "%s/v3/payscore/permissions"),
/**
* 查询与用户授权记录授权协议号API.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_QUEERY_PERMISSIONS_AUTHORIZATION_CODE(HttpMethod.GET, "%s/v3/payscore/permissions/authorization-code/{authorization_code}"),
/**
* 解除用户授权关系授权协议号API.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_TERMINATE_PERMISSIONS_AUTHORIZATION_CODE(HttpMethod.POST, "%s/v3/payscore/permissions/authorization-code/{authorization_code}/terminate"),
/**
* 查询与用户授权记录openidAPI.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_PERMISSIONS_OPENID(HttpMethod.GET, "%s/v3/payscore/permissions/openid/{openid}"),
/**
* 解除用户授权关系openidAPI.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_TERMINATE_PERMISSIONS_OPENID(HttpMethod.POST, "%s/v3/payscore/permissions/openid/{openid}/terminate"),
/**
* 查询用户授权状态API.
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_USER_SERVICE_STATE(HttpMethod.GET, "%s/v3/payscore/user-service-state?service_id={service_id}&appid={appid}&openid={openid}"),
/**
* 创建支付分订单API
*
@@ -133,6 +165,30 @@ public enum WechatPayV3Type {
* @since 1.0.2.RELEASE
*/
PAY_SCORE_CANCEL_USER_SERVICE_ORDER(HttpMethod.POST, "%s/v3/payscore/serviceorder/{out_order_no}/cancel"),
/**
* 修改订单金额API
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_MODIFY_USER_SERVICE_ORDER(HttpMethod.POST, "%s/v3/payscore/serviceorder/{out_order_no}/modify"),
/**
* 完结支付分订单API
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_COMPLETE_USER_SERVICE_ORDER(HttpMethod.POST, "%s/v3/payscore/serviceorder/{out_order_no}/complete"),
/**
* 商户发起催收扣款API
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_PAY_USER_SERVICE_ORDER(HttpMethod.POST, "%s/v3/payscore/serviceorder/{out_order_no}/pay"),
/**
* 同步服务订单信息API
*
* @since 1.0.2.RELEASE
*/
PAY_SCORE_SYNC_USER_SERVICE_ORDER(HttpMethod.POST, "%s/v3/payscore/serviceorder/{out_order_no}/sync"),
/**

View File

@@ -86,7 +86,7 @@ public class SignatureProvider {
/**
* 我方请求加签使用API证书.
* 我方请求前用 SHA256withRSA 加签使用API证书.
*
* @param tenantId the properties key
* @param method the method
@@ -95,7 +95,7 @@ public class SignatureProvider {
* @return the string
*/
@SneakyThrows
public String requestSign(String tenantId,String method, String canonicalUrl, String body) {
public String requestSign(String tenantId, String method, String canonicalUrl, String body) {
Signature signer = Signature.getInstance("SHA256withRSA");
WechatMetaBean wechatMetaBean = wechatMetaContainer.getWechatMeta(tenantId);
signer.initSign(wechatMetaBean.getKeyPair().getPrivate());
@@ -160,7 +160,7 @@ public class SignatureProvider {
}
// 签名
HttpMethod httpMethod = WechatPayV3Type.CERT.method();
String authorization = requestSign(tenantId,httpMethod.name(), canonicalUrl, "");
String authorization = requestSign(tenantId, httpMethod.name(), canonicalUrl, "");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
@@ -177,30 +177,27 @@ public class SignatureProvider {
ArrayNode certificates = bodyObjectNode.withArray("data");
if (certificates.isArray() && certificates.size() > 0) {
CERTIFICATE_MAP.clear();
final CertificateFactory cf = CertificateFactory.getInstance("X509");
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
certificates.forEach(objectNode -> {
JsonNode encryptCertificate = objectNode.get("encrypt_certificate");
String associatedData = encryptCertificate.get("associated_data").asText();
String nonce = encryptCertificate.get("nonce").asText();
String ciphertext = encryptCertificate.get("ciphertext").asText();
String publicKey = decryptResponseBody(tenantId,associatedData, nonce, ciphertext);
String publicKey = decryptResponseBody(tenantId, associatedData, nonce, ciphertext);
ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
Certificate certificate = null;
try {
certificate = cf.generateCertificate(inputStream);
} catch (CertificateException e) {
e.printStackTrace();
}
Certificate certificate = certificateFactory.generateCertificate(inputStream);
String responseSerialNo = objectNode.get("serial_no").asText();
CERTIFICATE_MAP.put(responseSerialNo, certificate);
} catch (CertificateException e) {
throw new PayException("An error occurred while generating the wechat v3 certificate, reason : " + e.getMessage());
}
});
}
}
/**
* 解密响应体.
*
@@ -210,7 +207,7 @@ public class SignatureProvider {
* @param ciphertext the ciphertext
* @return the string
*/
public String decryptResponseBody(String tenantId,String associatedData, String nonce, String ciphertext) {
public String decryptResponseBody(String tenantId, String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
String apiV3Key = wechatMetaContainer.getWechatMeta(tenantId).getV3().getAppV3Secret();
@@ -233,7 +230,6 @@ public class SignatureProvider {
}
}
/**
* Wechat meta container.
*
@@ -249,10 +245,9 @@ public class SignatureProvider {
* @param components the components
* @return string string
*/
private String createSign(String... components) {
private static String createSign(String... components) {
return Arrays.stream(components)
.collect(Collectors.joining("\n", "", "\n"));
}
}

View File

@@ -19,7 +19,7 @@ import java.util.function.Consumer;
* 微信支付回调工具.
* <p>
* 支付通知http应答码为200或204才会当作正常接收当回调处理异常时应答的HTTP状态码应为500或者4xx。
*
* TODO 微信支付分有三个回调通知
* @author felord.cn
* @since 1.0.0.RELEASE
*/

View File

@@ -3,10 +3,7 @@ package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.payscore.CancelServiceOrderParams;
import cn.felord.payment.wechat.v3.model.payscore.QueryServiceOrderParams;
import cn.felord.payment.wechat.v3.model.payscore.UserPayScoreOrderParams;
import cn.felord.payment.wechat.v3.model.payscore.UserServiceStateParams;
import cn.felord.payment.wechat.v3.model.payscore.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -72,7 +69,7 @@ public class WechatPayScoreApi extends AbstractApi {
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> createServiceOrder(UserPayScoreOrderParams params) {
public WechatResponseEntity<ObjectNode> createServiceOrder(UserServiceOrderParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_CREATE_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
@@ -147,16 +144,140 @@ public class WechatPayScoreApi extends AbstractApi {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_CANCEL_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(orderParams.getOutOrderNo())
.toUri();
orderParams.setOutOrderNo(null);
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
orderParams.setAppid(v3.getAppId());
orderParams.setOutOrderNo(null);
return Post(uri, orderParams);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 修改订单金额API
* <p>
* 完结订单总金额与实际金额不符时,可通过该接口修改订单金额。
* 例如充电宝场景由于机器计费问题导致商户完结订单时扣除用户99元用户客诉成功后商户需要按照实际的消费金额如10元扣费当服务订单支付状态处于“待支付”时商户可使用此能力修改订单金额。
* <p>
* 注意:
* • 若此笔订单已收款成功,商户直接使用退款能力,将差价退回用户即可。
* <p>
* • 修改次数>=1第n次修改后金额 <第n-1次修改后金额
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> modifyServiceOrder(ModifyServiceOrderParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_MODIFY_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(orderParams.getOutOrderNo())
.toUri();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
orderParams.setAppid(v3.getAppId());
orderParams.setOutOrderNo(null);
return Post(uri, orderParams);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 完结支付分订单API
* <p>
* 前置条件:服务订单状态为“进行中”且订单状态说明需为[USER_CONFIRM:用户确认]
* <p>
* 完结微信支付分订单。用户使用服务完成后,商户可通过此接口完结订单。
* <p>
* 特别说明:
* • 完结接口调用成功后,微信支付将自动发起免密代扣。 若扣款失败,微信支付将自动再次发起免密代扣(按照一定频次),直到扣成功为止。
*
* @param params the params
* @return wechat response entity
*/
public WechatResponseEntity<ObjectNode> completeServiceOrder(CompleteServiceOrderParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_COMPLETE_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(orderParams.getOutOrderNo())
.toUri();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
orderParams.setAppid(v3.getAppId());
orderParams.setOutOrderNo(null);
return Post(uri, orderParams);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 商户发起催收扣款API
* <p>
* 前置条件:服务订单支付状态处于“待支付”状态
* <p>
* 当微信支付分订单支付状态处于“待支付”时,商户可使用该接口向用户发起收款。
* <p>
* 注意:
* • 此能力不影响微信支付分代商户向用户发起收款的策略。
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> payServiceOrder(PayServiceOrderParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_PAY_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(orderParams.getOutOrderNo())
.toUri();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
orderParams.setAppid(v3.getAppId());
orderParams.setOutOrderNo(null);
return Post(uri, orderParams);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 同步服务订单信息API
* <p>
* 前提条件:同步商户渠道收款成功信息时,即场景类型=“Order_Paid”订单的状态需为[MCH_COMPLETE:商户完结订单]
* <p>
* 由于收款商户进行的某些“线下操作”会导致微信支付侧的订单状态与实际情况不符。例如,用户通过线下付款的方式已经完成支付,而微信支付侧并未支付成功,此时可能导致用户重复支付。因此商户需要通过订单同步接口将订单状态同步给微信支付,修改订单在微信支付系统中的状态。
*
* @param params the params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> syncServiceOrder(SyncServiceOrderParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PAY_SCORE_SYNC_USER_SERVICE_ORDER, params)
.function((wechatPayV3Type, orderParams) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(orderParams.getOutOrderNo())
.toUri();
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
orderParams.setAppid(v3.getAppId());
orderParams.setOutOrderNo(null);
return Post(uri, orderParams);
})
.consumer(wechatResponseEntity::convert)

View File

@@ -0,0 +1,94 @@
package cn.felord.payment.wechat.v3.model.payscore;
import lombok.Data;
import java.util.List;
/**
* 完结支付分订单请求参数
*
* @author felord.cn
* @since 1.0.2.RELEASE
*/
@Data
public class CompleteServiceOrderParams {
/**
* 商户服务订单号,必填
* <p>
* 商户系统内部服务订单号不是交易单号要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。详见[商户订单号]。
*/
private String outOrderNo;
/**
* 与传入的商户号建立了支付绑定关系的appid必填
*/
private String appid;
/**
* 服务ID必填
* <p>
* 该服务ID有本接口对应产品的权限。
*/
private String serviceId;
/**
* 后付费项目,必填
*/
private List<PostPayment> postPayments;
/**
* 后付费商户优惠,选填
*/
private List<PostDiscount> postDiscounts;
/**
* 总金额,单位分,必填
* <p>
* 不能超过完结订单时候的总金额,只能为整数,详见 <a target= "_blank" href= "https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=4_2">支付金额</a>。此参数需满足:总金额 =修改后付费项目1…+修改后完结付费项目n-(修改 后付费商户优惠项目1…+修改后付费商户优惠项目n
*/
private Long totalAmount;
/**
* 服务时间段,条件选填
* <p>
* 服务时间范围,创建订单未填写服务结束时间,则完结的时候,服务结束时间必填
* 如果传入,用户侧则显示此参数。
*/
private TimeRange timeRange;
/**
* 服务位置,选填
*/
private CompleteLocation location;
/**
* 微信支付服务分账标记,选填
* <p>
* 完结订单分账接口标记。分账开通流程,详见 <a target = "_blank" href = "https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=26_2">分账</a>
* false不分账默认false
* true分账。
*/
private Boolean profitSharing = Boolean.TRUE;
/**
* 订单优惠标记,选填
* <p>
* 代金券或立减金优惠的参数,说明详见代金券或立减金优惠
*/
private String goods_tag;
/**
* 服务位置信息
* <p>
* 如果传入,用户侧则显示此参数。
*
* @author felord.cn
* @since 1.0.2.RELEASE
*/
@Data
public static class CompleteLocation {
/**
* 预计服务结束地点,条件选填。
* <p>
* 结束使用服务的地点不超过50个字符超出报错处理 。 创建订单传入了【服务开始地点】,此项才能填写
* 【建议】
* 1、预计结束地点为空时实际结束地点与开始地点相同不填写
* 2、预计结束地点不为空时实际结束地点与预计结束地点相同不填写
*/
private String endLocation;
}
}

View File

@@ -0,0 +1,53 @@
package cn.felord.payment.wechat.v3.model.payscore;
import lombok.Data;
import java.util.List;
/**
* 修改微信支付分订单金额请求参数.
*
* @author felord.cn
* @since 1.0.2.RELEASE
*/
@Data
public class ModifyServiceOrderParams {
/**
* 商户服务订单号,必填
* <p>
* 商户系统内部服务订单号不是交易单号要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。详见[商户订单号]。
*/
private String outOrderNo;
/**
* 与传入的商户号建立了支付绑定关系的appid必填
*/
private String appid;
/**
* 服务ID必填
* <p>
* 该服务ID有本接口对应产品的权限。需要与创建订单时保持一致。
*/
private String serviceId;
/**
* 后付费项目,必填
*/
private List<PostPayment> postPayments;
/**
* 后付费商户优惠,选填
*/
private List<PostDiscount> postDiscounts;
/**
* 总金额,单位分,必填
*
* 不能超过完结订单时候的总金额,只能为整数,详见 <a target= "_blank" href= "https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=4_2">支付金额</a>。此参数需满足:总金额 =修改后付费项目1…+修改后完结付费项目n-(修改 后付费商户优惠项目1…+修改后付费商户优惠项目n
*/
private Long totalAmount;
/**
* 取消原因最长50个字符必填
*/
private String reason;
}

View File

@@ -0,0 +1,30 @@
package cn.felord.payment.wechat.v3.model.payscore;
import lombok.Data;
/**
* 商户发起催收扣款请求参数.
*
* @author felord.cn
* @since 1.0.2.RELEASE
*/
@Data
public class PayServiceOrderParams {
/**
* 商户服务订单号,必填
* <p>
* 商户系统内部服务订单号不是交易单号要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。详见[商户订单号]。
*/
private String outOrderNo;
/**
* 与传入的商户号建立了支付绑定关系的appid必填
*/
private String appid;
/**
* 服务ID必填
* <p>
* 该服务ID有本接口对应产品的权限。
*/
private String serviceId;
}

View File

@@ -26,6 +26,11 @@ public class PostDiscount {
* 优惠使用条件说明。{@link PostDiscount#name}若填写,则必须同时填写。
*/
private String description;
/**
* 总金额,单位分,必填
* todo 新增没有此字段,修改必填,感觉不太符合常理
*/
private Long amount;
/**
* 优惠数量,选填。
* <p>

View File

@@ -14,13 +14,13 @@ import lombok.Data;
@Data
public class PostPayment {
/**
* 付费项目名称,选填。
* 付费项目名称,选填。 修改订单必填
* <p>
* 相同订单号下不能出现相同的付费项目名称当参数长度超过20个字符时报错处理。
*/
private String name;
/**
* 金额,条件选填。
* 金额,条件选填。修改订单必填
* <p>
* 此付费项目总金额大于等于0单位为分等于0时代表不需要扣费只能为整数详见支付金额。如果填写了“付费项目名称”则amount或description必须填写其一或都填。
*/

View File

@@ -22,7 +22,7 @@ public class RiskFund {
* 1、数字必须>0单位分
* 2、风险金额≤每个服务ID的风险金额上限。
* 3、当商户优惠字段为空时付费项目总金额≤服务ID的风险金额上限 未填写金额的付费项目视为该付费项目金额为0
* 4、完结金额可大于、小于或等于风险金额。详细可见QA <a src = "https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter11_2.shtml#menu1">关于订单风险金额问题</a>
* 4、完结金额可大于、小于或等于风险金额。详细可见QA <a target= "_blank" href= "https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter11_2.shtml#menu1">关于订单风险金额问题</a>
*/
private Long amount;
/**

View File

@@ -0,0 +1,64 @@
package cn.felord.payment.wechat.v3.model.payscore;
import lombok.Data;
/**
* 同步服务订单信息请求参数.
*
* @author felord.cn
* @since 1.0.2.RELEASE
*/
@Data
public class SyncServiceOrderParams {
/**
* 商户服务订单号,必填
* <p>
* 商户系统内部服务订单号不是交易单号要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。详见[商户订单号]。
*/
private String outOrderNo;
/**
* 与传入的商户号建立了支付绑定关系的appid必填
*/
private String appid;
/**
* 服务ID必填
* <p>
* 该服务ID有本接口对应产品的权限。与订单要保持一致。
*/
private String serviceId;
/**
* 场景类型必填场景类型为“Order_Paid”字符串表示“订单收款成功” 。
*/
private String type = "Order_Paid";
/**
* 内容信息详情场景类型为Order_Paid时为必填项。
*/
private SyncDetail detail;
/**
* 内容信息详情
*/
@Data
public static class SyncDetail{
/**
* 收款成功时间
* <p>
* 支付成功时间支持两种格式yyyyMMddHHmmss和yyyyMMdd
* ● 传入20091225091010表示2009年12月25日9点10分10秒。
* ● 传入20091225默认认为时间为2009年12月25日0点0分0秒。
* 用户通过其他方式付款成功的实际时间需满足条件:服务开始时间<调用商户完结订单接口的时间<用户通过其他方式付款成功的实际时间≤商户调用支付分订单同步接口的时间。
* 【服务开始时间】
* 1、当完结订单有填写【实际服务开始时间】时【服务开始时间】=完结订单【实际服务开始时间】。
* 2、当完结订单未填写【实际服务开始时间】时【服务开始时间】=创建订单【服务开始时间】
* 场景类型为Order_Paid时必填。
* 支持两种格式yyyyMMddHHmmss和yyyyMMdd
* ● 传入20091225091010表示2009年12月25日9点10分10秒。
* ● 传入20091225表示时间为2009年12月25日23点59分59秒。
* 注意:微信支付分会根据此时间更新用户侧的守约记录、负面记录信息;因此请务必如实填写用户实际付款成功时间,以免造成不必要的客诉。
*/
private String paidTime;
}
}

View File

@@ -13,7 +13,7 @@ import java.util.List;
* @since 1.0.2.RELEASE
*/
@Data
public class UserPayScoreOrderParams {
public class UserServiceOrderParams {
/**
* 商户服务订单号必填