添加转账API接口的实现;

This commit is contained in:
terrfly 2021-08-12 15:27:56 +08:00
parent 267d5dfb80
commit 3ad69e9a29
18 changed files with 1258 additions and 7 deletions

View File

@ -365,6 +365,43 @@ CREATE TABLE `t_refund_order` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款订单表';
-- 转账订单表
DROP TABLE IF EXISTS t_transfer_order;
CREATE TABLE `t_transfer_order` (
`transfer_id` VARCHAR(32) NOT NULL COMMENT '转账订单号',
`mch_no` VARCHAR(64) NOT NULL COMMENT '商户号',
`isv_no` VARCHAR(64) COMMENT '服务商号',
`app_id` VARCHAR(64) NOT NULL COMMENT '应用ID',
`mch_name` VARCHAR(30) NOT NULL COMMENT '商户名称',
`mch_type` TINYINT(6) NOT NULL COMMENT '类型: 1-普通商户, 2-特约商户(服务商模式)',
`mch_order_no` VARCHAR(64) NOT NULL COMMENT '商户订单号',
`if_code` VARCHAR(20) NOT NULL COMMENT '支付接口代码',
`entry_type` VARCHAR(20) NOT NULL COMMENT '入账方式: WX_CASH-微信零钱; ALIPAY_CASH-支付宝转账; BANK_CARD-银行卡',
`amount` BIGINT(20) NOT NULL COMMENT '转账金额,单位分',
`currency` VARCHAR(3) NOT NULL DEFAULT 'cny' COMMENT '三位货币代码,人民币:cny',
`account_no` VARCHAR(64) NOT NULL COMMENT '收款账号',
`account_name` VARCHAR(64) COMMENT '收款人姓名',
`bank_name` VARCHAR(32) COMMENT '收款人开户行名称',
`transfer_desc` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '转账备注信息',
`client_ip` VARCHAR(32) DEFAULT NULL COMMENT '客户端IP',
`state` TINYINT(6) NOT NULL DEFAULT '0' COMMENT '支付状态: 0-订单生成, 1-转账中, 2-转账成功, 3-转账失败, 4-订单关闭',
`notify_state` TINYINT(6) NOT NULL DEFAULT '0' COMMENT '向下游回调状态, 0-未发送, 1-已发送',
`channel_extra` VARCHAR(512) DEFAULT NULL COMMENT '特定渠道发起额外参数',
`channel_order_no` VARCHAR(64) DEFAULT NULL COMMENT '渠道订单号',
`err_code` VARCHAR(128) DEFAULT NULL COMMENT '渠道支付错误码',
`err_msg` VARCHAR(256) DEFAULT NULL COMMENT '渠道支付错误描述',
`ext_param` VARCHAR(128) DEFAULT NULL COMMENT '商户扩展参数',
`notify_url` VARCHAR(128) NOT NULL default '' COMMENT '异步通知地址',
`success_time` DATETIME DEFAULT NULL COMMENT '转账成功时间',
`created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间',
PRIMARY KEY (`transfer_id`),
UNIQUE KEY `Uni_MchNo_MchOrderNo` (`mch_no`, `mch_order_no`),
INDEX(`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='转账订单表';
##### ↑↑↑↑↑↑↑↑↑↑ 表结构DDL ↑↑↑↑↑↑↑↑↑↑ #####
##### ↓↓↓↓↓↓↓↓↓↓ 初始化DML ↓↓↓↓↓↓↓↓↓↓ #####

View File

@ -41,9 +41,10 @@ import java.util.Date;
@TableName("t_mch_notify_record")
public class MchNotifyRecord extends BaseModel implements Serializable {
//订单类型:1-支付,2-退款
//订单类型:1-支付,2-退款, 3-转账
public static final byte TYPE_PAY_ORDER = 1;
public static final byte TYPE_REFUND_ORDER = 2;
public static final byte TYPE_TRANSFER_ORDER = 3;
//通知状态
public static final byte STATE_ING = 1;

View File

@ -0,0 +1,195 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.core.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 转账订单表
* </p>
*
* @author [mybatis plus generator]
* @since 2021-08-11
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_transfer_order")
public class TransferOrder implements Serializable {
/** 入账方式 **/
public static final String ENTRY_WX_CASH = "WX_CASH";
public static final String ENTRY_ALIPAY_CASH = "ALIPAY_CASH";
public static final String ENTRY_BANK_CARD = "BANK_CARD";
public static final byte STATE_INIT = 0; //订单生成
public static final byte STATE_ING = 1; //转账中
public static final byte STATE_SUCCESS = 2; //转账成功
public static final byte STATE_FAIL = 3; //转账失败
public static final byte STATE_CLOSED = 4; //转账关闭
public static final LambdaQueryWrapper<TransferOrder> gw(){
return new LambdaQueryWrapper<>();
}
private static final long serialVersionUID=1L;
/**
* 转账订单号
*/
private String transferId;
/**
* 商户号
*/
private String mchNo;
/**
* 服务商号
*/
private String isvNo;
/**
* 应用ID
*/
private String appId;
/**
* 商户名称
*/
private String mchName;
/**
* 类型: 1-普通商户, 2-特约商户(服务商模式)
*/
private Byte mchType;
/**
* 商户订单号
*/
private String mchOrderNo;
/**
* 支付接口代码
*/
private String ifCode;
/**
* 入账方式 WX_CASH-微信零钱; ALIPAY_CASH-支付宝转账; BANK_CARD-银行卡
*/
private String entryType;
/**
* 转账金额,单位分
*/
private Long amount;
/**
* 三位货币代码,人民币:cny
*/
private String currency;
/**
* 收款账号
*/
private String accountNo;
/**
* 收款人姓名
*/
private String accountName;
/**
* 收款人开户行名称
*/
private String bankName;
/**
* 转账备注信息
*/
private String transferDesc;
/**
* 客户端IP
*/
private String clientIp;
/**
* 支付状态: 0-订单生成, 1-转账中, 2-转账成功, 3-转账失败, 4-订单关闭
*/
private Byte state;
/**
* 向下游回调状态, 0-未发送, 1-已发送
*/
private Byte notifyState;
/**
* 特定渠道发起额外参数
*/
private String channelExtra;
/**
* 渠道订单号
*/
private String channelOrderNo;
/**
* 渠道支付错误码
*/
private String errCode;
/**
* 渠道支付错误描述
*/
private String errMsg;
/**
* 商户扩展参数
*/
private String extParam;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 转账成功时间
*/
private Date successTime;
/**
* 创建时间
*/
private Date createdAt;
/**
* 更新时间
*/
private Date updatedAt;
}

View File

@ -34,9 +34,11 @@ public class SeqKit {
private static final AtomicLong PAY_ORDER_SEQ = new AtomicLong(0L);
private static final AtomicLong REFUND_ORDER_SEQ = new AtomicLong(0L);
private static final AtomicLong MHO_ORDER_SEQ = new AtomicLong(0L);
private static final AtomicLong TRANSFER_ID_SEQ = new AtomicLong(0L);
private static final String PAY_ORDER_SEQ_PREFIX = "P";
private static final String REFUND_ORDER_SEQ_PREFIX = "R";
private static final String MHO_ORDER_SEQ_PREFIX = "M";
private static final String TRANSFER_ID_SEQ_PREFIX = "T";
/** 生成支付订单号 **/
public static String genPayOrderId() {
@ -60,4 +62,11 @@ public class SeqKit {
(int) MHO_ORDER_SEQ.getAndIncrement() % 10000);
}
/** 模拟生成商户订单号 **/
public static String genTransferId() {
return String.format("%s%s%04d", TRANSFER_ID_SEQ_PREFIX,
DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
(int) TRANSFER_ID_SEQ.getAndIncrement() % 10000);
}
}

View File

@ -19,7 +19,7 @@ import com.jeequan.jeepay.core.entity.PayOrder;
import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
/*
/**
* 查单渠道侧接口定义
*
* @author terrfly
@ -28,7 +28,7 @@ import com.jeequan.jeepay.pay.model.MchAppConfigContext;
*/
public interface IPayOrderQueryService {
/* 获取到接口code **/
/** 获取到接口code **/
String getIfCode();
/** 查询订单 **/

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.channel;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
import com.jeequan.jeepay.pay.rqrs.transfer.TransferOrderRQ;
/**
* 转账接口
*
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021/8/11 13:59
*/
public interface ITransferService {
/* 获取到接口code **/
String getIfCode();
/** 是否支持该支付入账方式 */
boolean isSupport(String entryType);
/** 前置检查如参数等信息是否符合要求, 返回错误信息或直接抛出异常即可 */
String preCheck(TransferOrderRQ bizRQ, TransferOrder refundOrder);
/** 调起退款接口,并响应数据; 内部处理普通商户和服务商模式 **/
ChannelRetMsg transfer(TransferOrderRQ bizRQ, TransferOrder refundOrder, MchAppConfigContext mchAppConfigContext) throws Exception;
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.channel.wxpay;
import com.github.binarywang.wxpay.bean.entpay.EntPayRequest;
import com.github.binarywang.wxpay.bean.entpay.EntPayResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.jeequan.jeepay.core.constants.CS;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.core.model.params.wxpay.WxpayNormalMchParams;
import com.jeequan.jeepay.pay.channel.ITransferService;
import com.jeequan.jeepay.pay.channel.wxpay.kits.WxpayKit;
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
import com.jeequan.jeepay.pay.rqrs.transfer.TransferOrderRQ;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
/**
* 转账接口 微信官方
*
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021/8/11 14:05
*/
@Slf4j
@Service
public class WxpayTransferService implements ITransferService {
@Override
public String getIfCode() {
return CS.IF_CODE.WXPAY;
}
@Override
public boolean isSupport(String entryType) {
// 微信仅支持 零钱 银行卡入账方式
if(TransferOrder.ENTRY_WX_CASH.equals(entryType) || TransferOrder.ENTRY_BANK_CARD.equals(entryType)){
return true;
}
return false;
}
@Override
public String preCheck(TransferOrderRQ bizRQ, TransferOrder refundOrder) {
return null;
}
@Override
public ChannelRetMsg transfer(TransferOrderRQ bizRQ, TransferOrder transferOrder, MchAppConfigContext mchAppConfigContext){
try {
WxpayNormalMchParams params = mchAppConfigContext.getNormalMchParamsByIfCode(getIfCode(), WxpayNormalMchParams.class);
EntPayRequest request = new EntPayRequest();
request.setMchAppid(params.getAppId()); // 商户账号appid
request.setMchId(params.getMchId()); //商户号
request.setPartnerTradeNo(transferOrder.getTransferId()); //商户订单号
request.setOpenid(transferOrder.getAccountNo()); //openid
request.setAmount(transferOrder.getAmount().intValue()); //付款金额单位为分
request.setSpbillCreateIp(transferOrder.getClientIp());
request.setDescription(transferOrder.getTransferDesc()); //付款备注
if(StringUtils.isNotEmpty(transferOrder.getAccountName())){
request.setReUserName(transferOrder.getAccountName());
request.setCheckName("FORCE_CHECK");
}else{
request.setCheckName("NO_CHECK");
}
EntPayResult entPayResult = mchAppConfigContext.getWxServiceWrapper().getWxPayService().getEntPayService().entPay(request);
// SUCCESS/FAIL注意当状态为FAIL时存在业务结果未明确的情况如果状态为FAIL请务必关注错误代码err_code字段通过查询接口确认此次付款的结果
if("SUCCESS".equalsIgnoreCase(entPayResult.getResultCode())){
return ChannelRetMsg.confirmSuccess(entPayResult.getPaymentNo());
}else{
return ChannelRetMsg.waiting();
}
} catch (WxPayException e) {
//出现未明确的错误码时SYSTEMERROR等请务必用原商户订单号重试或通过查询接口确认此次付款的结果
if("SYSTEMERROR".equalsIgnoreCase(e.getErrCode())){
return ChannelRetMsg.waiting();
}
return ChannelRetMsg.confirmFail(null,
WxpayKit.appendErrCode(e.getReturnMsg(), e.getErrCode()),
WxpayKit.appendErrMsg(e.getReturnMsg(), StringUtils.defaultIfEmpty(e.getErrCodeDes(), e.getCustomErrorMsg())));
} catch (Exception e) {
log.error("转账异常:", e);
return ChannelRetMsg.waiting();
}
}
}

View File

@ -0,0 +1,248 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.ctrl.transfer;
import com.jeequan.jeepay.core.entity.MchApp;
import com.jeequan.jeepay.core.entity.MchInfo;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.core.exception.BizException;
import com.jeequan.jeepay.core.model.ApiRes;
import com.jeequan.jeepay.core.utils.SeqKit;
import com.jeequan.jeepay.core.utils.SpringBeansUtil;
import com.jeequan.jeepay.core.utils.StringKit;
import com.jeequan.jeepay.pay.channel.ITransferService;
import com.jeequan.jeepay.pay.ctrl.ApiController;
import com.jeequan.jeepay.pay.exception.ChannelException;
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
import com.jeequan.jeepay.pay.rqrs.transfer.TransferOrderRQ;
import com.jeequan.jeepay.pay.rqrs.transfer.TransferOrderRS;
import com.jeequan.jeepay.pay.service.ConfigContextService;
import com.jeequan.jeepay.pay.service.PayMchNotifyService;
import com.jeequan.jeepay.service.impl.PayInterfaceConfigService;
import com.jeequan.jeepay.service.impl.TransferOrderService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* 转账接口
*
* @author terrfly
* @site https://www.jeepay.vip
* @date 2021/8/11 11:07
*/
@Slf4j
@RestController
public class TransferOrderController extends ApiController {
@Autowired private ConfigContextService configContextService;
@Autowired private TransferOrderService transferOrderService;
@Autowired private PayInterfaceConfigService payInterfaceConfigService;
@Autowired private PayMchNotifyService payMchNotifyService;
/**
* 转账
* **/
@PostMapping("/api/transferOrder")
public ApiRes transferOrder(){
TransferOrder transferOrder = null;
//获取参数 & 验签
TransferOrderRQ bizRQ = getRQByWithMchSign(TransferOrderRQ.class);
try {
String mchNo = bizRQ.getMchNo();
String appId = bizRQ.getAppId();
String ifCode = bizRQ.getIfCode();
// 商户订单号是否重复
if(transferOrderService.count(TransferOrder.gw().eq(TransferOrder::getMchNo, mchNo).eq(TransferOrder::getMchOrderNo, bizRQ.getMchOrderNo())) > 0){
throw new BizException("商户订单["+bizRQ.getMchOrderNo()+"]已存在");
}
if(StringUtils.isNotEmpty(bizRQ.getNotifyUrl()) && !StringKit.isAvailableUrl(bizRQ.getNotifyUrl())){
throw new BizException("异步通知地址协议仅支持http:// 或 https:// !");
}
// 商户配置信息
MchAppConfigContext mchAppConfigContext = configContextService.getMchAppConfigContext(mchNo, appId);
if(mchAppConfigContext == null){
throw new BizException("获取商户应用信息失败");
}
MchInfo mchInfo = mchAppConfigContext.getMchInfo();
MchApp mchApp = mchAppConfigContext.getMchApp();
// 是否已正确配置
if(!payInterfaceConfigService.mchAppHasAvailableIfCode(appId, ifCode)){
throw new BizException("应用未开通此接口配置!");
}
ITransferService transferService = SpringBeansUtil.getBean(ifCode + "TransferService", ITransferService.class);
if(transferService == null){
throw new BizException("无此转账通道接口");
}
if(!transferService.isSupport(bizRQ.getEntryType())){
throw new BizException("该接口不支持该入账方式");
}
transferOrder = genTransferOrder(bizRQ, mchInfo, mchApp, ifCode);
//预先校验
String errMsg = transferService.preCheck(bizRQ, transferOrder);
if(StringUtils.isNotEmpty(errMsg)){
throw new BizException(errMsg);
}
// 入库
transferOrderService.save(transferOrder);
// 调起上游接口
ChannelRetMsg channelRetMsg = transferService.transfer(bizRQ, transferOrder, mchAppConfigContext);
//处理退款单状态
this.processChannelMsg(channelRetMsg, transferOrder);
TransferOrderRS bizRes = TransferOrderRS.buildByRecord(transferOrder);
return ApiRes.okWithSign(bizRes, mchApp.getAppSecret());
} catch (BizException e) {
return ApiRes.customFail(e.getMessage());
} catch (ChannelException e) {
//处理上游返回数据
this.processChannelMsg(e.getChannelRetMsg(), transferOrder);
if(e.getChannelRetMsg().getChannelState() == ChannelRetMsg.ChannelState.SYS_ERROR ){
return ApiRes.customFail(e.getMessage());
}
TransferOrderRS bizRes = TransferOrderRS.buildByRecord(transferOrder);
return ApiRes.okWithSign(bizRes, configContextService.getMchAppConfigContext(bizRQ.getMchNo(), bizRQ.getAppId()).getMchApp().getAppSecret());
} catch (Exception e) {
log.error("系统异常:{}", e);
return ApiRes.customFail("系统异常");
}
}
private TransferOrder genTransferOrder(TransferOrderRQ rq, MchInfo mchInfo, MchApp mchApp, String ifCode){
TransferOrder transferOrder = new TransferOrder();
transferOrder.setTransferId(SeqKit.genTransferId()); //生成转账订单号
transferOrder.setMchNo(mchInfo.getMchNo()); //商户号
transferOrder.setIsvNo(mchInfo.getIsvNo()); //服务商号
transferOrder.setAppId(mchApp.getAppId()); //商户应用appId
transferOrder.setMchName(mchInfo.getMchShortName()); //商户名称简称
transferOrder.setMchType(mchInfo.getType()); //商户类型
transferOrder.setMchOrderNo(rq.getMchOrderNo()); //商户订单号
transferOrder.setIfCode(ifCode); //接口代码
transferOrder.setEntryType(rq.getEntryType()); //入账方式
transferOrder.setAmount(rq.getAmount()); //订单金额
transferOrder.setCurrency(rq.getCurrency()); //币种
transferOrder.setClientIp(StringUtils.defaultIfEmpty(rq.getClientIp(), getClientIp())); //客户端IP
transferOrder.setState(TransferOrder.STATE_INIT); //订单状态, 默认订单生成状态
transferOrder.setAccountNo(rq.getAccountNo()); //收款账号
transferOrder.setAccountName(rq.getAccountName()); //账户姓名
transferOrder.setBankName(rq.getBankName()); //银行名称
transferOrder.setTransferDesc(rq.getTransferDesc()); //转账备注
transferOrder.setExtParam(rq.getExtParam()); //商户扩展参数
transferOrder.setNotifyUrl(rq.getNotifyUrl()); //异步通知地址
transferOrder.setCreatedAt(new Date()); //订单创建时间
return transferOrder;
}
/**
* 处理返回的渠道信息并更新订单状态
* TransferOrder将对部分信息进行 赋值操作
* **/
private void processChannelMsg(ChannelRetMsg channelRetMsg, TransferOrder transferOrder){
//对象为空 || 上游返回状态为空 则无需操作
if(channelRetMsg == null || channelRetMsg.getChannelState() == null){
return ;
}
String transferId = transferOrder.getTransferId();
//明确成功
if(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS == channelRetMsg.getChannelState()) {
this.updateInitOrderStateThrowException(TransferOrder.STATE_SUCCESS, transferOrder, channelRetMsg);
payMchNotifyService.transferOrderNotify(transferOrder);
//明确失败
}else if(ChannelRetMsg.ChannelState.CONFIRM_FAIL == channelRetMsg.getChannelState()) {
this.updateInitOrderStateThrowException(TransferOrder.STATE_FAIL, transferOrder, channelRetMsg);
payMchNotifyService.transferOrderNotify(transferOrder);
// 上游处理中 || 未知 || 上游接口返回异常 订单为支付中状态
}else if( ChannelRetMsg.ChannelState.WAITING == channelRetMsg.getChannelState() ||
ChannelRetMsg.ChannelState.UNKNOWN == channelRetMsg.getChannelState() ||
ChannelRetMsg.ChannelState.API_RET_ERROR == channelRetMsg.getChannelState()
){
this.updateInitOrderStateThrowException(TransferOrder.STATE_ING, transferOrder, channelRetMsg);
// 系统异常 订单不再处理 生成状态
}else if( ChannelRetMsg.ChannelState.SYS_ERROR == channelRetMsg.getChannelState()){
}else{
throw new BizException("ChannelState 返回异常!");
}
}
/** 更新订单状态 --》 订单生成--》 其他状态 (向外抛出异常) **/
private void updateInitOrderStateThrowException(byte orderState, TransferOrder transferOrder, ChannelRetMsg channelRetMsg){
transferOrder.setState(orderState);
transferOrder.setChannelOrderNo(channelRetMsg.getChannelOrderId());
transferOrder.setErrCode(channelRetMsg.getChannelErrCode());
transferOrder.setErrMsg(channelRetMsg.getChannelErrMsg());
boolean isSuccess = transferOrderService.updateInit2Ing(transferOrder.getTransferId());
if(!isSuccess){
throw new BizException("更新转账订单异常!");
}
isSuccess = transferOrderService.updateIng2SuccessOrFail(transferOrder.getTransferId(), transferOrder.getState(),
channelRetMsg.getChannelOrderId(), channelRetMsg.getChannelErrCode(), channelRetMsg.getChannelErrMsg());
if(!isSuccess){
throw new BizException("更新转账订单异常!");
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.rqrs.transfer;
import com.jeequan.jeepay.pay.rqrs.AbstractMchAppRQ;
import lombok.Data;
/*
* 查询转账单请求参数对象
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/6/17 14:07
*/
@Data
public class QueryTransferOrderRQ extends AbstractMchAppRQ {
/** 商户转账单号 **/
private String mchRefundNo;
/** 支付系统转账单号 **/
private String transferId;
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.rqrs.transfer;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.pay.rqrs.AbstractRS;
import lombok.Data;
import org.springframework.beans.BeanUtils;
/*
* 查询转账订单 响应参数
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/6/17 14:08
*/
@Data
public class QueryTransferOrderRS extends AbstractRS {
/**
* 转账订单号
*/
private String transferId;
/**
* 商户号
*/
private String mchNo;
/**
* 应用ID
*/
private String appId;
/**
* 商户订单号
*/
private String mchOrderNo;
/**
* 支付接口代码
*/
private String ifCode;
/**
* 入账方式 WX_CASH-微信零钱; ALIPAY_CASH-支付宝转账; BANK_CARD-银行卡
*/
private String entryType;
/**
* 转账金额,单位分
*/
private Long amount;
/**
* 三位货币代码,人民币:cny
*/
private String currency;
/**
* 收款账号
*/
private String accountNo;
/**
* 收款人姓名
*/
private String accountName;
/**
* 收款人开户行名称
*/
private String bankName;
/**
* 转账备注信息
*/
private String transferDesc;
/**
* 支付状态: 0-订单生成, 1-转账中, 2-转账成功, 3-转账失败, 4-订单关闭
*/
private Byte state;
/**
* 特定渠道发起额外参数
*/
private String channelExtra;
/**
* 渠道订单号
*/
private String channelOrderNo;
/**
* 渠道支付错误码
*/
private String errCode;
/**
* 渠道支付错误描述
*/
private String errMsg;
/**
* 商户扩展参数
*/
private String extParam;
/**
* 转账成功时间
*/
private Long successTime;
/**
* 创建时间
*/
private Long createdAt;
public static QueryTransferOrderRS buildByRecord(TransferOrder record){
if(record == null){
return null;
}
QueryTransferOrderRS result = new QueryTransferOrderRS();
BeanUtils.copyProperties(record, result);
result.setSuccessTime(record.getSuccessTime() == null ? null : record.getSuccessTime().getTime());
result.setCreatedAt(record.getCreatedAt() == null ? null : record.getCreatedAt().getTime());
return result;
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.rqrs.transfer;
import com.jeequan.jeepay.pay.rqrs.AbstractMchAppRQ;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/*
* 申请转账 请求参数
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/8/10 11:31
*/
@Data
public class TransferOrderRQ extends AbstractMchAppRQ {
/** 商户订单号 **/
@NotBlank(message="商户订单号不能为空")
private String mchOrderNo;
/** 支付接口代码 **/
@NotBlank(message="支付接口代码不能为空")
private String ifCode;
/** 入账方式 **/
@NotBlank(message="入账方式不能为空")
private String entryType;
/** 支付金额, 单位:分 **/
@NotNull(message="转账金额不能为空")
@Min(value = 1, message = "转账金额不能小于1分")
private Long amount;
/** 货币代码 **/
@NotBlank(message="货币代码不能为空")
private String currency;
/** 收款账号 **/
@NotBlank(message="收款账号不能为空")
private String accountNo;
/** 收款人姓名 **/
private String accountName;
/** 收款人开户行名称 **/
private String bankName;
/** 客户端IP地址 **/
private String clientIp;
/** 转账备注信息 **/
@NotBlank(message="转账备注信息不能为空")
private String transferDesc;
/** 异步通知地址 **/
private String notifyUrl;
/** 特定渠道发起额外参数 **/
private String channelExtra;
/** 商户扩展参数 **/
private String extParam;
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.pay.rqrs.transfer;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.pay.rqrs.AbstractRS;
import lombok.Data;
import org.springframework.beans.BeanUtils;
/*
* 创建订单(统一订单) 响应参数
*
* @author terrfly
* @site https://www.jeequan.com
* @date 2021/6/8 17:34
*/
@Data
public class TransferOrderRS extends AbstractRS {
/** 转账单号 **/
private String transferId;
/** 商户单号 **/
private String mchOrderNo;
/** 转账金额 **/
private Long amount;
/**
* 收款账号
*/
private String accountNo;
/**
* 收款人姓名
*/
private String accountName;
/**
* 收款人开户行名称
*/
private String bankName;
/** 状态 **/
private Byte state;
/** 渠道退款单号 **/
private String channelOrderNo;
/** 渠道返回错误代码 **/
private String errCode;
/** 渠道返回错误信息 **/
private String errMsg;
public static TransferOrderRS buildByRecord(TransferOrder record){
if(record == null){
return null;
}
TransferOrderRS result = new TransferOrderRS();
BeanUtils.copyProperties(record, result);
return result;
}
}

View File

@ -21,10 +21,12 @@ import com.jeequan.jeepay.components.mq.vender.IMQSender;
import com.jeequan.jeepay.core.entity.MchNotifyRecord;
import com.jeequan.jeepay.core.entity.PayOrder;
import com.jeequan.jeepay.core.entity.RefundOrder;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.core.utils.JeepayKit;
import com.jeequan.jeepay.core.utils.StringKit;
import com.jeequan.jeepay.pay.rqrs.payorder.QueryPayOrderRS;
import com.jeequan.jeepay.pay.rqrs.refund.QueryRefundOrderRS;
import com.jeequan.jeepay.pay.rqrs.transfer.QueryTransferOrderRS;
import com.jeequan.jeepay.service.impl.MchNotifyRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@ -138,6 +140,51 @@ public class PayMchNotifyService {
}
/** 商户通知信息,转账订单的通知接口 **/
public void transferOrderNotify(TransferOrder dbTransferOrder){
try {
// 通知地址为空
if(StringUtils.isEmpty(dbTransferOrder.getNotifyUrl())){
return ;
}
//获取到通知对象
MchNotifyRecord mchNotifyRecord = mchNotifyRecordService.findByTransferOrder(dbTransferOrder.getTransferId());
if(mchNotifyRecord != null){
log.info("当前已存在通知消息, 不再发送。");
return ;
}
//商户app私钥
String appSecret = configContextService.getMchAppConfigContext(dbTransferOrder.getMchNo(), dbTransferOrder.getAppId()).getMchApp().getAppSecret();
// 封装通知url
String notifyUrl = createNotifyUrl(dbTransferOrder, appSecret);
mchNotifyRecord = new MchNotifyRecord();
mchNotifyRecord.setOrderId(dbTransferOrder.getTransferId());
mchNotifyRecord.setOrderType(MchNotifyRecord.TYPE_TRANSFER_ORDER);
mchNotifyRecord.setMchNo(dbTransferOrder.getMchNo());
mchNotifyRecord.setMchOrderNo(dbTransferOrder.getMchOrderNo()); //商户订单号
mchNotifyRecord.setIsvNo(dbTransferOrder.getIsvNo());
mchNotifyRecord.setAppId(dbTransferOrder.getAppId());
mchNotifyRecord.setNotifyUrl(notifyUrl);
mchNotifyRecord.setResResult("");
mchNotifyRecord.setNotifyCount(0);
mchNotifyRecord.setState(MchNotifyRecord.STATE_ING); // 通知中
mchNotifyRecordService.save(mchNotifyRecord);
//推送到MQ
Long notifyId = mchNotifyRecord.getNotifyId();
mqSender.send(PayOrderMchNotifyMQ.build(notifyId));
} catch (Exception e) {
log.error("推送失败!", e);
}
}
/**
* 创建响应URL
*/
@ -172,6 +219,23 @@ public class PayMchNotifyService {
}
/**
* 创建响应URL
*/
public String createNotifyUrl(TransferOrder transferOrder, String appSecret) {
QueryTransferOrderRS rs = QueryTransferOrderRS.buildByRecord(transferOrder);
JSONObject jsonObject = (JSONObject)JSONObject.toJSON(rs);
jsonObject.put("reqTime", System.currentTimeMillis()); //添加请求时间
// 报文签名
jsonObject.put("sign", JeepayKit.getSign(jsonObject, appSecret));
// 生成通知
return StringKit.appendUrlQuery(transferOrder.getNotifyUrl(), jsonObject);
}
/**
* 创建响应URL
*/

View File

@ -49,6 +49,11 @@ public class MchNotifyRecordService extends ServiceImpl<MchNotifyRecordMapper, M
return findByOrderAndType(orderId, MchNotifyRecord.TYPE_REFUND_ORDER);
}
/** 查询退款订单订单 */
public MchNotifyRecord findByTransferOrder(String transferId){
return findByOrderAndType(transferId, MchNotifyRecord.TYPE_TRANSFER_ORDER);
}
public Integer updateNotifyResult(Long notifyId, Byte state, String resResult){
return baseMapper.updateNotifyResult(notifyId, state, resResult);
}

View File

@ -19,10 +19,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jeequan.jeepay.core.constants.ApiCodeEnum;
import com.jeequan.jeepay.core.constants.CS;
import com.jeequan.jeepay.core.entity.MchApp;
import com.jeequan.jeepay.core.entity.MchInfo;
import com.jeequan.jeepay.core.entity.PayInterfaceConfig;
import com.jeequan.jeepay.core.entity.PayInterfaceDefine;
import com.jeequan.jeepay.core.entity.*;
import com.jeequan.jeepay.core.exception.BizException;
import com.jeequan.jeepay.service.mapper.PayInterfaceConfigMapper;
import org.springframework.beans.factory.annotation.Autowired;
@ -155,4 +152,21 @@ public class PayInterfaceConfigService extends ServiceImpl<PayInterfaceConfigMap
}
return defineList;
}
/** 查询商户app使用已正确配置了通道信息 */
public boolean mchAppHasAvailableIfCode(String appId, String ifCode){
return this.count(
PayInterfaceConfig.gw()
.eq(PayInterfaceConfig::getIfCode, ifCode)
.eq(PayInterfaceConfig::getState, CS.PUB_USABLE)
.eq(PayInterfaceConfig::getInfoId, appId)
.eq(PayInterfaceConfig::getInfoType, CS.INFO_TYPE_MCH_APP)
) > 0;
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jeequan.jeepay.core.entity.TransferOrder;
import com.jeequan.jeepay.service.mapper.TransferOrderMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* <p>
* 转账订单表 服务实现类
* </p>
*
* @author [mybatis plus generator]
* @since 2021-08-11
*/
@Service
public class TransferOrderService extends ServiceImpl<TransferOrderMapper, TransferOrder> {
/** 更新转账订单状态 【转账订单生成】 --》 【转账中】 **/
public boolean updateInit2Ing(String transferId){
TransferOrder updateRecord = new TransferOrder();
updateRecord.setState(TransferOrder.STATE_ING);
return update(updateRecord, new LambdaUpdateWrapper<TransferOrder>()
.eq(TransferOrder::getTransferId, transferId).eq(TransferOrder::getState, TransferOrder.STATE_INIT));
}
/** 更新转账订单状态 【转账中】 --》 【转账成功】 **/
@Transactional
public boolean updateIng2Success(String transferId, String channelOrderNo){
TransferOrder updateRecord = new TransferOrder();
updateRecord.setState(TransferOrder.STATE_SUCCESS);
updateRecord.setChannelOrderNo(channelOrderNo);
updateRecord.setSuccessTime(new Date());
//更新转账订单表数据
if(! update(updateRecord, new LambdaUpdateWrapper<TransferOrder>()
.eq(TransferOrder::getTransferId, transferId).eq(TransferOrder::getState, TransferOrder.STATE_ING))
){
return false;
}
return true;
}
/** 更新转账订单状态 【转账中】 --》 【转账失败】 **/
@Transactional
public boolean updateIng2Fail(String transferId, String channelOrderNo, String channelErrCode, String channelErrMsg){
TransferOrder updateRecord = new TransferOrder();
updateRecord.setState(TransferOrder.STATE_FAIL);
updateRecord.setErrCode(channelErrCode);
updateRecord.setErrMsg(channelErrMsg);
updateRecord.setChannelOrderNo(channelOrderNo);
return update(updateRecord, new LambdaUpdateWrapper<TransferOrder>()
.eq(TransferOrder::getTransferId, transferId).eq(TransferOrder::getState, TransferOrder.STATE_ING));
}
/** 更新转账订单状态 【转账中】 --》 【转账成功/转账失败】 **/
@Transactional
public boolean updateIng2SuccessOrFail(String transferId, Byte updateState, String channelOrderNo, String channelErrCode, String channelErrMsg){
if(updateState == TransferOrder.STATE_ING){
return true;
}else if(updateState == TransferOrder.STATE_SUCCESS){
return updateIng2Success(transferId, channelOrderNo);
}else if(updateState == TransferOrder.STATE_FAIL){
return updateIng2Fail(transferId, channelOrderNo, channelErrCode, channelErrMsg);
}
return false;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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 com.jeequan.jeepay.service.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jeequan.jeepay.core.entity.TransferOrder;
/**
* <p>
* 转账订单表 Mapper 接口
* </p>
*
* @author [mybatis plus generator]
* @since 2021-08-11
*/
public interface TransferOrderMapper extends BaseMapper<TransferOrder> {
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jeequan.jeepay.service.mapper.TransferOrderMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.jeequan.jeepay.core.entity.TransferOrder">
<id column="transfer_id" property="transferId" />
<result column="mch_no" property="mchNo" />
<result column="isv_no" property="isvNo" />
<result column="app_id" property="appId" />
<result column="mch_name" property="mchName" />
<result column="mch_type" property="mchType" />
<result column="mch_order_no" property="mchOrderNo" />
<result column="if_code" property="ifCode" />
<result column="entry_type" property="entryType" />
<result column="amount" property="amount" />
<result column="currency" property="currency" />
<result column="account_no" property="accountNo" />
<result column="account_name" property="accountName" />
<result column="bank_name" property="bankName" />
<result column="transfer_desc" property="transferDesc" />
<result column="client_ip" property="clientIp" />
<result column="state" property="state" />
<result column="notify_state" property="notifyState" />
<result column="channel_extra" property="channelExtra" />
<result column="channel_order_no" property="channelOrderNo" />
<result column="err_code" property="errCode" />
<result column="err_msg" property="errMsg" />
<result column="ext_param" property="extParam" />
<result column="notify_url" property="notifyUrl" />
<result column="success_time" property="successTime" />
<result column="created_at" property="createdAt" />
<result column="updated_at" property="updatedAt" />
</resultMap>
</mapper>