68 Commits

Author SHA1 Message Date
felord
554a93d41a Merge branch '1.0.11.SNAPSHOT' into release 2021-06-05 14:37:44 +08:00
felord
c1d2cfce13 docs: Update pathParams 2021-06-05 14:37:01 +08:00
felord.cn
ee8d7d0d92 Merge pull request #36 from NotFound403/1.0.11.SNAPSHOT
1.0.11.RELEASE
2021-06-05 14:04:36 +08:00
felord
60fe80e615 refactor: Update pathParams 2021-06-05 13:28:37 +08:00
felord
53a34e6a71 feat: (微信支付更新)基础支付《申请退款API》、《查询退款API》新增字段 from(退款出资账户及金额) 2021-06-01 16:00:02 +08:00
felord
398df66ec5 feat: 服务商V3分账 2021-06-01 15:37:21 +08:00
felord
e387e91e38 feat: 直连商户V3分账 2021-06-01 15:37:20 +08:00
felord
10a91c51be Merge branch 'release' into 1.0.11.SNAPSHOT 2021-05-28 17:29:40 +08:00
felord.cn
453844bd9a Merge pull request #33 from AmazingDM/release
fix :批量转账到零钱:微信明细单号查询明细单API,商家明细单号查询明细单API 参数错误
2021-05-28 17:27:53 +08:00
felord
3871e05ea7 feat: 完善批量转账到零钱API
- 转账明细电子回单受理API
- 查询转账明细电子回单受理结果API
- 查询账户实时余额API
- 查询账户日终余额API
- 商户银行来账查询API
2021-05-28 17:01:04 +08:00
felord
16440d01cf refactor: 签名逻辑修改,全部使用换行符 2021-05-28 17:00:52 +08:00
felord
91dc8e8f8a refactor: 现在下载交易账单和资金账单更加方便了
- 直接返回资源流,可以直接返回给前端,会自动下载成txt或者gzip文件
2021-05-27 18:37:16 +08:00
felord
8cd3f63d68 refactor: Style 2021-05-26 10:19:53 +08:00
felord
a46590d0a9 Merge remote-tracking branch 'origin/1.0.11.SNAPSHOT' into 1.0.11.SNAPSHOT 2021-05-25 12:38:42 +08:00
felord
0c230547a9 refactor: 交易账单和资金账单现在能够正常的下载文件了 2021-05-25 12:38:13 +08:00
felord.cn
591eec7f65 Merge remote-tracking branch 'origin/1.0.11.SNAPSHOT' into 1.0.11.SNAPSHOT
# Conflicts:
#	payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/SignatureProvider.java
2021-05-16 23:45:27 +08:00
felord.cn
ed190cc875 refactor: 增加解密参数的判断 2021-05-16 23:44:08 +08:00
AmazingDM
82941dffcb fix :批量转账到零钱:微信明细单号查询明细单API,商家明细单号查询明细单API 参数错误 2021-05-14 18:27:49 +08:00
felord
9d01a0fa3a refactor: 分账标记默认改为 不分账 2021-05-13 16:20:47 +08:00
felord
78cbce016d refactor: 优化证书刷新
https://gitee.com/felord/payment-spring-boot/issues/I3NGSB

Closes I3NGSB
2021-05-10 10:56:43 +08:00
felord.cn
7470787441 feat: 日常维护
- 支付宝增加证书路径的选择方式
- 微信支付 《支付通知API》新增优惠功能(promotion_detail)字段
2021-05-09 22:13:10 +08:00
felord.cn
65cfbaf3b2 Merge pull request #29 from NotFound403/dependabot/maven/org.bouncycastle-bcprov-jdk15to18-1.67
build(deps): bump bcprov-jdk15to18 from 1.66 to 1.67
2021-05-07 11:54:52 +08:00
dependabot[bot]
3be70e7d61 build(deps): bump bcprov-jdk15to18 from 1.66 to 1.67
Bumps [bcprov-jdk15to18](https://github.com/bcgit/bc-java) from 1.66 to 1.67.
- [Release notes](https://github.com/bcgit/bc-java/releases)
- [Changelog](https://github.com/bcgit/bc-java/blob/master/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 20:22:34 +00:00
felord
5646bab7e6 docs: Update changelog 2021-04-08 14:28:50 +08:00
felord.cn
40ffb286d3 Merge pull request #27 from NotFound403/1.0.10.SNAPSHOT
1.0.10.RELEASE
2021-04-08 14:24:37 +08:00
felord.cn
5047be7fd0 Merge branch 'release' into 1.0.10.SNAPSHOT 2021-04-08 14:24:22 +08:00
felord
285356600a docs: Update changelog 2021-04-08 10:21:50 +08:00
felord
277a1223c5 fix: 微信native支付v3通知回调successTime字段无时区信息
Closes I3ED43
2021-03-31 14:53:38 +08:00
felord
6086dd5101 style: copy right 2021-03-31 13:36:49 +08:00
felord
c9ed68fa80 Merge remote-tracking branch 'origin/1.0.10.SNAPSHOT' into 1.0.10.SNAPSHOT 2021-03-31 10:36:47 +08:00
felord.cn
e9307bf490 Merge pull request #25 from zacone/1.0.10.SNAPSHOT
feat: 微信支付V2分账接口实现
2021-03-31 10:34:53 +08:00
wangzecheng
b4f8aa48b1 feat: 微信支付V2分账接口实现 2021-03-31 10:17:05 +08:00
felord
0bfa439c4e docs: 1.0.9 changelog 2021-03-26 09:33:46 +08:00
felord
ee5522e980 factor: 商家券修改API的请求方法改为Patch 2021-03-26 09:30:27 +08:00
felord
53e6016c4c factor: 优化证书store加载机制 2021-03-25 15:01:19 +08:00
felord
538e11dabe fix: 修复多证书初始化的问题 2021-03-25 13:50:08 +08:00
felord
ae618da2d3 fix: 支付宝maven打包读取不了证书
Closes #24
2021-03-25 11:00:46 +08:00
felord
9dfdb2b295 fix: 修改读取文件的方式,避免因为文件系统的不同导致配置加载失败 2021-03-24 17:35:29 +08:00
felord
d95f38fb0a Merge remote-tracking branch 'origin/release' into release 2021-03-24 14:40:19 +08:00
felord
00dc8da556 Merge remote-tracking branch 'origin/release' into release 2021-03-24 11:15:39 +08:00
felord
f1c9a873b5 Merge remote-tracking branch 'origin/release' into release 2021-03-24 10:04:02 +08:00
felord.cn
65b3035c55 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:56:17 +08:00
felord.cn
d1f3de7906 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:56:00 +08:00
felord.cn
ef172471c5 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:43:14 +08:00
felord.cn
f62e8eb921 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:42:59 +08:00
felord.cn
291e73b34c Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:39:12 +08:00
felord.cn
39ec2ef4f5 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:38:54 +08:00
felord.cn
71d0342eee Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:28:46 +08:00
felord.cn
7ed3582c12 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:28:31 +08:00
felord.cn
adfb37a71a Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:21:58 +08:00
felord.cn
7ffcc47255 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:21:43 +08:00
felord.cn
237851c7c5 Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:08:54 +08:00
felord.cn
8914d7272d Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:08:39 +08:00
felord.cn
cea3395aed Merge remote-tracking branch 'origin/release' into release 2021-03-23 23:04:09 +08:00
felord.cn
77d80588fb ci: github action support 2021-03-23 23:03:54 +08:00
felord.cn
f0c0b64a93 ci: github action support 2021-03-23 22:52:13 +08:00
felord.cn
02ce523cde ci: github action support 2021-03-23 22:47:40 +08:00
felord.cn
01b9fc3aa0 ci: github action support 2021-03-23 22:03:07 +08:00
felord.cn
73b19fcc0a ci: github action support 2021-03-23 21:58:55 +08:00
felord.cn
aae5096349 fix: 修复一些已知Bug 2021-03-23 21:19:01 +08:00
felord.cn
ace5a900d2 ci: github action support 2021-03-23 21:09:46 +08:00
felord
24f748a197 fix: rollback 2021-03-23 16:50:42 +08:00
felord.cn
9880a1e561 ci: github action support 2021-03-23 16:49:18 +08:00
felord.cn
0f8aa8f2d0 ci: github action support 2021-03-23 16:49:18 +08:00
felord.cn
7eac8dcdc4 fix: remove unused files 2021-03-23 16:49:18 +08:00
felord.cn
7c95c8ec7c feat: upgrade to 1.0.9 2021-03-23 16:49:18 +08:00
felord.cn
34b65b6417 fix: 商家券回调反序列化异常
Closes #21
2021-03-23 16:49:17 +08:00
felord.cn
03fba59c49 ci: github action support 2021-03-23 01:09:40 +08:00
76 changed files with 3488 additions and 253 deletions

View File

@@ -1,33 +1,35 @@
# 相当于脚本用途的一个声明
name: Maven Central Repo Deployment
# release 的时候触发
# 触发脚本的事件 这里为发布release之后触发
on:
release:
types: [released]
# 定义一个发行任务
jobs:
publish:
# 任务运行的环境
runs-on: ubuntu-latest
# 任务的步骤
steps:
- uses: actions/checkout@v2
# 1. 声明 checkout 仓库代码到工作区
- name: Checkout Git Repo
uses: actions/checkout@v2
# 2. 安装Java 环境 这里会用到的参数就是 Git Action secrets中配置的
# 取值要在key前面加 secrets.
- name: Set up Maven Central Repo
uses: actions/setup-java@v1
with:
java-version: 1.8
server-id: sonatype-nexus-staging
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- id: install-secret-key
name: Install GPG Secret Key
run: |
cat <(echo -e "${{ secrets.GPG_PUB }}") | gpg --batch --import
gpg --list-secret-keys --keyid-format LONG
- id: publish-to-central
name: Publish to Maven Central Repo
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USER }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
run: |
mvn \
--no-transfer-progress \
--batch-mode \
-Dgpg.passphrase=${{ secrets.GPG_PASSWORD }} \
clean deploy
server-username: ${{ secrets.OSSRH_USER }}
server-password: ${{ secrets.OSSRH_PASSWORD }}
gpg-passphrase: ${{ secrets.GPG_PASSWORD }}
# 3. 发布到Maven中央仓库
- name: Publish to Maven Central Repo
# 这里用到了其他人写的action脚本详细可以去看他的文档。
uses: samuelmeuli/action-maven-publish@v1
with:
gpg_private_key: ${{ secrets.GPG_SECRET }}
gpg_passphrase: ${{ secrets.GPG_PASSWORD }}
nexus_username: ${{ secrets.OSSRH_USER }}
nexus_password: ${{ secrets.OSSRH_PASSWORD }}

View File

@@ -11,7 +11,7 @@
<dependency>
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot-starter</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
</dependency>
```
@@ -26,6 +26,7 @@
- 实现微信支付V3 商家券
- 实现微信支付V3 批量转账到零钱
更多参考[changelog](https://notfound403.github.io/payment-spring-boot/#/changelog)
## 核心API结构
![](https://asset.felord.cn/blog/20210112211759.png)

View File

@@ -35,7 +35,7 @@
<dependency>
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot-starter</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
</dependency>
```
## 采用技术

View File

@@ -1,3 +1,20 @@
## 1.0.10.RELEASE
- 微信支付
- feat: 微信支付V2分账接口实现感谢**zacone**同学贡献的PR
- factor: 优化证书加载方式
- factor: 商家券修改API的请求方式变更为`Patch`
- fix: 修复微信支付V3中native支付通知回调`successTime`字段无时区信息的问题([#I3ED43](https://gitee.com/felord/payment-spring-boot/issues/I3ED43))
- 支付宝
- fix: 修复支付宝Maven打包无法读取证书的问题([#24](https://github.com/NotFound403/payment-spring-boot/issues/24))
## 1.0.9.RELEASE
- 微信支付
- refactor: `WechatPartnerPayApi` 加入**Spring IOC**
- fix: 支付分支付成功回调反序列化异常 ([#21](https://github.com/NotFound403/payment-spring-boot/issues/21))
- fix: 修复枚举空指针问题 ([#22](https://github.com/NotFound403/payment-spring-boot/issues/22))
## 1.0.8.RELEASE
- 微信支付

View File

@@ -4,7 +4,7 @@
<dependency>
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot-starter</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
</dependency>
```
> 基于 **Spring Boot 2.x**
@@ -92,7 +92,7 @@ wechat:
### 支付宝
在Spring Boot项目中的`application.yaml`中配置`ali.pay.v1`相关参数。
在Spring Boot项目中的`application.yaml`中配置`ali.pay.v1`相关参数。证书细节参见【日常踩坑】
```yaml
ali:
pay:

View File

@@ -5,11 +5,11 @@
<parent>
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
</parent>
<artifactId>payment-spring-boot-autoconfigure</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>

View File

@@ -19,11 +19,12 @@
package cn.felord.payment.alipay;
import cn.felord.payment.PayException;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.CertAlipayRequest;
import com.alipay.api.DefaultAlipayClient;
import cn.felord.payment.PayException;
import com.alipay.api.internal.util.file.IOUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -32,7 +33,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Function;
/**
* @author felord.cn
@@ -53,39 +56,37 @@ public class AliPayConfiguration {
CertAlipayRequest certAlipayRequest = new CertAlipayRequest();
propertyMapper.from(v1::getServerUrl).to(certAlipayRequest::setServerUrl);
propertyMapper.from(v1::getAppId).to(certAlipayRequest::setAppId);
propertyMapper.from(v1::getAppPrivateKeyPath).as(this::appRSAPrivateKey).to(certAlipayRequest::setPrivateKey);
propertyMapper.from(v1::getAppPrivateKeyPath)
.as(this::loadFile)
.to(certAlipayRequest::setPrivateKey);
propertyMapper.from(v1::getFormat).to(certAlipayRequest::setFormat);
propertyMapper.from(v1::getCharset).to(certAlipayRequest::setCharset);
propertyMapper.from(v1::getSignType).to(certAlipayRequest::setSignType);
propertyMapper.from(v1::getAppCertPublicKeyPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setCertPath);
propertyMapper.from(v1::getAlipayPublicCertPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setAlipayPublicCertPath);
propertyMapper.from(v1::getAlipayRootCertPath).as(this::getFileAbsolutePath).to(certAlipayRequest::setRootCertPath);
Function<String,String> certStrategyFunc = v1.isClasspathUsed()?this::loadFile:s -> s;
propertyMapper.from(v1::getAppCertPublicKeyPath)
.as(certStrategyFunc)
.to(certAlipayRequest::setCertContent);
propertyMapper.from(v1::getAlipayPublicCertPath)
.as(certStrategyFunc)
.to(certAlipayRequest::setAlipayPublicCertContent);
propertyMapper.from(v1::getAlipayRootCertPath)
.as(certStrategyFunc)
.to(certAlipayRequest::setRootCertContent);
return new DefaultAlipayClient(certAlipayRequest);
}
private String getFileAbsolutePath(String classPath) {
try {
return new ClassPathResource(classPath).getFile().getAbsolutePath();
} catch (IOException e) {
log.error("ali pay cert path is not exist ,{}", e.getMessage());
throw new PayException("ali pay cert path is not exist");
}
}
private String appRSAPrivateKey(String classPath) {
private String loadFile(String classPath) {
ClassPathResource resource = new ClassPathResource(classPath);
try {
FileReader in = new FileReader(resource.getFile());
try(BufferedReader bufferedReader = new BufferedReader(in)){
return bufferedReader.readLine();
}
try (InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream())) {
return IOUtils.toString(inputStreamReader);
} catch (IOException e) {
log.error("ali pay app private key is required ,{}", e.getMessage());
throw new PayException("ali pay app private key is required");
}
}
}

View File

@@ -70,6 +70,10 @@ public class AliPayProperties {
* charset default utf-8
*/
private String charset = "utf-8";
/**
* use classpath or not ,default true
*/
private boolean classpathUsed = true;
/**
* alipay public cert path
*/

View File

@@ -60,7 +60,7 @@ public class WechatPayConfiguration {
WechatPayProperties.V3 v3 = v3Map.get(tenantId);
String certPath = v3.getCertPath();
String mchId = v3.getMchId();
WechatMetaBean wechatMetaBean = keyPairFactory.createPKCS12(certPath, CERT_ALIAS, mchId);
WechatMetaBean wechatMetaBean = keyPairFactory.initWechatMetaBean(certPath, CERT_ALIAS, mchId);
wechatMetaBean.setV3(v3);
wechatMetaBean.setTenantId(tenantId);
container.addWechatMeta(tenantId, wechatMetaBean);

View File

@@ -29,64 +29,42 @@ public enum CouponBgColor {
/**
* Color 010 coupon bg color.
*/
COLOR010("#63B359"),
Color010,
/**
* Color 020 coupon bg color.
*/
COLOR020("#2C9F67"),
Color020,
/**
* Color 030 coupon bg color.
*/
COLOR030("#509FC9"),
Color030,
/**
* Color 040 coupon bg color.
*/
COLOR040("#5885CF"),
Color040,
/**
* Color 050 coupon bg color.
*/
COLOR050("#9062C0"),
Color050,
/**
* Color 060 coupon bg color.
*/
COLOR060("#D09A45"),
Color060,
/**
* Color 070 coupon bg color.
*/
COLOR070("#E4B138"),
Color070,
/**
* Color 080 coupon bg color.
*/
COLOR080("#EE903C"),
Color080,
/**
* Color 090 coupon bg color.
*/
COLOR090("#DD6549"),
Color090,
/**
* Color 100 coupon bg color.
*/
COLOR100("#CC463D");
Color100
/**
* The Color.
*/
private final String color;
/**
* Instantiates a new Coupon bg color.
*
* @param color the color
*/
CouponBgColor(String color) {
this.color = color;
}
/**
* Color string.
*
* @return the string
*/
public String color() {
return this.color;
}
}

View File

@@ -0,0 +1,23 @@
package cn.felord.payment.wechat.enumeration;
/**
* 分账接收方类型
*
* @author n1
* @since 2021/5/31 17:47
*/
public enum ReceiverType {
/**
* 商户号
*/
MERCHANT_ID,
/**
* 个人openid由父商户APPID转换得到
*/
PERSONAL_OPENID,
/**
* 个人sub_openid由子商户APPID转换得到服务商模式
*/
PERSONAL_SUB_OPENID
}

View File

@@ -0,0 +1,22 @@
package cn.felord.payment.wechat.enumeration;
/**
* 批量转账到零钱 - 电子回单受理类型
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
public enum TransferAcceptType {
/**
* 批量转账明细电子回单
*/
BATCH_TRANSFER,
/**
* 企业付款至零钱电子回单
*/
TRANSFER_TO_POCKET,
/**
* 企业付款至银行卡电子回单
*/
TRANSFER_TO_BANK
}

View File

@@ -47,13 +47,13 @@ public enum WechatPayV3Type {
*
* @since 1.0.3.RELEASE
*/
TRADEBILL(HttpMethod.GET, "%s/v3/bill/tradebill"),
TRADE_BILL(HttpMethod.GET, "%s/v3/bill/tradebill"),
/**
* 申请资金账单API.
*
* @since 1.0.3.RELEASE
*/
FUNDFLOWBILL(HttpMethod.GET, "%s/v3/bill/fundflowbill"),
FUND_FLOW_BILL(HttpMethod.GET, "%s/v3/bill/fundflowbill"),
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -431,7 +431,7 @@ public enum WechatPayV3Type {
*
* @since 1.0.4.RELEASES
*/
MARKETING_BUSI_FAVOR_UPDATE(HttpMethod.POST, "%s/v3/marketing/busifavor/stocks/{stock_id}"),
MARKETING_BUSI_FAVOR_UPDATE(HttpMethod.PATCH, "%s/v3/marketing/busifavor/stocks/{stock_id}"),
/**
* 申请退券API.
*
@@ -487,6 +487,37 @@ public enum WechatPayV3Type {
* @since 1.0.6.RELEASES
*/
BATCH_TRANSFER_DOWNLOAD_BILL(HttpMethod.GET, "%s/v3/transfer/bill-receipt/{out_batch_no}"),
/**
* 转账明细电子回单受理API.
*
* @since 1.0.11.RELEASES
*/
BATCH_TRANSFER_ELECTRONIC(HttpMethod.POST, "%s/v3/transfer-detail/electronic-receipts"),
/**
* 查询转账明细电子回单受理结果API.
* 请求方式同{@link WechatPayV3Type#BATCH_TRANSFER_ELECTRONIC}不同
*
* @since 1.0.11.RELEASES
*/
BATCH_TRANSFER_ELECTRONIC_DETAIL(HttpMethod.GET, "%s/v3/transfer-detail/electronic-receipts"),
/**
* 查询账户实时余额API
*
* @since 1.0.11.RELEASES
*/
BATCH_TRANSFER_FUND_BALANCE(HttpMethod.GET, "%s/v3/merchant/fund/balance/{account_type}"),
/**
* 查询账户日终余额API
*
* @since 1.0.11.RELEASES
*/
BATCH_TRANSFER_FUND_DAY_BALANCE(HttpMethod.GET, "%s/v3/merchant/fund/dayendbalance/{account_type}"),
/**
* 商户银行来账查询API
*
* @since 1.0.11.RELEASES
*/
BATCH_TRANSFER_FUND_INCOME_RECORDS(HttpMethod.GET, "%s/v3/merchantfund/merchant/income-records"),
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -536,7 +567,62 @@ public enum WechatPayV3Type {
* @since 1.0.0.RELEASE
*/
TRANSACTION_OUT_TRADE_NO_PARTNER(HttpMethod.GET, "%s/v3/pay/partner/transactions/out-trade-no/{out_trade_no}"),
;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* 请求分账API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_ORDERS(HttpMethod.POST, "%s/v3/profitsharing/orders"),
/**
* 查询分账结果API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_ORDERS_RESULT(HttpMethod.GET, "%s/v3/profitsharing/orders/{out_order_no}"),
/**
* 请求分账回退API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_RETURN_ORDERS(HttpMethod.POST, "%s/v3/profitsharing/return-orders"),
/**
* 查询分账回退结果API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_RETURN_ORDERS_RESULT(HttpMethod.GET, "%s/v3/profitsharing/return-orders"),
/**
* 解冻剩余资金API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_ORDERS_UNFREEZE(HttpMethod.POST, "%s/v3/profitsharing/orders/unfreeze"),
/**
* 查询剩余待分金额API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_AMOUNTS(HttpMethod.GET, "%s/v3/profitsharing/transactions/{transaction_id}/amounts"),
/**
* 服务商专用-查询最大分账比例API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_MCH_CONFIG(HttpMethod.GET, "%s/v3/profitsharing/merchant-configs/{sub_mchid}"),
/**
* 添加分账接收方API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_RECEIVERS_ADD(HttpMethod.POST, "%s/v3/profitsharing/receivers/add"),
/**
* 删除分账接收方API.
*
* @since 1.0.11.RELEASE
*/
PROFITSHARING_RECEIVERS_DELETE(HttpMethod.POST, "%s/v3/profitsharing/receivers/add");
/**
* The Pattern.
*

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2;
import cn.felord.payment.wechat.WechatPayProperties;
import cn.felord.payment.wechat.v2.model.BaseModel;
import cn.felord.payment.wechat.v2.model.allocation.*;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import java.util.List;
/**
* 微信支付分账
* <p>
*
* @author felord.cn
* @since 1.0.10.RELEASE
*/
@Slf4j
public class WechatAllocationApi {
/**
* The constant MAPPER.
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
.registerModule(new JavaTimeModule());
}
private final WechatV2Client wechatV2Client;
/**
* Instantiates a new Wechat allocation api.
*
* @param wechatV2Client the wechat v 2 client
*/
public WechatAllocationApi(WechatV2Client wechatV2Client) {
this.wechatV2Client = wechatV2Client;
}
/**
* 请求单次分账
*
* @param profitSharingModel the profit sharing model
* @return json node
*/
@SneakyThrows
public JsonNode profitSharing(ProfitSharingModel profitSharingModel) {
ProfitSharingSModel model = new ProfitSharingSModel();
List<Receiver> receivers = profitSharingModel.getReceivers();
model.setReceivers(MAPPER.writeValueAsString(receivers));
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
model.setAppid(v3.getAppId());
model.setMchId(v3.getMchId());
model.setTransactionId(profitSharingModel.getTransactionId());
model.setOutOrderNo(profitSharingModel.getOutOrderNo());
model.certPath(v3.getCertPath());
model.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(model,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/secapi/pay/profitsharing");
}
/**
* 请求单次分账
*
* @param multiProfitSharingModel the multi profit sharing model
* @return json node
*/
@SneakyThrows
public JsonNode multiProfitSharing(MultiProfitSharingModel multiProfitSharingModel) {
MultiProfitSharingSModel multiProfitSharingSModel = new MultiProfitSharingSModel();
List<Receiver> receivers = multiProfitSharingModel.getReceivers();
multiProfitSharingSModel.setReceivers(MAPPER.writeValueAsString(receivers));
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
multiProfitSharingSModel.setAppid(v3.getAppId());
multiProfitSharingSModel.setMchId(v3.getMchId());
multiProfitSharingSModel.setTransactionId(multiProfitSharingModel.getTransactionId());
multiProfitSharingSModel.setOutOrderNo(multiProfitSharingModel.getOutOrderNo());
multiProfitSharingSModel.certPath(v3.getCertPath());
multiProfitSharingSModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(multiProfitSharingSModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing");
}
/**
* 查询分账结果
*
* @param profitSharingQueryModel the profit sharing query model
* @return json node
*/
public JsonNode profitSharingQuery(ProfitSharingQueryModel profitSharingQueryModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingQueryModel.setMchId(v3.getMchId());
profitSharingQueryModel.certPath(v3.getCertPath());
profitSharingQueryModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingQueryModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/pay/profitsharingquery");
}
/**
* 添加分账接收方
*
* @param profitSharingAddReceiverModel the profit sharing add receiver model
* @return json node
*/
@SneakyThrows
public JsonNode profitSharingAddReceiver(ProfitSharingAddReceiverModel profitSharingAddReceiverModel) {
ProfitSharingAddReceiverSModel profitSharingAddReceiverSModel = new ProfitSharingAddReceiverSModel();
ProfitSharingAddReceiverModel.Receiver receiver = profitSharingAddReceiverModel.getReceiver();
profitSharingAddReceiverSModel.setReceiver(MAPPER.writeValueAsString(receiver));
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingAddReceiverSModel.setAppid(v3.getAppId());
profitSharingAddReceiverSModel.setMchId(v3.getMchId());
profitSharingAddReceiverSModel.certPath(v3.getCertPath());
profitSharingAddReceiverSModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingAddReceiverSModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver");
}
/**
* 删除分账接收方
*
* @param profitSharingRemoveReceiverModel the profit sharing remove receiver model
* @return json node
*/
@SneakyThrows
public JsonNode profitSharingRemoveReceiver(ProfitSharingRemoveReceiverModel profitSharingRemoveReceiverModel) {
ProfitSharingRemoveReceiverSModel profitSharingRemoveReceiverSModel = new ProfitSharingRemoveReceiverSModel();
ProfitSharingRemoveReceiverModel.Receiver receiver = profitSharingRemoveReceiverModel.getReceiver();
profitSharingRemoveReceiverSModel.setReceiver(MAPPER.writeValueAsString(receiver));
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingRemoveReceiverSModel.setAppid(v3.getAppId());
profitSharingRemoveReceiverSModel.setMchId(v3.getMchId());
profitSharingRemoveReceiverSModel.certPath(v3.getCertPath());
profitSharingRemoveReceiverSModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingRemoveReceiverSModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver");
}
/**
* 完结分账
*
* @param profitSharingFinishModel the profit sharing finish model
* @return json node
*/
public JsonNode profitSharingFinish(ProfitSharingFinishModel profitSharingFinishModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingFinishModel.setAppid(v3.getAppId());
profitSharingFinishModel.setMchId(v3.getMchId());
profitSharingFinishModel.certPath(v3.getCertPath());
profitSharingFinishModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingFinishModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish");
}
/**
* 查询订单待分账金额
*
* @param profitSharingOrderAmountQueryModel the profit sharing order amount query model
* @return json node
*/
public JsonNode profitSharingOrderAmountQuery(ProfitSharingOrderAmountQueryModel profitSharingOrderAmountQueryModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingOrderAmountQueryModel.setMchId(v3.getMchId());
profitSharingOrderAmountQueryModel.certPath(v3.getCertPath());
profitSharingOrderAmountQueryModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingOrderAmountQueryModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/pay/profitsharingorderamountquery");
}
/**
* 分账回退
*
* @param profitSharingReturnModel the profit sharing return model
* @return json node
*/
public JsonNode profitSharingReturn(ProfitSharingReturnModel profitSharingReturnModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingReturnModel.setAppid(v3.getAppId());
profitSharingReturnModel.setMchId(v3.getMchId());
profitSharingReturnModel.certPath(v3.getCertPath());
profitSharingReturnModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingReturnModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/secapi/pay/profitsharingreturn");
}
/**
* 回退结果查询
*
* @param profitSharingReturnQueryModel the profit sharing return query model
* @return json node
*/
public JsonNode profitSharingReturnQuery(ProfitSharingReturnQueryModel profitSharingReturnQueryModel) {
WechatPayProperties.V3 v3 = wechatV2Client.getWechatMetaBean().getV3();
profitSharingReturnQueryModel.setAppid(v3.getAppId());
profitSharingReturnQueryModel.setMchId(v3.getMchId());
profitSharingReturnQueryModel.certPath(v3.getCertPath());
profitSharingReturnQueryModel.signType(BaseModel.HMAC_SHA256);
return wechatV2Client.wechatPayRequest(profitSharingReturnQueryModel,
HttpMethod.POST,
"https://api.mch.weixin.qq.com/pay/profitsharingreturnquery");
}
}

View File

@@ -48,6 +48,8 @@ import org.springframework.util.IdGenerator;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
@@ -63,6 +65,7 @@ import java.security.cert.CertificateException;
*/
@Getter
public abstract class BaseModel {
public static final String HMAC_SHA256="HMAC-SHA256";
private static final XmlMapper XML_MAPPER = new XmlMapper();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -84,13 +87,27 @@ public abstract class BaseModel {
private String sign;
@JsonIgnore
private String appSecret;
@JsonIgnore
private String certPath;
@JsonIgnore
private String signType;
public BaseModel appSecret(String appSecret) {
this.appSecret = appSecret;
return this;
}
public BaseModel certPath(String certPath) {
this.certPath = certPath;
return this;
}
public BaseModel signType(String signType) {
this.signType = signType;
return this;
}
/**
* Xml string.
*
@@ -99,7 +116,11 @@ public abstract class BaseModel {
@SneakyThrows
private String xml() {
String link = link(this);
this.sign = this.md5(link);
if (HMAC_SHA256.equals(signType)) {
this.sign = this.hmacSha256(link);
} else {
this.sign = this.md5(link);
}
return XML_MAPPER.writer()
.withRootName("xml")
.writeValueAsString(this);
@@ -120,6 +141,21 @@ public abstract class BaseModel {
return Hex.toHexString(md5Bytes).toUpperCase();
}
/**
* hmacSha256.
*
* @param src the src
* @return the string
*/
@SneakyThrows
private String hmacSha256(String src) {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(appSecret.getBytes(),"HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] bytes = sha256_HMAC.doFinal(src.getBytes(StandardCharsets.UTF_8));
return Hex.toHexString(bytes).toUpperCase();
}
/**
* 按照格式拼接参数以生成签名
*
@@ -130,14 +166,13 @@ public abstract class BaseModel {
@SneakyThrows
private <T> String link(T t) {
Assert.hasText(appSecret, "wechat pay appSecret is required");
return OBJECT_MAPPER
String link = OBJECT_MAPPER
.writer()
.writeValueAsString(t)
.replaceAll("\":\"", "=")
.replaceAll("\",\"", "&")
.replaceAll("\\{\"", "")
.replaceAll("\"}", "")
.concat("&key=").concat(this.appSecret);
.replaceAll("\\\\\"", "\"");
return link.substring(2, link.length() - 2).concat("&key=").concat(this.appSecret);
}
@@ -164,7 +199,7 @@ public abstract class BaseModel {
private RestTemplate getRestTemplateClientAuthentication(String mchId)
throws IOException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
ClassPathResource resource = new ClassPathResource("wechat/apiclient_cert.p12");
ClassPathResource resource = new ClassPathResource(certPath == null ? "wechat/apiclient_cert.p12" : certPath);
char[] pem = mchId.toCharArray();

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Base profit sharing model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BaseProfitSharingModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
private String appid;
private String transactionId;
private String outOrderNo;
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Base profit sharing receiver model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BaseProfitSharingReceiverModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 公众账号ID.
* <p>
* 微信分配的公众账号ID
*/
private String appid;
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
/**
* The type Multi profit sharing model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
public class MultiProfitSharingModel extends ProfitSharingModel {
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
/**
* The type Multi profit sharing s model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
public class MultiProfitSharingSModel extends ProfitSharingSModel {
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing add receiver model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingAddReceiverModel extends BaseProfitSharingReceiverModel {
private Receiver receiver;
/**
* The type Receiver.
*/
@Data
public static class Receiver {
/**
* 分账接收方类型.
* <p>
* MERCHANT_ID商户号mch_id或者sub_mch_id
* PERSONAL_OPENID个人openid
*/
private Type type;
/**
* 分账接收方帐号.
* <p>
* 类型是MERCHANT_ID时是商户号mch_id或者sub_mch_id
* 类型是PERSONAL_OPENID时是个人openid
*/
private String account;
/**
* 分账接收方全称.
* <p>
* 分账接收方类型是MERCHANT_ID时是商户全称必传当商户是小微商户或个体户时是开户人姓名
* 分账接收方类型是PERSONAL_OPENID时是个人姓名选传传则校验
*/
private String name;
/**
* 与分账方的关系类型.
* <p>
* 子商户与接收方的关系。
* 本字段值为枚举:
* SERVICE_PROVIDER服务商
* STORE门店
* STAFF员工
* STORE_OWNER店主
* PARTNER合作伙伴
* HEADQUARTER总部
* BRAND品牌方
* DISTRIBUTOR分销商
* USER用户
* SUPPLIER供应商
* CUSTOM自定义
*/
private RelationType relationType;
/**
* 自定义的分账关系.
* <p>
* 子商户与接收方具体的关系本字段最多10个字。
* 当字段relation_type的值为CUSTOM时本字段必填
* 当字段relation_type的值不为CUSTOM时本字段无需填写
*/
private String customRelation;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing add receiver s model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingAddReceiverSModel extends BaseProfitSharingReceiverModel {
private String receiver;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing finish model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingFinishModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 公众账号ID.
* <p>
* 微信分配的公众账号ID
*/
private String appid;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
/**
* 商户分账单号.
* <p>
* 查询分账结果,输入申请分账时的商户分账单号; 查询分账完结执行的结果,输入发起分账完结时的商户分账单号
*/
private String outOrderNo;
/**
* 分账完结描述.
* <p>
* 分账完结的原因描述
*/
private String description;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 单次分账请求按照传入的分账接收方账号和资金进行分账,同时会将订单剩余的待分账金额解冻给本商户。
* 故操作成功后,订单不能再进行分账,也不能进行分账完结。
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingModel extends BaseProfitSharingModel {
/**
* 分账接收方列表不超过50个json对象不能设置分账方作为分账接受方
*/
private List<Receiver> receivers;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing order amount query model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingOrderAmountQueryModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing query model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingQueryModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
/**
* 商户分账单号.
* <p>
* 查询分账结果,输入申请分账时的商户分账单号; 查询分账完结执行的结果,输入发起分账完结时的商户分账单号
*/
private String outOrderNo;
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing remove receiver model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingRemoveReceiverModel extends BaseProfitSharingReceiverModel {
private Receiver receiver;
/**
* The type Receiver.
*/
@Data
public static class Receiver {
private Type type;
private String account;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing remove receiver s model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingRemoveReceiverSModel extends BaseProfitSharingReceiverModel {
private String receiver;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing return model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingReturnModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 公众账号ID.
* <p>
* 微信分配的公众账号ID
*/
private String appid;
private String orderId;
private String outOrderNo;
private String outReturnNo;
private String returnAccountType;
private String returnAccount;
private String returnAmount;
private String description;
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import cn.felord.payment.wechat.v2.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing return query model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingReturnQueryModel extends BaseModel {
/**
* 商户号.
* <p>
* 微信支付分配的商户号
*/
private String mchId;
/**
* 公众账号ID.
* <p>
* 微信分配的公众账号ID
*/
private String appid;
/**
* 微信分账订单号.
* <p>
* 原发起分账请求时,微信返回的微信分账单号,与商户分账单号一一对应。
* 微信分账单号与商户分账单号二选一填写
*/
private String orderId;
/**
* 商户分账单号.
* <p>
* 原发起分账请求时使用的商户系统内部的分账单号。
* 微信分账单号与商户分账单号二选一填写
*/
private String outOrderNo;
/**
* 商户回退单号.
* <p>
* 调用回退接口提供的商户系统内部的回退单号
*/
private String outReturnNo;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* The type Profit sharing s model.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ProfitSharingSModel extends BaseProfitSharingModel {
/**
* 分账接收方列表不超过50个json对象不能设置分账方作为分账接受方。
*/
private String receivers;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
import lombok.Data;
/**
* The type Receiver.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@Data
public class Receiver {
/**
* 分账接收方类型.
* <p>
* MERCHANT_ID商户号mch_id或者sub_mch_id
* PERSONAL_OPENID个人openid
*/
private Type type;
/**
* 分账接收方帐号.
* <p>
* 类型是MERCHANT_ID时是商户号mch_id或者sub_mch_id
* 类型是PERSONAL_OPENID时是个人openid
*/
private String account;
/**
* 分账金额.
* <p>
* 单位为分,只能为整数,不能超过原订单支付金额及最大分账比例金额
*/
private Integer amount;
/**
* 分账描述.
* <p>
* 分账的原因描述,分账账单中需要体现
*/
private String description;
/**
* 分账个人接收方姓名.
* <p>
* 可选项,在接收方类型为个人的时可选填,若有值,会检查与 name 是否实名匹配,不匹配会拒绝分账请求.
* 1、分账接收方类型是PERSONAL_OPENID时是个人姓名选传传则校验
*/
private String name;
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019-2021 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.felord.payment.wechat.v2.model.allocation;
/**
* The enum Relation type.
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
public enum RelationType {
/**
* Service provider relation type.
*/
SERVICE_PROVIDER,
/**
* Store relation type.
*/
STORE,
/**
* Staff relation type.
*/
STAFF,
/**
* Store owner relation type.
*/
STORE_OWNER,
/**
* Partner relation type.
*/
PARTNER,
/**
* Headquarter relation type.
*/
HEADQUARTER,
/**
* Brand relation type.
*/
BRAND,
/**
* Distributor relation type.
*/
DISTRIBUTOR,
/**
* User relation type.
*/
USER,
/**
* Supplier relation type.
*/
SUPPLIER,
/**
* Custom relation type.
*/
CUSTOM
}

View File

@@ -0,0 +1,20 @@
package cn.felord.payment.wechat.v2.model.allocation;
/**
* The enum Type.
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
public enum Type {
/**
* 商户号mch_id或者sub_mch_id
*/
MERCHANT_ID,
/**
* 个人openid
*/
PERSONAL_OPENID
}

View File

@@ -24,11 +24,13 @@ import cn.felord.payment.wechat.v3.model.FundFlowBillParams;
import cn.felord.payment.wechat.v3.model.TradeBillParams;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -88,10 +90,12 @@ public abstract class AbstractApi {
* @param mapper the mapper
*/
private void applyObjectMapper(ObjectMapper mapper) {
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new JavaTimeModule();
mapper.registerModule(module);
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// empty string error
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.registerModule(new JavaTimeModule());
}
@@ -185,10 +189,11 @@ public abstract class AbstractApi {
return RequestEntity.get(uri).header("Pay-TenantId", tenantId)
.build();
}
/**
* 构建Get请求对象.
*
* @param uri the uri
* @param uri the uri
* @param httpHeaders the http headers
* @return the request entity
*/
@@ -200,12 +205,29 @@ public abstract class AbstractApi {
}
/**
* 对账单内容下载,非流文件。
* 构建Post请求对象.
*
* @param uri the uri
* @param params the params
* @return the request entity
*/
protected RequestEntity<?> Patch(URI uri, Object params) {
try {
return RequestEntity.patch(uri).header("Pay-TenantId", tenantId)
.body(mapper.writeValueAsString(params));
} catch (JsonProcessingException e) {
throw new PayException("wechat app pay json failed");
}
}
/**
* 对账单CSV内容下载非流文件。
*
* @param link the link
* @return 对账单内容 ,有可能为空字符 “”
* @see AbstractApi#billResource(String) AbstractApi#billResource(String)AbstractApi#billResource(String)AbstractApi#billResource(String)
*/
protected String billDownload(String link) {
protected String billCsvDownload(String link) {
return this.client().withType(WechatPayV3Type.FILE_DOWNLOAD, link)
.function((type, downloadUrl) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(downloadUrl)
@@ -216,26 +238,12 @@ public abstract class AbstractApi {
.download();
}
/**
* 对账单下载,流文件。
*
* @param link the link
* @return response entity
*/
protected ResponseEntity<Resource> billResource(String link) {
return this.client().withType(WechatPayV3Type.FILE_DOWNLOAD, link)
.function((type, downloadUrl) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(downloadUrl)
.build()
.toUri();
return Get(uri);
})
.resource();
}
/**
* 申请交易账单API
* <p>
* 返回值直接返回前端会下载tradebill-前缀加上日期的txt文件默认或者gzip文件。
* <p>
* 微信支付按天提供交易账单文件,商户可以通过该接口获取账单文件的下载地址。文件内包含交易相关的金额、时间、营销等信息,供商户核对订单、退款、银行到账等情况。
* <p>
* 注意:
@@ -247,10 +255,13 @@ public abstract class AbstractApi {
* </ul>
*
* @param tradeBillParams tradeBillParams
* @return the response entity
* @since 1.0.3.RELEASE
*/
public final void downloadTradeBill(TradeBillParams tradeBillParams) {
this.client().withType(WechatPayV3Type.TRADEBILL, tradeBillParams)
public ResponseEntity<Resource> downloadTradeBill(TradeBillParams tradeBillParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.TRADE_BILL, tradeBillParams)
.function((wechatPayV3Type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
LocalDate billDate = params.getBillDate();
@@ -273,8 +284,15 @@ public abstract class AbstractApi {
.build().toUri();
return Get(uri);
})
.consumer(response -> this.billDownload(Objects.requireNonNull(response.getBody()).get("download_url").asText()))
.consumer(wechatResponseEntity::convert)
.request();
String downloadUrl = Objects.requireNonNull(wechatResponseEntity.getBody())
.get("download_url")
.asText();
String ext = Objects.equals(TarType.GZIP, tradeBillParams.getTarType()) ? ".gzip" : ".txt";
String filename = "tradebill-" + tradeBillParams.getBillDate().toString() + ext;
return downloadBillResponse(downloadUrl, filename);
}
/**
@@ -289,10 +307,12 @@ public abstract class AbstractApi {
* </ul>
*
* @param fundFlowBillParams fundFlowBillParams
* @return the response entity
* @since 1.0.3.RELEASE
*/
public final void downloadFundFlowBill(FundFlowBillParams fundFlowBillParams) {
this.client().withType(WechatPayV3Type.FUNDFLOWBILL, fundFlowBillParams)
public ResponseEntity<Resource> downloadFundFlowBill(FundFlowBillParams fundFlowBillParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.FUND_FLOW_BILL, fundFlowBillParams)
.function((wechatPayV3Type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
LocalDate billDate = params.getBillDate();
@@ -310,7 +330,46 @@ public abstract class AbstractApi {
.build().toUri();
return Get(uri);
})
.consumer(response -> this.billDownload(Objects.requireNonNull(response.getBody()).get("download_url").asText()))
.consumer(wechatResponseEntity::convert)
.request();
String downloadUrl = Objects.requireNonNull(wechatResponseEntity.getBody())
.get("download_url")
.asText();
String ext = Objects.equals(TarType.GZIP, fundFlowBillParams.getTarType()) ? ".gzip" : ".txt";
String filename = "fundflowbill-" + fundFlowBillParams.getBillDate().toString() + ext;
return this.downloadBillResponse(downloadUrl, filename);
}
/**
* 调用{@code /v3/billdownload/file}直接下载为文件.
*
* @param downloadUrl 格式为 {@code https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx}
* @param filename 文件名称包含扩展名
* @return the response entity
*/
public ResponseEntity<Resource> downloadBillResponse(String downloadUrl, String filename) {
ResponseEntity<Resource> responseEntity = this.billResource(downloadUrl);
HttpHeaders httpHeaders = new HttpHeaders();
// utf is not support
httpHeaders.setContentDisposition(ContentDisposition.attachment().filename(filename).build());
return ResponseEntity.ok().headers(httpHeaders).body(responseEntity.getBody());
}
/**
* 调用{@code /v3/billdownload/file}下载返回的原始文件流
*
* @param link the link
* @return response entity
*/
protected ResponseEntity<Resource> billResource(String link) {
return this.client().withType(WechatPayV3Type.FILE_DOWNLOAD, link)
.function((type, downloadUrl) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(downloadUrl)
.build()
.toUri();
return Get(uri);
})
.resource();
}
}

View File

@@ -22,10 +22,7 @@ package cn.felord.payment.wechat.v3;
import cn.felord.payment.PayException;
import org.springframework.core.io.ClassPathResource;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.*;
import java.security.cert.X509Certificate;
/**
@@ -36,9 +33,16 @@ import java.security.cert.X509Certificate;
**/
public class KeyPairFactory {
private KeyStore store;
private static final KeyStore PKCS12_KEY_STORE;
static {
try {
PKCS12_KEY_STORE = KeyStore.getInstance("PKCS12");
} catch (KeyStoreException e) {
throw new PayException(" wechat pay keystore initialization failed");
}
}
private final Object lock = new Object();
/**
* 获取公私钥.
@@ -48,23 +52,16 @@ public class KeyPairFactory {
* @param keyPass password
* @return the key pair
*/
public WechatMetaBean createPKCS12(String keyPath, String keyAlias, String keyPass) {
public WechatMetaBean initWechatMetaBean(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
PKCS12_KEY_STORE.load(resource.getInputStream(), pem);
X509Certificate certificate = (X509Certificate) PKCS12_KEY_STORE.getCertificate(keyAlias);
certificate.checkValidity();
String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
PublicKey publicKey = certificate.getPublicKey();
PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
PrivateKey storeKey = (PrivateKey) PKCS12_KEY_STORE.getKey(keyAlias, pem);
WechatMetaBean wechatMetaBean = new WechatMetaBean();
wechatMetaBean.setKeyPair(new KeyPair(publicKey, storeKey));
wechatMetaBean.setSerialNumber(serialNumber);

View File

@@ -30,9 +30,7 @@ import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.http.*;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Base64Utils;
import org.springframework.util.IdGenerator;
import org.springframework.util.*;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
@@ -120,7 +118,6 @@ public class SignatureProvider {
/**
* 我方请求前用 SHA256withRSA 加签使用API证书.
*
* @param newLine the new line
* @param tenantId the properties key
* @param method the method
* @param canonicalUrl the canonical url
@@ -128,7 +125,7 @@ public class SignatureProvider {
* @return the string
*/
@SneakyThrows
public String requestSign(boolean newLine,String tenantId, String method, String canonicalUrl, String body) {
public String requestSign(String tenantId, String method, String canonicalUrl, String body) {
long timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
String nonceStr = nonceStrGenerator.generateId()
@@ -136,7 +133,7 @@ public class SignatureProvider {
.replaceAll("-", "");
WechatMetaBean wechatMetaBean = wechatMetaContainer.getWechatMeta(tenantId);
PrivateKey privateKey = wechatMetaBean.getKeyPair().getPrivate();
String encode = this.doRequestSign(newLine,privateKey, method, canonicalUrl, String.valueOf(timestamp), nonceStr, body);
String encode = this.doRequestSign(privateKey, method, canonicalUrl, String.valueOf(timestamp), nonceStr, body);
// 序列号
String serialNo = wechatMetaBean.getSerialNumber();
// 生成token
@@ -150,17 +147,16 @@ public class SignatureProvider {
/**
* Do request sign.
*
* @param newLine the has suffix
* @param privateKey the private key
* @param orderedComponents the orderedComponents
* @return the string
* @since 1.0.4.RELEASE
*/
@SneakyThrows
public String doRequestSign(boolean newLine,PrivateKey privateKey, String... orderedComponents) {
public String doRequestSign(PrivateKey privateKey, String... orderedComponents) {
Signature signer = Signature.getInstance("SHA256withRSA", BC_PROVIDER);
signer.initSign(privateKey);
final String signatureStr = createSign(newLine,orderedComponents);
final String signatureStr = createSign(true, orderedComponents);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(signer.sign());
}
@@ -180,7 +176,7 @@ public class SignatureProvider {
}
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);
final String signatureStr = createSign(true,params.getWechatpayTimestamp(), params.getWechatpayNonce(), params.getBody());
final String signatureStr = createSign(true, params.getWechatpayTimestamp(), params.getWechatpayNonce(), params.getBody());
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(certificate);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
@@ -208,7 +204,7 @@ public class SignatureProvider {
}
// 签名
HttpMethod httpMethod = WechatPayV3Type.CERT.method();
String authorization = requestSign(true,tenantId, httpMethod.name(), canonicalUrl, "");
String authorization = requestSign(tenantId, httpMethod.name(), canonicalUrl, "");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
@@ -224,8 +220,8 @@ public class SignatureProvider {
}
ArrayNode certificates = bodyObjectNode.withArray("data");
if (certificates.isArray() && certificates.size() > 0) {
CERTIFICATE_MAP.clear();
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X509",BC_PROVIDER);
CERTIFICATE_MAP.remove(tenantId);
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X509", BC_PROVIDER);
certificates.forEach(objectNode -> {
JsonNode encryptCertificate = objectNode.get("encrypt_certificate");
String associatedData = encryptCertificate.get("associated_data").asText();
@@ -256,6 +252,15 @@ public class SignatureProvider {
* @return the string
*/
public String decryptResponseBody(String tenantId, String associatedData, String nonce, String ciphertext) {
try {
Assert.hasText(associatedData, "associatedData is invalid");
Assert.hasText(nonce, "nonce is invalid");
Assert.hasText(ciphertext, "ciphertext is invalid");
} catch (Exception e) {
throw new PayException(e.getMessage());
}
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", BC_PROVIDER);
String apiV3Key = wechatMetaContainer.getWechatMeta(tenantId).getV3().getAppV3Secret();
@@ -285,14 +290,14 @@ public class SignatureProvider {
* @return encrypt message
* @since 1.0.6.RELEASE
*/
public String encryptRequestMessage(String message,Certificate certificate) {
public String encryptRequestMessage(String message, Certificate certificate) {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding", BC_PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] cipherData = cipher.doFinal(data);
return Base64Utils.encodeToString(cipherData);
return Base64Utils.encodeToString(cipherData);
} catch (Exception e) {
throw new PayException(e);
@@ -304,14 +309,14 @@ public class SignatureProvider {
*
* @return the x 509 wechat certificate info
*/
public X509WechatCertificateInfo getCertificate(){
public X509WechatCertificateInfo getCertificate() {
for (String serial : CERTIFICATE_MAP.keySet()) {
X509Certificate x509Cert = (X509Certificate) CERTIFICATE_MAP.get(serial);
try {
x509Cert.checkValidity();
X509WechatCertificateInfo x509WechatCertificateInfo = new X509WechatCertificateInfo();
x509WechatCertificateInfo.setWechatPaySerial(serial);
x509WechatCertificateInfo.setX509Certificate(x509Cert);
x509WechatCertificateInfo.setWechatPaySerial(serial);
x509WechatCertificateInfo.setX509Certificate(x509Cert);
return x509WechatCertificateInfo;
} catch (Exception e) {
log.warn("the wechat certificate is invalid , {}", e.getMessage());
@@ -348,9 +353,9 @@ public class SignatureProvider {
* @param components the components
* @return string string
*/
private static String createSign(boolean newLine,String... components) {
private static String createSign(boolean newLine, String... components) {
String suffix = newLine? "\n":"";
String suffix = newLine ? "\n" : "";
return Arrays.stream(components)
.collect(Collectors.joining("\n", "", suffix));
}

View File

@@ -18,6 +18,7 @@
*/
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.v2.WechatAllocationApi;
import cn.felord.payment.wechat.v2.WechatPayRedpackApi;
import cn.felord.payment.wechat.v2.WechatPayTransfersApi;
import cn.felord.payment.wechat.v2.WechatV2Client;
@@ -122,7 +123,7 @@ public class WechatApiProvider {
}
/**
* 批量转账到零钱.
* 批量转账到零钱.
* <p>
* 批量转账到零钱提供商户同时向多个用户微信零钱转账的能力。商户可以使用批量转账到零钱用于费用报销、员工福利发放、合作伙伴货款或服务款项支付等场景,提高转账效率。
*
@@ -177,4 +178,38 @@ public class WechatApiProvider {
return new WechatPayTransfersApi(wechatV2Client);
}
/**
* 微信支付分账基于V2
*
* @param tenantId the tenant id
* @return wechat allocation api
* @since 1.0.10.RELEASE
*/
public WechatAllocationApi allocationApi(String tenantId) {
WechatMetaBean wechatMeta = wechatPayClient.signatureProvider()
.wechatMetaContainer()
.getWechatMeta(tenantId);
WechatV2Client wechatV2Client = new WechatV2Client(wechatMeta);
return new WechatAllocationApi(wechatV2Client);
}
/**
* 直连商户微信支付分账基于V3
*
* @param tenantId the tenant id
* @return the wechat profitsharing api
*/
public WechatProfitsharingApi profitsharingApi(String tenantId) {
return new WechatProfitsharingApi(wechatPayClient, tenantId);
}
/**
* 服务商微信支付分账基于V3
*
* @param tenantId the tenant id
* @return the wechat partner profitsharing api
*/
public WechatPartnerProfitsharingApi partnerProfitsharingApi(String tenantId) {
return new WechatPartnerProfitsharingApi(wechatPayClient, tenantId);
}
}

View File

@@ -17,11 +17,10 @@
package cn.felord.payment.wechat.v3;
import cn.felord.payment.wechat.enumeration.FundFlowAccountType;
import cn.felord.payment.wechat.enumeration.WeChatServer;
import cn.felord.payment.wechat.enumeration.WechatPayV3Type;
import cn.felord.payment.wechat.v3.model.batchtransfer.CreateBatchTransferParams;
import cn.felord.payment.wechat.v3.model.batchtransfer.QueryBatchTransferDetailParams;
import cn.felord.payment.wechat.v3.model.batchtransfer.QueryBatchTransferParams;
import cn.felord.payment.wechat.v3.model.batchtransfer.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
@@ -38,6 +37,7 @@ import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
@@ -77,11 +77,11 @@ public class WechatBatchTransferApi extends AbstractApi {
List<CreateBatchTransferParams.TransferDetailListItem> transferDetailList = createBatchTransferParams.getTransferDetailList();
SignatureProvider signatureProvider = this.client().signatureProvider();
final X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
final X509Certificate x509Certificate = certificate.getX509Certificate();
List<CreateBatchTransferParams.TransferDetailListItem> encrypted = transferDetailList.stream()
.peek(transferDetailListItem -> {
String userName = transferDetailListItem.getUserName();
X509Certificate x509Certificate = certificate.getX509Certificate();
String encryptedUserName = signatureProvider.encryptRequestMessage(userName, x509Certificate);
transferDetailListItem.setUserName(encryptedUserName);
String userIdCard = transferDetailListItem.getUserIdCard();
@@ -140,13 +140,13 @@ public class WechatBatchTransferApi extends AbstractApi {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_DETAIL_WECHAT, queryBatchTransferDetailParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("batch_id", params.getBatchIdOrOutBatchNo());
queryParams.add("detail_id", params.getDetailIdOrOutDetailNo());
Map<String, String> pathParams = new HashMap<>(2);
pathParams.put("batch_id", params.getBatchIdOrOutBatchNo());
pathParams.put("detail_id", params.getDetailIdOrOutDetailNo());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(queryParams)
.expand(pathParams)
.toUri();
return Get(uri);
})
@@ -195,13 +195,13 @@ public class WechatBatchTransferApi extends AbstractApi {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_DETAIL_MCH, queryBatchTransferDetailParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("out_batch_no", params.getBatchIdOrOutBatchNo());
queryParams.add("out_detail_no", params.getDetailIdOrOutDetailNo());
Map<String, String> pathParams = new HashMap<>(2);
pathParams.put("batch_id", params.getBatchIdOrOutBatchNo());
pathParams.put("detail_id", params.getDetailIdOrOutDetailNo());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(queryParams)
.expand(pathParams)
.toUri();
return Get(uri);
})
@@ -256,4 +256,150 @@ public class WechatBatchTransferApi extends AbstractApi {
Assert.hasText(downloadUrl, "download url has no text");
return this.billResource(downloadUrl);
}
/**
* 转账明细电子回单受理API
* <p>
* 受理转账明细电子回单接口,商户通过该接口可以申请受理转账明细单电子回单服务。
* <p>
* 返回的下载链接可调用{@link #downloadBillResponse(String, String)}下载文件
*
* @param params the params
* @return the wechat response entity
* @since 1.0.11.RELEASE
*/
public WechatResponseEntity<ObjectNode> transferElectronic(TransferDetailElectronicParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_ELECTRONIC, params)
.function((type, transferDetailElectronic) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, transferDetailElectronic);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询转账明细电子回单受理结果API
* <p>
* 查询转账明细电子回单受理结果接口,商户通过该接口可以查询电子回单受理进度信息,
* 包括电子回单据信息电子回单文件的hash值电子回单文件的下载地址等。
* <p>
* 返回的下载链接可调用{@link #downloadBillResponse(String, String)}下载文件
*
* @param params the params
* @return the wechat response entity
* @since 1.0.11.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryTransferElectronicResult(TransferDetailElectronicParams params) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_ELECTRONIC_DETAIL, params)
.function((type, transferDetailElectronic) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("accept_type", transferDetailElectronic.getAcceptType().name());
queryParams.add("out_batch_no", transferDetailElectronic.getOutBatchNo());
queryParams.add("out_detail_no", transferDetailElectronic.getOutDetailNo());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询账户实时余额API
* <p>
* 商户通过此接口可以查询本商户号的账号余额情况。
*
* @param accountType the account type
* @return the wechat response entity
* @since 1.0.11.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryFundBalance(FundFlowAccountType accountType) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_FUND_BALANCE, accountType)
.function((type, flowAccountType) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.build()
.expand(flowAccountType.name())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询账户日终余额API
* <p>
* 通过此接口可以查询本商户号指定日期当天24点的账户余额。
*
* @param queryDayBalanceParams the transfer day balance params
* @return the wechat response entity
* @since 1.0.11.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryDayFundBalance(QueryDayBalanceParams queryDayBalanceParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_FUND_DAY_BALANCE, queryDayBalanceParams)
.function((type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParam("date", params.getDate().toString())
.build()
.expand(params.getAccountType().name())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 商户银行来账查询API
* <p>
* 商户通过本接口查询指定日期内本商户银行来账记录列表。
* 列表内包含本商户银行来账相关的业务单号、金额、完成时间等信息,用于查询和核对。
* <p>
* 注意:
* <ol>
* <li>如需检索,请在前端缓存所有银行来账记录数据并自行完成检索功能;</li>
* <li>调用该接口前,商户需提前开通“来账识别”产品权限;</li>
* <li>本接口对可查询的商户范围有所规定,仅支持对本商户进行查询;</li>
* <li>本接口仅提供近90天内的银行来账记录查询且一次只能查询一天商户需确保查询记录日期在此范围内</li>
* <li>本接口返回的记录字段后续可能会有所扩充,商户需做好接口兼容准备;</li>
* <li>单商户单接口最大请求频率不超过50TPS。</li>
* </ol>
*
* @param queryIncomeRecordParams the transfer day balance params
* @return the wechat response entity
* @since 1.0.11.RELEASE
*/
public WechatResponseEntity<ObjectNode> queryIncomeRecords(QueryIncomeRecordParams queryIncomeRecordParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.BATCH_TRANSFER_FUND_INCOME_RECORDS, queryIncomeRecordParams)
.function((type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("account_type", params.getAccountType().name());
queryParams.add("date", params.getDate().toString());
queryParams.add("offset", Optional.ofNullable(params.getOffset()).orElse(0).toString());
queryParams.add("limit", params.getLimit().toString());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
}

View File

@@ -87,7 +87,7 @@ public class WechatDirectPayApi extends AbstractApi {
.toString()
.replaceAll("-", "");
String prepayId = body.get("prepay_id").asText();
String paySign = signatureProvider.doRequestSign(true,privateKey, appId, timestamp, nonceStr, prepayId);
String paySign = signatureProvider.doRequestSign(privateKey, appId, timestamp, nonceStr, prepayId);
String mchId = wechatMetaBean.getV3().getMchId();
body.put("appid", appId);
@@ -136,7 +136,7 @@ public class WechatDirectPayApi extends AbstractApi {
.toString()
.replaceAll("-", "");
String packageStr = "prepay_id=" + body.get("prepay_id").asText();
String paySign = signatureProvider.doRequestSign(true,privateKey, appId, timestamp, nonceStr, packageStr);
String paySign = signatureProvider.doRequestSign(privateKey, appId, timestamp, nonceStr, packageStr);
body.put("appId", appId);
body.put("timeStamp", timestamp);

View File

@@ -384,7 +384,7 @@ public class WechatMarketingBusiFavorApi extends AbstractApi {
.expand(updateParams.getStockId())
.toUri();
updateParams.setStockId(null);
return Post(uri, updateParams);
return Patch(uri, updateParams);
})
.consumer(wechatResponseEntity::convert)
.request();

View File

@@ -353,9 +353,9 @@ public class WechatMarketingFavorApi extends AbstractApi {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("appid", v3.getAppId());
MultiValueMap<String, String> pathParams = new LinkedMultiValueMap<>();
pathParams.add("openid", params.getOpenId());
pathParams.add("coupon_id", params.getCouponId());
Map<String, String> pathParams = new HashMap<>(2);
pathParams.put("openid", params.getOpenId());
pathParams.put("coupon_id", params.getCouponId());
URI uri = UriComponentsBuilder.fromHttpUrl(type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
@@ -493,7 +493,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
*
* @param stockId the stock id
* @return the wechat response entity
* @see AbstractApi#billDownload(String) AbstractApi#billDownload(String)对账单下载api
* @see AbstractApi#billCsvDownload(String) AbstractApi#billDownload(String)对账单下载api
*/
public WechatResponseEntity<ObjectNode> downloadStockUseFlow(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
@@ -501,7 +501,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.function(this::downloadFlowFunction)
.consumer(wechatResponseEntity::convert)
.request();
String csv = this.billDownload(wechatResponseEntity.getBody().get("url").asText());
String csv = this.billCsvDownload(wechatResponseEntity.getBody().get("url").asText());
wechatResponseEntity.getBody().put("csv", csv);
return wechatResponseEntity;
}
@@ -515,7 +515,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
*
* @param stockId the stock id
* @return the wechat response entity
* @see AbstractApi#billDownload(String) AbstractApi#billDownload(String)对账单下载api
* @see AbstractApi#billCsvDownload(String) AbstractApi#billDownload(String)对账单下载api
*/
public WechatResponseEntity<ObjectNode> downloadStockRefundFlow(String stockId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
@@ -523,7 +523,7 @@ public class WechatMarketingFavorApi extends AbstractApi {
.function(this::downloadFlowFunction)
.consumer(wechatResponseEntity::convert)
.request();
String csv = this.billDownload(wechatResponseEntity.getBody().get("url").asText());
String csv = this.billCsvDownload(wechatResponseEntity.getBody().get("url").asText());
wechatResponseEntity.getBody().put("csv", csv);
return wechatResponseEntity;
}

View File

@@ -87,7 +87,7 @@ public class WechatPartnerPayApi extends AbstractApi {
.toString()
.replaceAll("-", "");
String prepayId = body.get("prepay_id").asText();
String paySign = signatureProvider.doRequestSign(true, privateKey, subAppid, timestamp, nonceStr, prepayId);
String paySign = signatureProvider.doRequestSign(privateKey, subAppid, timestamp, nonceStr, prepayId);
body.put("appid", subAppid);
body.put("partnerid", partnerPayParams.getSubMchid());
@@ -136,7 +136,7 @@ public class WechatPartnerPayApi extends AbstractApi {
.toString()
.replaceAll("-", "");
String packageStr = "prepay_id=" + body.get("prepay_id").asText();
String paySign = signatureProvider.doRequestSign(true, privateKey, subAppid, timestamp, nonceStr, packageStr);
String paySign = signatureProvider.doRequestSign(privateKey, subAppid, timestamp, nonceStr, packageStr);
body.put("appId", subAppid);
body.put("timeStamp", timestamp);

View File

@@ -0,0 +1,318 @@
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.profitsharing.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 微信V3服务商分账
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
public class WechatPartnerProfitsharingApi extends AbstractApi {
/**
* Instantiates a new Abstract api.
*
* @param wechatPayClient the wechat pay client
* @param tenantId the tenant id
*/
public WechatPartnerProfitsharingApi(WechatPayClient wechatPayClient, String tenantId) {
super(wechatPayClient, tenantId);
}
/**
* 请求分账API
* <p>
* 微信订单支付成功后,商户发起分账请求,将结算后的资金分到分账接收方
* <p>
* 注意:
* <ul>
* <li>对同一笔订单最多能发起20次分账请求每次请求最多分给50个接收方</li>
* <li>此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取</li>
* </ul>
*
* @param profitSharingOrder the profit sharing order
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> profitsharingOrders(PartnerProfitSharingOrder profitSharingOrder) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS, profitSharingOrder)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
SignatureProvider signatureProvider = this.client().signatureProvider();
X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
final X509Certificate x509Certificate = certificate.getX509Certificate();
params.setAppid(v3.getAppId());
List<Receiver> receivers = params.getReceivers();
if (!CollectionUtils.isEmpty(receivers)) {
List<Receiver> encrypted = receivers.stream()
.peek(receiversItem -> {
String name = receiversItem.getName();
if (StringUtils.hasText(name)) {
String encryptedName = signatureProvider.encryptRequestMessage(name, x509Certificate);
receiversItem.setName(encryptedName);
}
}).collect(Collectors.toList());
params.setReceivers(encrypted);
}
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Wechatpay-Serial", certificate.getWechatPaySerial());
return Post(uri, params, httpHeaders);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询分账结果API
* <p>
* 发起分账请求后,可调用此接口查询分账结果
* <p>
* 注意:
* <ul>
* <li>发起解冻剩余资金请求后,可调用此接口查询解冻剩余资金的结果</li>
* </ul>
*
* @param queryOrderParams the query order params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryProfitsharingOrder(PartnerQueryOrderParams queryOrderParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS_RESULT, queryOrderParams)
.function((wechatPayV3Type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("transaction_id", params.getTransactionId());
Optional.ofNullable(params.getSubMchid())
.ifPresent(mchId -> queryParams.add("sub_mchid", params.getSubMchid()));
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(params.getOutOrderNo())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 请求分账回退API
* <p>
* 如果订单已经分账,在退款时,可以先调此接口,将已分账的资金从分账接收方的账户回退给分账方,再发起退款
* <p>
* 注意:
* <ul>
* <li>分账回退以原分账单为依据,支持多次回退,申请回退总金额不能超过原分账单分给该接收方的金额</li>
* <li>此接口采用同步处理模式,即在接收到商户请求后,会实时返回处理结果</li>
* <li>对同一笔分账单最多能发起20次分账回退请求</li>
* <li>退款和分账回退没有耦合,分账回退可以先于退款请求,也可以后于退款请求</li>
* <li>此功能需要接收方在商户平台-交易中心-分账-分账接收设置下,开启同意分账回退后,才能使用</li>
* </ul>
*
* @param returnOrdersParams the return orders params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> returnOrders(PartnerReturnOrdersParams returnOrdersParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RETURN_ORDERS, returnOrdersParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询分账回退结果API
* <p>
* 商户需要核实回退结果,可调用此接口查询回退结果
* <p>
* 注意:
* <ul>
* <li>如果分账回退接口返回状态为处理中,可调用此接口查询回退结果</li>
* </ul>
*
* @param queryReturnOrderParams the query return order params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryReturnOrders(PartnerQueryReturnOrderParams queryReturnOrderParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RETURN_ORDERS_RESULT, queryReturnOrderParams)
.function((wechatPayV3Type, params) -> {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("out_order_no", params.getOutOrderNo());
Optional.ofNullable(params.getSubMchid())
.ifPresent(mchId -> queryParams.add("sub_mchid", params.getSubMchid()));
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.queryParams(queryParams)
.build()
.expand(params.getOutReturnNo())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 解冻剩余资金API
* <p>
* 不需要进行分账的订单,可直接调用本接口将订单的金额全部解冻给特约商户
* <p>
* 注意:
* <ul>
* <li>调用分账接口后,需要解冻剩余资金时,调用本接口将剩余的分账金额全部解冻给特约商户</li>
* <li>此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取</li>
* </ul>
*
* @param unfreezeParams the unfreeze params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> unfreeze(PartnerUnfreezeParams unfreezeParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS_UNFREEZE, unfreezeParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询剩余待分金额API
* <p>
* 可调用此接口查询订单剩余待分金额
*
* @param transactionId the transaction id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryAmounts(String transactionId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_AMOUNTS, transactionId)
.function((wechatPayV3Type, id) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(id)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 服务商查询剩余待分金额API
* <p>
* 可调用此接口查询订单剩余待分金额
*
* @param subMchid the sub mchid
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryMchConfigs(String subMchid) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_MCH_CONFIG, subMchid)
.function((wechatPayV3Type, id) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(id)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 添加分账接收方API
* <p>
* 商户发起添加分账接收方请求,建立分账接收方列表。后续可通过发起分账请求,将分账方商户结算后的资金,分到该分账接收方
*
* @param addReceiversParams the add receivers params
* @return wechat response entity
*/
public WechatResponseEntity<ObjectNode> addReceivers(PartnerAddReceiversParams addReceiversParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RECEIVERS_ADD, addReceiversParams)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
SignatureProvider signatureProvider = this.client().signatureProvider();
X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
final X509Certificate x509Certificate = certificate.getX509Certificate();
params.setAppid(v3.getAppId());
String name = params.getName();
if (StringUtils.hasText(name)) {
String encryptedName = signatureProvider.encryptRequestMessage(name, x509Certificate);
params.setName(encryptedName);
}
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Wechatpay-Serial", certificate.getWechatPaySerial());
return Post(uri, params, httpHeaders);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 删除分账接收方API
* <p>
* 商户发起删除分账接收方请求。删除后,不支持将分账方商户结算后的资金,分到该分账接收方
*
* @param delReceiversParams the del receivers params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> deleteReceivers(PartnerDelReceiversParams delReceiversParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RECEIVERS_DELETE, delReceiversParams)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
params.setAppid(v3.getAppId());
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
}

View File

@@ -29,18 +29,17 @@ import cn.felord.payment.wechat.v3.model.payscore.PayScoreConsumer;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserConfirmConsumeData;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserPaidConsumeData;
import cn.felord.payment.wechat.v3.model.payscore.PayScoreUserPermissionConsumeData;
import cn.felord.payment.wechat.v3.model.profitsharing.ProfitsharingConsumeData;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -72,11 +71,11 @@ public class WechatPayCallback {
private final String tenantId;
static {
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
SimpleModule module = new JavaTimeModule();
MAPPER.registerModule(module);
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
.registerModule(new JavaTimeModule());
}
/**
@@ -92,6 +91,24 @@ public class WechatPayCallback {
}
/**
* 微信支付分账V2回调.
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return the map
* @since 1.0.10.RELEASE
*/
@SneakyThrows
public Map<String, String> profitSharingCallback(ResponseSignVerifyParams params, Consumer<ProfitSharingConsumeData> consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
ProfitSharingConsumeData consumeData = MAPPER.readValue(data, ProfitSharingConsumeData.class);
consumeDataConsumer.accept(consumeData);
return response();
}
/**
* 微信支付代金券核销回调.
*
@@ -101,14 +118,11 @@ public class WechatPayCallback {
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map<String, ?> couponCallback(ResponseSignVerifyParams params, Consumer<CouponConsumeData> consumeDataConsumer) {
public Map<String, String> couponCallback(ResponseSignVerifyParams params, Consumer<CouponConsumeData> consumeDataConsumer) {
String data = this.callback(params, EventType.COUPON_USE);
CouponConsumeData consumeData = MAPPER.readValue(data, CouponConsumeData.class);
consumeDataConsumer.accept(consumeData);
Map<String, Object> responseBody = new HashMap<>(2);
responseBody.put("code", 200);
responseBody.put("message", "SUCCESS");
return responseBody;
return response();
}
@@ -123,12 +137,11 @@ public class WechatPayCallback {
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map<String, ?> transactionCallback(ResponseSignVerifyParams params, Consumer<TransactionConsumeData> consumeDataConsumer) {
public Map<String, String> transactionCallback(ResponseSignVerifyParams params, Consumer<TransactionConsumeData> consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
TransactionConsumeData consumeData = MAPPER.readValue(data, TransactionConsumeData.class);
consumeDataConsumer.accept(consumeData);
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -142,12 +155,11 @@ public class WechatPayCallback {
* @since 1.0.0.RELEASE
*/
@SneakyThrows
public Map<String, ?> combineTransactionCallback(ResponseSignVerifyParams params, Consumer<CombineTransactionConsumeData> consumeDataConsumer) {
public Map<String, String> combineTransactionCallback(ResponseSignVerifyParams params, Consumer<CombineTransactionConsumeData> consumeDataConsumer) {
String data = this.callback(params, EventType.TRANSACTION);
CombineTransactionConsumeData consumeData = MAPPER.readValue(data, CombineTransactionConsumeData.class);
consumeDataConsumer.accept(consumeData);
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -161,7 +173,7 @@ public class WechatPayCallback {
* @since 1.0.2.RELEASE
*/
@SneakyThrows
public Map<String, ?> payscoreUserOrderCallback(ResponseSignVerifyParams params, PayScoreConsumer payScoreConsumer) {
public Map<String, String> payscoreUserOrderCallback(ResponseSignVerifyParams params, PayScoreConsumer payScoreConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
@@ -177,8 +189,7 @@ public class WechatPayCallback {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -193,7 +204,7 @@ public class WechatPayCallback {
* @return the map
*/
@SneakyThrows
public Map<String, ?> permissionCallback(ResponseSignVerifyParams params, Consumer<PayScoreUserPermissionConsumeData> consumeDataConsumer) {
public Map<String, String> permissionCallback(ResponseSignVerifyParams params, Consumer<PayScoreUserPermissionConsumeData> consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
boolean closed;
@@ -209,7 +220,7 @@ public class WechatPayCallback {
PayScoreUserPermissionConsumeData consumeData = MAPPER.readValue(data, PayScoreUserPermissionConsumeData.class);
consumeData.setClosed(closed);
consumeDataConsumer.accept(consumeData);
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -245,7 +256,7 @@ public class WechatPayCallback {
log.error("wechat pay event type is not matched, callbackParams {}", callbackParams);
throw new PayException(" wechat pay event type is not matched");
}
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -260,7 +271,7 @@ public class WechatPayCallback {
* @return the map
*/
@SneakyThrows
public Map<String, ?> busiFavorReceiveCallback(ResponseSignVerifyParams params, Consumer<BusiFavorReceiveConsumeData> consumeDataConsumer) {
public Map<String, String> busiFavorReceiveCallback(ResponseSignVerifyParams params, Consumer<BusiFavorReceiveConsumeData> consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
@@ -272,7 +283,7 @@ public class WechatPayCallback {
BusiFavorReceiveConsumeData consumeData = MAPPER.readValue(data, BusiFavorReceiveConsumeData.class);
consumeDataConsumer.accept(consumeData);
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
@@ -282,10 +293,10 @@ public class WechatPayCallback {
*
* @param params the params
* @param consumeDataConsumer the consume data consumer
* @return map
* @return map map
*/
@SneakyThrows
public Map<String, ?> refundCallback(ResponseSignVerifyParams params, Consumer<RefundConsumeData> consumeDataConsumer) {
public Map<String, String> refundCallback(ResponseSignVerifyParams params, Consumer<RefundConsumeData> consumeDataConsumer) {
CallbackParams callbackParams = resolve(params);
String eventType = callbackParams.getEventType();
@@ -299,9 +310,23 @@ public class WechatPayCallback {
RefundConsumeData consumeData = MAPPER.readValue(data, RefundConsumeData.class);
consumeDataConsumer.accept(consumeData);
return Collections.singletonMap("code", "SUCCESS");
return response();
}
/**
* 微信支付分账V3动账通知
*
* @param params the params
* @param profitsharingConsumeDataConsumer the profitsharing consume data consumer
* @return map map
*/
@SneakyThrows
public Map<String, String> profitsharingCallback(ResponseSignVerifyParams params, Consumer<ProfitsharingConsumeData> profitsharingConsumeDataConsumer) {
String callback = this.callback(params, EventType.TRANSACTION);
ProfitsharingConsumeData consumeData = MAPPER.readValue(callback, ProfitsharingConsumeData.class);
profitsharingConsumeDataConsumer.accept(consumeData);
return response();
}
/**
* Callback.
@@ -353,6 +378,18 @@ public class WechatPayCallback {
return data;
}
/**
* 回调应答
*
* @return response
*/
private Map<String, String> response() {
Map<String, String> responseBody = new HashMap<>(2);
responseBody.put("code", "SUCCESS");
responseBody.put("message", "SUCCESS");
return responseBody;
}
/**
* 事件类型用于处理回调.
@@ -430,7 +467,7 @@ public class WechatPayCallback {
COUPON_SEND("COUPON.SEND"),
/**
* 支付成功事件.
* 支付成功、分账、分账回退事件.
*
* @since 1.0.0.RELEASE
*/

View File

@@ -164,7 +164,7 @@ public class WechatPayClient {
public void request() {
RequestEntity<?> requestEntity = this.requestEntityBiFunction.apply(this.wechatPayV3Type, this.model);
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
this.doExecute(this.header(true,wechatRequestEntity));
this.doExecute(this.header(wechatRequestEntity));
}
@@ -176,7 +176,7 @@ public class WechatPayClient {
public String download() {
RequestEntity<?> requestEntity = this.requestEntityBiFunction.apply(this.wechatPayV3Type, this.model);
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
return this.doDownload(this.header(true,wechatRequestEntity));
return this.doDownload(this.header(wechatRequestEntity));
}
/**
@@ -185,10 +185,10 @@ public class WechatPayClient {
* @return the string
* @since 1.0.6.RELEASE
*/
public ResponseEntity<Resource> resource() {
protected ResponseEntity<Resource> resource() {
RequestEntity<?> requestEntity = this.requestEntityBiFunction.apply(this.wechatPayV3Type, this.model);
WechatRequestEntity<?> wechatRequestEntity = WechatRequestEntity.of(requestEntity, this.responseBodyConsumer);
return this.doResource(this.header(false,wechatRequestEntity));
return this.doResource(this.header(wechatRequestEntity));
}
@@ -199,7 +199,7 @@ public class WechatPayClient {
* @param requestEntity the request entity
* @return the wechat request entity
*/
private <T> WechatRequestEntity<T> header(boolean newLine,WechatRequestEntity<T> requestEntity) {
private <T> WechatRequestEntity<T> header(WechatRequestEntity<T> requestEntity) {
UriComponents uri = UriComponentsBuilder.fromUri(requestEntity.getUrl()).build();
String canonicalUrl = uri.getPath();
@@ -220,7 +220,7 @@ public class WechatPayClient {
}
String tenantId = Objects.requireNonNull(headers.get("Pay-TenantId")).get(0);
String authorization = signatureProvider.requestSign(newLine,tenantId, httpMethod.name(), canonicalUrl, body);
String authorization = signatureProvider.requestSign(tenantId, httpMethod.name(), canonicalUrl, body);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.addAll(headers);

View File

@@ -29,6 +29,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
/**
* 微信支付分API.
@@ -63,14 +65,14 @@ public class WechatPayScoreApi extends AbstractApi {
.function((wechatPayV3Type, userServiceStateParams) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
MultiValueMap<String, String> expandParams = new LinkedMultiValueMap<>();
expandParams.add("appid", v3.getAppId());
expandParams.add("service_id", params.getServiceId());
expandParams.add("openid", params.getOpenId());
Map<String, String> pathParams = new HashMap<>(3);
pathParams.put("appid", v3.getAppId());
pathParams.put("service_id", params.getServiceId());
pathParams.put("openid", params.getOpenId());
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(expandParams)
.expand(pathParams)
.toUri();
return Get(uri);
})

View File

@@ -0,0 +1,285 @@
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.profitsharing.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.stream.Collectors;
/**
* 微信V3直联商户分账
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
public class WechatProfitsharingApi extends AbstractApi {
/**
* Instantiates a new Abstract api.
*
* @param wechatPayClient the wechat pay client
* @param tenantId the tenant id
*/
public WechatProfitsharingApi(WechatPayClient wechatPayClient, String tenantId) {
super(wechatPayClient, tenantId);
}
/**
* 请求分账API
* <p>
* 微信订单支付成功后,商户发起分账请求,将结算后的资金分到分账接收方
* <p>
* 注意:
* <ul>
* <li>对同一笔订单最多能发起20次分账请求每次请求最多分给50个接收方</li>
* <li>此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取</li>
* </ul>
*
* @param profitSharingOrder the profit sharing order
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> profitsharingOrders(ProfitSharingOrder profitSharingOrder) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS, profitSharingOrder)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
SignatureProvider signatureProvider = this.client().signatureProvider();
X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
final X509Certificate x509Certificate = certificate.getX509Certificate();
params.setAppid(v3.getAppId());
List<Receiver> receivers = params.getReceivers();
if (!CollectionUtils.isEmpty(receivers)) {
List<Receiver> encrypted = receivers.stream()
.peek(receiversItem -> {
String name = receiversItem.getName();
if (StringUtils.hasText(name)) {
String encryptedName = signatureProvider.encryptRequestMessage(name, x509Certificate);
receiversItem.setName(encryptedName);
}
}).collect(Collectors.toList());
params.setReceivers(encrypted);
}
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Wechatpay-Serial", certificate.getWechatPaySerial());
return Post(uri, params, httpHeaders);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询分账结果API
* <p>
* 发起分账请求后,可调用此接口查询分账结果
* <p>
* 注意:
* <ul>
* <li>发起解冻剩余资金请求后,可调用此接口查询解冻剩余资金的结果</li>
* </ul>
*
* @param queryOrderParams the query order params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryProfitsharingOrder(QueryOrderParams queryOrderParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS_RESULT, queryOrderParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.queryParam("transaction_id", params.getTransactionId())
.build()
.expand(params.getOutOrderNo())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 请求分账回退API
* <p>
* 如果订单已经分账,在退款时,可以先调此接口,将已分账的资金从分账接收方的账户回退给分账方,再发起退款
* <p>
* 注意:
* <ul>
* <li>分账回退以原分账单为依据,支持多次回退,申请回退总金额不能超过原分账单分给该接收方的金额</li>
* <li>此接口采用同步处理模式,即在接收到商户请求后,会实时返回处理结果</li>
* <li>对同一笔分账单最多能发起20次分账回退请求</li>
* <li>退款和分账回退没有耦合,分账回退可以先于退款请求,也可以后于退款请求</li>
* <li>此功能需要接收方在商户平台-交易中心-分账-分账接收设置下,开启同意分账回退后,才能使用</li>
* </ul>
*
* @param returnOrdersParams the return orders params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> returnOrders(ReturnOrdersParams returnOrdersParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RETURN_ORDERS, returnOrdersParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询分账回退结果API
* <p>
* 商户需要核实回退结果,可调用此接口查询回退结果
* <p>
* 注意:
* <ul>
* <li>如果分账回退接口返回状态为处理中,可调用此接口查询回退结果</li>
* </ul>
*
* @param queryReturnOrderParams the query return order params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryReturnOrders(QueryReturnOrderParams queryReturnOrderParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RETURN_ORDERS_RESULT, queryReturnOrderParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.queryParam("out_order_no", params.getOutOrderNo())
.build()
.expand(params.getOutReturnNo())
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 解冻剩余资金API
* <p>
* 不需要进行分账的订单,可直接调用本接口将订单的金额全部解冻给特约商户
* <p>
* 注意:
* <ul>
* <li>调用分账接口后,需要解冻剩余资金时,调用本接口将剩余的分账金额全部解冻给特约商户</li>
* <li>此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取</li>
* </ul>
*
* @param unfreezeParams the unfreeze params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> unfreeze(UnfreezeParams unfreezeParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_ORDERS_UNFREEZE, unfreezeParams)
.function((wechatPayV3Type, params) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 查询剩余待分金额API
* <p>
* 可调用此接口查询订单剩余待分金额
*
* @param transactionId the transaction id
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> queryAmounts(String transactionId) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_AMOUNTS, transactionId)
.function((wechatPayV3Type, id) -> {
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.expand(id)
.toUri();
return Get(uri);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 添加分账接收方API
* <p>
* 商户发起添加分账接收方请求,建立分账接收方列表。后续可通过发起分账请求,将分账方商户结算后的资金,分到该分账接收方
*
* @param addReceiversParams the add receivers params
* @return wechat response entity
*/
public WechatResponseEntity<ObjectNode> addReceivers(AddReceiversParams addReceiversParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RECEIVERS_ADD, addReceiversParams)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
SignatureProvider signatureProvider = this.client().signatureProvider();
X509WechatCertificateInfo certificate = signatureProvider.getCertificate();
final X509Certificate x509Certificate = certificate.getX509Certificate();
params.setAppid(v3.getAppId());
String name = params.getName();
if (StringUtils.hasText(name)) {
String encryptedName = signatureProvider.encryptRequestMessage(name, x509Certificate);
params.setName(encryptedName);
}
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Wechatpay-Serial", certificate.getWechatPaySerial());
return Post(uri, params, httpHeaders);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
/**
* 删除分账接收方API
* <p>
* 商户发起删除分账接收方请求。删除后,不支持将分账方商户结算后的资金,分到该分账接收方
*
* @param delReceiversParams the del receivers params
* @return the wechat response entity
*/
public WechatResponseEntity<ObjectNode> deleteReceivers(DelReceiversParams delReceiversParams) {
WechatResponseEntity<ObjectNode> wechatResponseEntity = new WechatResponseEntity<>();
this.client().withType(WechatPayV3Type.PROFITSHARING_RECEIVERS_DELETE, delReceiversParams)
.function((wechatPayV3Type, params) -> {
WechatPayProperties.V3 v3 = this.wechatMetaBean().getV3();
params.setAppid(v3.getAppId());
URI uri = UriComponentsBuilder.fromHttpUrl(wechatPayV3Type.uri(WeChatServer.CHINA))
.build()
.toUri();
return Post(uri, params);
})
.consumer(wechatResponseEntity::convert)
.request();
return wechatResponseEntity;
}
}

View File

@@ -0,0 +1,77 @@
/*
*
* Copyright 2019-2020 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.felord.payment.wechat.v3.model;
import cn.felord.payment.wechat.v2.model.allocation.Receiver;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 微信支付分账通知参数
*
* @author wangzecheng
* @since 1.0.10.RELEASE
*/
@Data
public class ProfitSharingConsumeData {
/**
* 直连商户号.
* <p>
* 直连模式分账发起和出资商户
*/
private String mchid;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
/**
* 微信分账/回退单号.
*/
private String orderId;
/**
* 商户分账/回退单号.
* <p>
* 分账方系统内部的分账/回退单号
*/
private String outOrderNo;
/**
* 分账接收方.
* <p>
* 分账接收方对象
*/
private Receiver receiver;
/**
* 成功时间.
* <p>
* Rfc3339标准
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private LocalDateTime successTime;
}

View File

@@ -22,7 +22,7 @@ import cn.felord.payment.wechat.enumeration.RefundStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
/**
* 微信支付退款结果通知解密
@@ -60,7 +60,7 @@ public class RefundConsumeData {
* 退款成功时间
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private OffsetDateTime successTime;
private LocalDateTime successTime;
/**
* 退款入账账户
*/

View File

@@ -75,6 +75,12 @@ public class RefundParams {
*/
@Data
public static class RefundAmount {
/**
* 退款出资账户及金额
*
* @since 1.0.11.RELEASE
*/
private List<RefundForm> form;
/**
* 原订单金额,币种的最小单位,只能为整数,不能超过原订单支付金额。
*/
@@ -89,4 +95,41 @@ public class RefundParams {
private Integer refund;
}
/**
* 退款出资账户及金额
* <p>
* 退款需要从指定账户出资时,传递此参数指定出资金额(币种的最小单位,只能为整数)。
* <p>
* 同时指定多个账户出资退款的使用场景需要满足以下条件:
* <ol>
* <li>未开通退款支出分离产品功能;</li>
* <li>订单属于分账订单,且分账处于待分账或分账中状态。</li>
* </ol>
* <p>
* 参数传递需要满足条件:
* <ol>
* <li>基本账户可用余额出资金额与基本账户不可用余额出资金额之和等于退款金额;</li>
* <li>账户类型不能重复。</li>
* </ol>
* <p>
* 上述任一条件不满足将返回错误
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public static class RefundForm {
/**
* 出资账户类型
* <p>
* {@code AVAILABLE} 可用余额
* {@code UNAVAILABLE} 不可用余额
*/
private String account;
/**
* 对应账户出资金额
*/
private String amount;
}
}

View File

@@ -23,7 +23,7 @@ import cn.felord.payment.wechat.enumeration.TradeType;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -91,7 +91,7 @@ public class TransactionConsumeData {
* 支付完成时间 YYYY-MM-DDTHH:mm:ss+TIMEZONE
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private OffsetDateTime successTime;
private LocalDateTime successTime;
/**
* 在 1.0.0.RELEASE 直接返回了枚举字符串1.0.2.RELEASE 中变更为枚举
*

View File

@@ -0,0 +1,25 @@
package cn.felord.payment.wechat.v3.model.batchtransfer;
import cn.felord.payment.wechat.enumeration.FundFlowAccountType;
import lombok.Data;
import java.time.LocalDate;
/**
* 查询账户日终余额API 请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class QueryDayBalanceParams {
/**
* 账户类型,必填
* @see FundFlowAccountType
*/
private FundFlowAccountType accountType;
/**
* 日期,必填
*/
private LocalDate date;
}

View File

@@ -0,0 +1,37 @@
package cn.felord.payment.wechat.v3.model.batchtransfer;
import cn.felord.payment.wechat.enumeration.FundFlowAccountType;
import lombok.Data;
import java.time.LocalDate;
/**
* 商户银行来账查询API 请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class QueryIncomeRecordParams {
/**
* 账户类型,必填
* @see FundFlowAccountType
*/
private FundFlowAccountType accountType;
/**
* 日期,必填
*/
private LocalDate date;
/**
* 本次查询偏移量,选填
* <p>
* 表示该次请求资源的起始位置从0开始计数。调用方选填默认为0。offset为20limit为100时查询第20-119条数据
*/
private Integer offset;
/**
* 本次请求最大查询条数,必填
* <p>
* 非0非负的整数该次请求可返回的最大资源条数最大支持100条。
*/
private Integer limit;
}

View File

@@ -0,0 +1,35 @@
package cn.felord.payment.wechat.v3.model.batchtransfer;
import cn.felord.payment.wechat.enumeration.TransferAcceptType;
import lombok.Data;
/**
* 转账明细电子回单受理API请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class TransferDetailElectronicParams {
/**
* 电子回单受理类型,必填。
*
* @see TransferAcceptType
*/
private TransferAcceptType acceptType;
/**
* 商家转账批次单号,选填。
* <p>
* 需要电子回单的批量转账明细单所在的转账批次单号,该单号为商户申请转账时生成的商户单号。
* 受理类型为{@code BATCH_TRANSFER}时该单号必填,否则该单号留空。
*/
private String outBatchNo;
/**
* 商家转账明细单号,必填。
* <ul>
* <li>受理类型为{@code BATCH_TRANSFER}时填写商家批量转账明细单号。</li>
* <li>受理类型为{@code TRANSFER_TO_POCKET}或{@code TRANSFER_TO_BANK}时填写商家转账单号。</li>
* </ul>
*/
private String outDetailNo;
}

View File

@@ -20,7 +20,7 @@ package cn.felord.payment.wechat.v3.model.busifavor;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
/**
* 商家券领券事件回调通知解密
@@ -49,8 +49,8 @@ public class BusiFavorReceiveConsumeData {
/**
* 发放时间 rfc 3339 yyyy-MM-ddTHH:mm:ss+TIMEZONE
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "GMT+8")
private OffsetDateTime sendTime;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private LocalDateTime sendTime;
/**
* 微信用户在appid下的唯一标识。
*/

View File

@@ -19,6 +19,7 @@ package cn.felord.payment.wechat.v3.model.combine;
import cn.felord.payment.wechat.enumeration.TradeState;
import cn.felord.payment.wechat.enumeration.TradeType;
import cn.felord.payment.wechat.v3.model.PromotionDetail;
import cn.felord.payment.wechat.v3.model.SceneInfo;
import lombok.Data;
@@ -125,6 +126,13 @@ public class CombineTransactionConsumeData {
*/
private String transactionId;
/**
* 优惠功能,子单有核销优惠券时有返回
*
* @since 1.0.11.RELEASE
*/
private List<PromotionDetail> promotionDetail;
}
/**

View File

@@ -78,7 +78,7 @@ public class CompleteServiceOrderParams {
* false不分账默认false
* true分账。
*/
private Boolean profitSharing = Boolean.TRUE;
private Boolean profitSharing = Boolean.FALSE;
/**
* 订单优惠标记,选填
* <p>

View File

@@ -0,0 +1,92 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.enumeration.ReceiverType;
import lombok.Data;
/**
* 直连商户-添加分账接收方API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class AddReceiversParams {
/**
* 应用ID自动注入
*/
private String appid;
/**
* 分账接收方类型,必填
*/
private ReceiverType type;
/**
* 分账接收方帐号,必填
*/
private String account;
/**
* 分账个人接收方姓名,选填
* <p>
* 分账接收方类型是{@code MERCHANT_ID}时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名 分账接收方类型是{@code PERSONAL_OPENID}时,是个人姓名(选传,传则校验)
* <ol>
* <li>分账接收方类型是{@code PERSONAL_OPENID},是个人姓名的密文(选传,传则校验) 此字段的加密方法详见:敏感信息加密说明</li>
* <li>使用微信支付平台证书中的公钥</li>
* <li>使用RSAES-OAEP算法进行加密</li>
* <li>将请求中HTTP头部的Wechatpay-Serial设置为证书序列号</li>
* </ol>
*/
private String name;
/**
* 与分账方的关系类型,必填
*/
private RelationType relationType;
/**
* 自定义的分账关系,选填
*/
private String customRelation;
/**
* 子商户与接收方的关系
*/
public enum RelationType {
/**
* 门店.
*/
STORE,
/**
* 员工.
*/
STAFF,
/**
* 店主.
*/
STORE_OWNER,
/**
* 合作伙伴.
*/
PARTNER,
/**
* 总部.
*/
HEADQUARTER,
/**
* 品牌方.
*/
BRAND,
/**
* 分销商.
*/
DISTRIBUTOR,
/**
* 用户.
*/
USER,
/**
* 供应商.
*/
SUPPLIER,
/**
* 自定义.
*/
CUSTOM
}
}

View File

@@ -0,0 +1,26 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.enumeration.ReceiverType;
import lombok.Data;
/**
* 直连商户-删除分账接收方API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class DelReceiversParams {
/**
* 应用ID自动注入
*/
private String appid;
/**
* 分账接收方类型,必填
*/
private ReceiverType type;
/**
* 分账接收方帐号,必填
*/
private String account;
}

View File

@@ -0,0 +1,102 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.enumeration.ReceiverType;
import lombok.Data;
/**
* 服务商-添加分账接收方API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerAddReceiversParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 应用ID自动注入
*/
private String appid;
/**
* 子商户应用ID选填
* <p>
* 分账接收方类型包含{@code PERSONAL_SUB_OPENID}时必填
*/
private String subAppid;
/**
* 分账接收方类型,必填
*/
private ReceiverType type;
/**
* 分账接收方帐号,必填
*/
private String account;
/**
* 分账个人接收方姓名,选填
* <p>
* 分账接收方类型是{@code MERCHANT_ID}时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名 分账接收方类型是{@code PERSONAL_OPENID}时,是个人姓名(选传,传则校验)
* <ol>
* <li>分账接收方类型是{@code PERSONAL_OPENID},是个人姓名的密文(选传,传则校验) 此字段的加密方法详见:敏感信息加密说明</li>
* <li>使用微信支付平台证书中的公钥</li>
* <li>使用RSAES-OAEP算法进行加密</li>
* <li>将请求中HTTP头部的Wechatpay-Serial设置为证书序列号</li>
* </ol>
*/
private String name;
/**
* 与分账方的关系类型,必填
*/
private RelationType relationType;
/**
* 自定义的分账关系,选填
*/
private String customRelation;
/**
* 子商户与接收方的关系
*/
public enum RelationType {
/**
* 门店.
*/
STORE,
/**
* 员工.
*/
STAFF,
/**
* 店主.
*/
STORE_OWNER,
/**
* 合作伙伴.
*/
PARTNER,
/**
* 总部.
*/
HEADQUARTER,
/**
* 品牌方.
*/
BRAND,
/**
* 分销商.
*/
DISTRIBUTOR,
/**
* 用户.
*/
USER,
/**
* 供应商.
*/
SUPPLIER,
/**
* 自定义.
*/
CUSTOM
}
}

View File

@@ -0,0 +1,36 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.enumeration.ReceiverType;
import lombok.Data;
/**
* 服务商-删除分账接收方API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerDelReceiversParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 应用ID自动注入
*/
private String appid;
/**
* 子商户应用ID选填
* <p>
* 分账接收方类型包含{@code PERSONAL_SUB_OPENID}时必填
*/
private String subAppid;
/**
* 分账接收方类型,必填
*/
private ReceiverType type;
/**
* 分账接收方帐号,必填
*/
private String account;
}

View File

@@ -0,0 +1,54 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
import java.util.List;
/**
* 服务商请求分账API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerProfitSharingOrder {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 服务商应用ID自动注入
*/
private String appid;
/**
* 子商户应用ID选填
* <p>
* 分账接收方类型包含{@code PERSONAL_SUB_OPENID}时必填
*/
private String subAppid;
/**
* 微信订单号,必填
*/
private String transactionId;
/**
* 商户分账单号,必填
* <p>
* 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。
* 只能是数字、大小写字母_-|*@
*/
private String outOrderNo;
/**
* 分账接收方列表,选填
* <p>
* 可以设置出资商户作为分账接受方最多可有50个分账接收方
*/
private List<Receiver> receivers;
/**
* 是否解冻剩余未分资金,必填
* <ol>
* <li>如果为{@code true},该笔订单剩余未分账的金额会解冻回分账方商户;</li>
* <li>如果为{@code false},该笔订单剩余未分账的金额不会解冻回分账方商户,可以对该笔订单再次进行分账。</li>
* </ol>
*/
private Boolean unfreezeUnsplit;
}

View File

@@ -0,0 +1,84 @@
/*
*
* Copyright 2019-2020 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.v2.model.allocation.Receiver;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务商-微信支付分账动账通知参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerProfitsharingConsumeData {
/**
* 服务商商户号.
* <p>
* 服务商模式分账发起商户
*/
private String mchid;
/**
* 子商户号
* <p>
* 服务商模式分账出资商户
*/
private String subMchid;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
/**
* 微信分账/回退单号.
*/
private String orderId;
/**
* 商户分账/回退单号.
* <p>
* 分账方系统内部的分账/回退单号
*/
private String outOrderNo;
/**
* 分账接收方.
* <p>
* 分账接收方对象
*/
private List<Receiver> receivers;
/**
* 成功时间.
* <p>
* Rfc3339标准
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private LocalDateTime successTime;
}

View File

@@ -0,0 +1,25 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 服务商-查询分账结果API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerQueryOrderParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 商户分账单号,必填
*/
private String outOrderNo;
/**
* 微信订单号,必填
*/
private String transactionId;
}

View File

@@ -0,0 +1,25 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 服务商-查询分账回退结果API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerQueryReturnOrderParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 商户回退单号,必填
*/
private String outReturnNo;
/**
* 商户分账单号,必填
*/
private String outOrderNo;
}

View File

@@ -0,0 +1,45 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 服务商-请求分账回退API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerReturnOrdersParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 微信分账单号,同{@link #outOrderNo} 二选一
*/
private String orderId;
/**
* 商户分账单号,同{@link #orderId} 二选一
*/
private String outOrderNo;
/**
* 商户回退单号,必填
*/
private String outReturnNo;
/**
* 回退商户号,必填
* <p>
* 分账回退的出资商户,只能对原分账请求中成功分给商户接收方进行回退
*/
private String returnMchid;
/**
* 回退金额,必填
* <p>
* 需要从分账接收方回退的金额,单位为分,只能为整数,不能超过原始分账单分出给该接收方的金额
*/
private Integer amount;
/**
* 回退描述,必填
*/
private String description;
}

View File

@@ -0,0 +1,29 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 服务商-解冻剩余资金API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class PartnerUnfreezeParams {
/**
* 子商户号,选填
*/
private String subMchid;
/**
* 微信订单号,必填
*/
private String transactionId;
/**
* 商户分账单号,必填
*/
private String outOrderNo;
/**
* 分账描述,必填
*/
private String description;
}

View File

@@ -0,0 +1,44 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
import java.util.List;
/**
* 直连商户-请求分账API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class ProfitSharingOrder {
/**
* 应用ID自动注入
*/
private String appid;
/**
* 微信订单号,必填
*/
private String transactionId;
/**
* 商户分账单号,必填
* <p>
* 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。
* 只能是数字、大小写字母_-|*@
*/
private String outOrderNo;
/**
* 分账接收方列表,选填
* <p>
* 可以设置出资商户作为分账接受方最多可有50个分账接收方
*/
private List<Receiver> receivers;
/**
* 是否解冻剩余未分资金,必填
* <ol>
* <li>如果为{@code true},该笔订单剩余未分账的金额会解冻回分账方商户;</li>
* <li>如果为{@code false},该笔订单剩余未分账的金额不会解冻回分账方商户,可以对该笔订单再次进行分账。</li>
* </ol>
*/
private Boolean unfreezeUnsplit;
}

View File

@@ -0,0 +1,78 @@
/*
*
* Copyright 2019-2020 felord.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* Website:
* https://felord.cn
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.v2.model.allocation.Receiver;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 直连商户-微信支付分账动账通知参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class ProfitsharingConsumeData {
/**
* 直连商户号.
* <p>
* 直连模式分账发起和出资商户
*/
private String mchid;
/**
* 微信订单号.
* <p>
* 微信支付订单号
*/
private String transactionId;
/**
* 微信分账/回退单号.
*/
private String orderId;
/**
* 商户分账/回退单号.
* <p>
* 分账方系统内部的分账/回退单号
*/
private String outOrderNo;
/**
* 分账接收方.
* <p>
* 分账接收方对象
*/
private List<Receiver> receivers;
/**
* 成功时间.
* <p>
* Rfc3339标准
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "GMT+8")
private LocalDateTime successTime;
}

View File

@@ -0,0 +1,21 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 直连商户-查询分账结果API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class QueryOrderParams {
/**
* 商户分账单号,必填
*/
private String outOrderNo;
/**
* 微信订单号,必填
*/
private String transactionId;
}

View File

@@ -0,0 +1,21 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 直连商户-查询分账回退结果API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class QueryReturnOrderParams {
/**
* 商户回退单号,必填
*/
private String outReturnNo;
/**
* 商户分账单号,必填
*/
private String outOrderNo;
}

View File

@@ -0,0 +1,44 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import cn.felord.payment.wechat.enumeration.ReceiverType;
import lombok.Data;
/**
* 分账接收方信息
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class Receiver {
/**
* 分账接收方类型,必填
*/
private ReceiverType type;
/**
* 分账接收方帐号,必填
*/
private String account;
/**
* 分账个人接收方姓名,选填
* <p>
* 在接收方类型为个人的时可选填,若有值,会检查与 name 是否实名匹配,不匹配会拒绝分账请求
* <ol>
* <li>分账接收方类型是{@code PERSONAL_OPENID},是个人姓名的密文(选传,传则校验) 此字段的加密方法详见:敏感信息加密说明</li>
* <li>使用微信支付平台证书中的公钥</li>
* <li>使用RSAES-OAEP算法进行加密</li>
* <li>将请求中HTTP头部的Wechatpay-Serial设置为证书序列号</li>
* </ol>
*/
private String name;
/**
* 分账金额,必填
* <p>
* 单位为分,只能为整数,不能超过原订单支付金额及最大分账比例金额
*/
private Integer amount;
/**
* 分账的原因描述,必填。分账账单中需要体现
*/
private String description;
}

View File

@@ -0,0 +1,41 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 直连商户-请求分账回退API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class ReturnOrdersParams {
/**
* 微信分账单号,同{@link #outOrderNo} 二选一
*/
private String orderId;
/**
* 商户分账单号,同{@link #orderId} 二选一
*/
private String outOrderNo;
/**
* 商户回退单号,必填
*/
private String outReturnNo;
/**
* 回退商户号,必填
* <p>
* 分账回退的出资商户,只能对原分账请求中成功分给商户接收方进行回退
*/
private String returnMchid;
/**
* 回退金额,必填
* <p>
* 需要从分账接收方回退的金额,单位为分,只能为整数,不能超过原始分账单分出给该接收方的金额
*/
private Integer amount;
/**
* 回退描述,必填
*/
private String description;
}

View File

@@ -0,0 +1,25 @@
package cn.felord.payment.wechat.v3.model.profitsharing;
import lombok.Data;
/**
* 直连商户-解冻剩余资金API-请求参数
*
* @author felord.cn
* @since 1.0.11.RELEASE
*/
@Data
public class UnfreezeParams {
/**
* 微信订单号,必填
*/
private String transactionId;
/**
* 商户分账单号,必填
*/
private String outOrderNo;
/**
* 分账描述,必填
*/
private String description;
}

View File

@@ -5,11 +5,11 @@
<parent>
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
</parent>
<artifactId>payment-spring-boot-starter</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>

52
pom.xml
View File

@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>cn.felord</groupId>
<artifactId>payment-spring-boot</artifactId>
<version>1.0.9.RELEASE</version>
<version>1.0.11.RELEASE</version>
<packaging>pom</packaging>
<modelVersion>4.0.0</modelVersion>
@@ -36,6 +36,29 @@
<developerConnection>scm:git:https://github.com/NotFound403/payment-spring-boot.git</developerConnection>
</scm>
<profiles>
<!-- Deployment profile (required so these plugins are only used when deploying) -->
<profile>
<id>deploy</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<!-- GPG plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<modules>
<module>payment-spring-boot-autoconfigure</module>
@@ -53,14 +76,14 @@
<oss-starter.version>1.0.0.RELEASE</oss-starter.version>
<lombok.verison>1.18.12</lombok.verison>
<jackson.version>2.9.10</jackson.version>
<bcprov.version>1.66</bcprov.version>
<bcprov.version>1.67</bcprov.version>
<jackson.version>2.11.4</jackson.version>
<httpclient.version>4.5.13</httpclient.version>
</properties>
<distributionManagement>
<repository>
<id>sonatype-nexus-staging</id>
<id>ossrh</id>
<name>Nexus Release Repository</name>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
@@ -173,10 +196,18 @@
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
@@ -186,25 +217,14 @@
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>sonatype-nexus-staging</serverId>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>false</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>