diff --git a/_doc/Task.md b/_doc/Task.md index ed8f520b2..0f7c51f82 100644 --- a/_doc/Task.md +++ b/_doc/Task.md @@ -15,6 +15,7 @@ - [ ] 增加定时同步退款中的退款订单 2.0.x 版本内容 +- [ ] 首页驾驶舱功能: 各通道收入和支付情况 - [ ] 增加各类日志记录,例如钱包的各项操作 - [ ] 支付流程涉及异步支付时, 更换支付方式需要控制预防客户重复付款 - [ ] 增加撤销功能,用于处理线下支付订单的情况 diff --git a/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayConfigController.java b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayConfigController.java new file mode 100644 index 000000000..d18e146a0 --- /dev/null +++ b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayConfigController.java @@ -0,0 +1,57 @@ +package cn.bootx.platform.daxpay.admin.controller.channel; + +import cn.bootx.platform.common.core.rest.Res; +import cn.bootx.platform.common.core.rest.ResResult; +import cn.bootx.platform.common.core.rest.dto.LabelValue; +import cn.bootx.platform.daxpay.service.core.channel.union.service.UnionPayConfigService; +import cn.bootx.platform.daxpay.service.dto.channel.union.UnionPayConfigDto; +import cn.bootx.platform.daxpay.service.param.channel.union.UnionPayConfigParam; +import cn.hutool.core.codec.Base64; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 云闪付配置 + * @author xxm + * @since 2024/3/9 + */ +@Tag(name = "云闪付配置") +@RestController +@RequestMapping("/union/pay/config") +@RequiredArgsConstructor +public class UnionPayConfigController { + + private final UnionPayConfigService unionPayConfigService; + + @Operation(summary = "获取配置") + @GetMapping("/getConfig") + public ResResult getConfig() { + return Res.ok(unionPayConfigService.getConfig().toDto()); + } + + @Operation(summary = "更新") + @PostMapping("/update") + public ResResult update(@RequestBody UnionPayConfigParam param) { + unionPayConfigService.update(param); + return Res.ok(); + } + + @Operation(summary = "支持的支付方式") + @GetMapping("/findPayWays") + public ResResult> findPayWays() { + return Res.ok(unionPayConfigService.findPayWays()); + } + + @SneakyThrows + @Operation(summary = "读取证书文件内容") + @PostMapping("/toBase64") + public ResResult toBase64(MultipartFile file){ + return Res.ok(Base64.encode(file.getBytes())); + } +} diff --git a/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayController.java b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayController.java new file mode 100644 index 000000000..f26487898 --- /dev/null +++ b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/admin/controller/channel/UnionPayController.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.admin.controller.channel; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 云闪付控制器 + * @author xxm + * @since 2024/3/9 + */ +@Tag(name = "云闪付控制器") +@RestController +@RequestMapping("/union/pay") +@RequiredArgsConstructor +public class UnionPayController { +} diff --git a/daxpay-single/daxpay-single-core/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatusEnum.java b/daxpay-single/daxpay-single-core/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatusEnum.java index ac325d7ee..f41170b9d 100644 --- a/daxpay-single/daxpay-single-core/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatusEnum.java +++ b/daxpay-single/daxpay-single-core/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatusEnum.java @@ -28,7 +28,7 @@ public enum PaySyncStatusEnum { * 所以查询为了区分,增加一个未知的状态, 用于处理这种特殊情况, 然后根据业务需要,关闭订单或者进行其他操作 */ NOT_FOUND_UNKNOWN("pay_not_found_unknown","交易不存在(特殊)"), - /** 不属于网关同步过来的状态, 需要手动设置, 处理本地订单到了超时时间, 但是网关和本地都未关闭, 需要触发关闭相关处理 */ + /** 本地订单到了超时时间, 但是网关和本地都未关闭, 需要触发关闭相关处理, 可以进行手动设置 */ TIMEOUT("pay_timeout", "支付超时"); /** 编码 */ diff --git a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/gateway/controller/PayReturnController.java b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/gateway/controller/PayReturnController.java index 524f9ed75..2ce224af5 100644 --- a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/gateway/controller/PayReturnController.java +++ b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/gateway/controller/PayReturnController.java @@ -36,4 +36,10 @@ public class PayReturnController { public ModelAndView wechat(){ return null; } + + @Operation(summary = "云闪付同步通知") + @GetMapping("/union") + public ModelAndView union(){ + return null; + } } diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayCode.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayCode.java index 81e245e52..1eb44a0d9 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayCode.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayCode.java @@ -18,26 +18,26 @@ public interface UnionPayCode { String RESULT_CODE = "result_code"; /** 网关订单号 */ - String TRANSACTION_ID = "transaction_id"; + String QUERY_ID = "queryId"; /** 第三方订单号 */ - String OUT_TRANSACTION_ID = "out_transaction_id"; + String ORDER_ID = "orderId"; /** 退款ID */ String REFUND_ID = "refund_id"; /** - * 支付完成时间 + * 订单发送时间 * 格式: yyyyMMddHHmmss */ - String TIME_END = "time_end"; + String TXN_TIME = "txnTime"; /** 支付结果 */ String PAY_RESULT = "pay_result"; /** 总金额 */ - String TOTAL_FEE = "total_fee"; + String TOTAL_FEE = "settleAmt"; /** 交易状态 */ String TRADE_STATE = "trade_state"; diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayWay.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayWay.java new file mode 100644 index 000000000..9f9ebb7cd --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/code/UnionPayWay.java @@ -0,0 +1,39 @@ +package cn.bootx.platform.daxpay.service.code; + +import cn.bootx.platform.daxpay.code.PayWayEnum; +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 云闪付支付方式 + * @author xxm + * @since 2024/3/9 + */ +@UtilityClass +public class UnionPayWay { + + // 支付方式 + private static final List PAY_WAYS = Arrays.asList(PayWayEnum.WAP, PayWayEnum.APP, PayWayEnum.WEB, + PayWayEnum.QRCODE, PayWayEnum.BARCODE); + + /** + * 根据编码获取 + */ + public PayWayEnum findByCode(String code) { + return PAY_WAYS.stream() + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 获取支持的支付方式 + */ + public List getPayWays() { + return PAY_WAYS; + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/entity/UnionPayConfig.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/entity/UnionPayConfig.java index 0952a8ba5..44c94e90e 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/entity/UnionPayConfig.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/entity/UnionPayConfig.java @@ -53,7 +53,7 @@ public class UnionPayConfig extends MpBaseEntity implements EntityBaseFunction callbackParam = callbackInfo.getCallbackParam(); // 网关订单号 - callbackInfo.setGatewayOrderNo(callbackParam.get(TRANSACTION_ID)); + callbackInfo.setGatewayOrderNo(callbackParam.get(QUERY_ID)); // 支付订单ID - callbackInfo.setOrderId(Long.valueOf(callbackParam.get(OUT_TRANSACTION_ID))); + callbackInfo.setOrderId(Long.valueOf(callbackParam.get(ORDER_ID))); // 支付结果 PayStatusEnum payStatus = WxPayKit.codeIsOk(callbackParam.get(PAY_RESULT)) ? PayStatusEnum.SUCCESS : PayStatusEnum.FAIL; callbackInfo.setGatewayStatus(payStatus.getCode()); // 支付金额 callbackInfo.setAmount(callbackParam.get(TOTAL_FEE)); - String timeEnd = callbackParam.get(TIME_END); + String timeEnd = callbackParam.get(TXN_TIME); if (StrUtil.isNotBlank(timeEnd)) { LocalDateTime time = LocalDateTimeUtil.parse(timeEnd, DatePattern.PURE_DATETIME_PATTERN); callbackInfo.setFinishTime(time); diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayCloseService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayCloseService.java index 42b1b7e93..b28f1b2f5 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayCloseService.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayCloseService.java @@ -1,20 +1,12 @@ package cn.bootx.platform.daxpay.service.core.channel.union.service; import cn.bootx.platform.daxpay.exception.pay.PayFailureException; -import cn.bootx.platform.daxpay.service.code.UnionPayCode; import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder; import cn.bootx.platform.daxpay.service.sdk.union.api.UnionPayKit; -import cn.hutool.core.util.StrUtil; -import com.egzosn.pay.common.bean.AssistOrder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.Map; -import java.util.Objects; - -import static cn.bootx.platform.daxpay.service.code.UnionPayCode.*; - /** * 云闪付支付关闭 * @author xxm @@ -29,30 +21,7 @@ public class UnionPayCloseService { * 关闭订单 */ public void close(PayOrder payOrder, UnionPayKit unionPayKit) { - AssistOrder closeOrder = new AssistOrder(); - - closeOrder.setOutTradeNo(String.valueOf(payOrder.getId())); - - Map result = unionPayKit.close(closeOrder); - + throw new PayFailureException("云闪付没有关闭订单功能!"); // this.verifyErrorMsg(result); } - - /** - * 验证错误信息 - */ - private void verifyErrorMsg(Map result) { - String status = result.get(UnionPayCode.STATUS); - String returnCode = result.get(UnionPayCode.RESULT_CODE); - - // 判断查询是否成功 - if (!(Objects.equals(SUCCESS, status) && Objects.equals(SUCCESS, returnCode))){ - String errorMsg = result.get(ERR_MSG); - if (StrUtil.isBlank(errorMsg)) { - errorMsg = result.get(MESSAGE); - } - log.error("订单关闭失败 {}", errorMsg); - throw new PayFailureException(errorMsg); - } - } } diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayConfigService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayConfigService.java index 74ab897b8..b96a48c14 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayConfigService.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPayConfigService.java @@ -4,14 +4,16 @@ import cn.bootx.platform.common.core.exception.DataNotExistException; import cn.bootx.platform.common.core.rest.dto.LabelValue; import cn.bootx.platform.daxpay.code.PayChannelEnum; import cn.bootx.platform.daxpay.exception.pay.PayFailureException; -import cn.bootx.platform.daxpay.service.code.AliPayWay; +import cn.bootx.platform.daxpay.service.code.UnionPayWay; import cn.bootx.platform.daxpay.service.core.channel.union.dao.UnionPayConfigManager; import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionPayConfig; import cn.bootx.platform.daxpay.service.core.system.config.service.PayChannelConfigService; -import cn.bootx.platform.daxpay.service.param.channel.alipay.AliPayConfigParam; +import cn.bootx.platform.daxpay.service.param.channel.union.UnionPayConfigParam; import cn.bootx.platform.daxpay.service.sdk.union.api.UnionPayKit; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.CharsetUtil; import com.egzosn.pay.common.bean.CertStoreType; import com.egzosn.pay.common.http.HttpConfigStorage; import com.egzosn.pay.union.api.UnionPayConfigStorage; @@ -20,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.ByteArrayInputStream; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -42,7 +45,7 @@ public class UnionPayConfigService { * 修改 */ @Transactional(rollbackFor = Exception.class) - public void update(AliPayConfigParam param) { + public void update(UnionPayConfigParam param) { UnionPayConfig unionPayConfig = unionPayConfigManager.findById(ID).orElseThrow(() -> new DataNotExistException("支付宝配置不存在")); // 启用或停用 if (!Objects.equals(param.getEnable(), unionPayConfig.getEnable())){ @@ -54,10 +57,10 @@ public class UnionPayConfigService { } /** - * 支付宝支持支付方式 + * 云闪付支持支付方式 */ public List findPayWays() { - return AliPayWay.getPayWays() + return UnionPayWay.getPayWays() .stream() .map(e -> new LabelValue(e.getName(),e.getCode())) .collect(Collectors.toList()); @@ -67,7 +70,7 @@ public class UnionPayConfigService { * 获取支付配置 */ public UnionPayConfig getConfig(){ - return unionPayConfigManager.findById(ID).orElseThrow(() -> new DataNotExistException("支付宝配置不存在")); + return unionPayConfigManager.findById(ID).orElseThrow(() -> new DataNotExistException("云闪付支付配置不存在")); } /** @@ -87,32 +90,35 @@ public class UnionPayConfigService { */ public UnionPayKit initPayService(UnionPayConfig config){ UnionPayConfigStorage unionPayConfigStorage = new UnionPayConfigStorage(); + unionPayConfigStorage.setInputCharset(CharsetUtil.UTF_8); + // 商户号 unionPayConfigStorage.setMerId(config.getMachId()); //是否为证书签名 unionPayConfigStorage.setCertSign(config.isCertSign()); - //中级证书 证书字符串信息 - unionPayConfigStorage.setAcpMiddleCert(config.getAcpMiddleCert()); - //根证书路径 证书字符串信息 - unionPayConfigStorage.setAcpRootCert(config.getAcpRootCert()); - // 私钥证书路径 证书字符串信息 - unionPayConfigStorage.setKeyPrivateCert(config.getKeyPrivateCert()); + //中级证书 流 + unionPayConfigStorage.setAcpMiddleCert(new ByteArrayInputStream(Base64.decode(config.getAcpMiddleCert()))); + //根证书 流 + unionPayConfigStorage.setAcpRootCert(new ByteArrayInputStream(Base64.decode(config.getAcpRootCert()))); + // 私钥证书 流 + unionPayConfigStorage.setKeyPrivateCert(new ByteArrayInputStream(Base64.decode(config.getKeyPrivateCert()))); + //私钥证书对应的密码 私钥证书对应的密码 unionPayConfigStorage.setKeyPrivateCertPwd(config.getKeyPrivateCertPwd()); //设置证书对应的存储方式,证书字符串信息 - unionPayConfigStorage.setCertStoreType(CertStoreType.STR); + unionPayConfigStorage.setCertStoreType(CertStoreType.INPUT_STREAM); // 回调地址 unionPayConfigStorage.setNotifyUrl(config.getNotifyUrl()); // 同步回调可不填 unionPayConfigStorage.setReturnUrl(config.getReturnUrl()); - unionPayConfigStorage.setSignType(config.signType); + unionPayConfigStorage.setSignType(config.getSignType()); //是否为测试账号,沙箱环境 unionPayConfigStorage.setTest(config.isSandbox()); // 网络请求配置 HttpConfigStorage httpConfigStorage = new HttpConfigStorage(); - httpConfigStorage.setCertStoreType(CertStoreType.STR); + httpConfigStorage.setCertStoreType(CertStoreType.INPUT_STREAM); //最大连接数 httpConfigStorage.setMaxTotal(20); //默认的每个路由的最大连接数 diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPaySyncService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPaySyncService.java index eabe590d5..605cdbddb 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPaySyncService.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/core/channel/union/service/UnionPaySyncService.java @@ -1,5 +1,6 @@ package cn.bootx.platform.daxpay.service.core.channel.union.service; +import cn.bootx.platform.common.core.util.LocalDateTimeUtil; import cn.bootx.platform.daxpay.code.PaySyncStatusEnum; import cn.bootx.platform.daxpay.code.RefundSyncStatusEnum; import cn.bootx.platform.daxpay.service.code.UnionPayCode; @@ -9,13 +10,18 @@ import cn.bootx.platform.daxpay.service.core.payment.sync.result.PayGatewaySyncR import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult; import cn.bootx.platform.daxpay.service.sdk.union.api.UnionPayKit; import cn.bootx.platform.daxpay.service.sdk.union.bean.UnionRefundOrder; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.egzosn.pay.common.bean.AssistOrder; +import com.egzosn.pay.common.bean.NoticeParams; +import com.egzosn.pay.union.bean.SDKConstants; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; @@ -39,33 +45,42 @@ public class UnionPaySyncService { AssistOrder query = new AssistOrder(); query.setOutTradeNo(String.valueOf(order.getId())); + Map result = unionPayKit.query(query); - syncResult.setSyncInfo(JSONUtil.toJsonStr(result)); + if (!unionPayKit.verify(new NoticeParams(result))) { + log.warn("查询云闪付订单验签失败:{}", result); + return syncResult.setErrorMsg("查询订单验签失败"); + } -// String status = result.get(STATUS); -// String returnCode = result.get(RESULT_CODE); -// -// // 判断查询是否成功 -// if (!(Objects.equals(SUCCESS, status) && Objects.equals(SUCCESS, returnCode))){ -// log.warn("查询云闪付订单失败:{}", result); -// return syncResult; -// } -// -// // 设置微信支付网关订单号 -// syncResult.setGatewayOrderNo(result.get(TRANSACTION_ID)); -// // 查询到订单的状态 -// String tradeStatus = result.get(TRADE_STATE); -// // 支付完成 -// if (Objects.equals(tradeStatus, SUCCESS)) { -// String timeEnd = result.get(TIME_END); -// LocalDateTime time = LocalDateTimeUtil.parse(timeEnd, DatePattern.PURE_DATETIME_PATTERN); -// return syncResult.setPayTime(time).setSyncStatus(PaySyncStatusEnum.SUCCESS); -// } -// // 待支付 -// if (Objects.equals(tradeStatus, TRADE_NOT_PAY)) { -// return syncResult.setSyncStatus(PaySyncStatusEnum.PROGRESS); -// } + // 查询失败 + String resultCode = MapUtil.getStr(result, SDKConstants.param_respCode); + if (!SDKConstants.OK_RESP_CODE.equals(resultCode)) { + log.warn("查询云闪付订单失败:{}", result); + return syncResult.setErrorMsg(MapUtil.getStr(result, SDKConstants.param_respMsg)); + } + String origRespCode = MapUtil.getStr(result, SDKConstants.param_origRespCode); + + // 查询流水号, 相当于网关订单号 + + + // 成功 + if (Objects.equals(origRespCode, SDKConstants.OK_RESP_CODE)) { + String queryId = MapUtil.getStr(result, QUERY_ID); + String timeEnd = MapUtil.getStr(result, TXN_TIME); + LocalDateTime time = LocalDateTimeUtil.parse(timeEnd, DatePattern.PURE_DATETIME_PATTERN); + return syncResult.setGatewayOrderNo(queryId).setPayTime(time).setSyncStatus(PaySyncStatusEnum.SUCCESS); + } + // 支付超时 交易不在受理时间范围内 + if (Objects.equals(origRespCode, "39")) { + return syncResult.setSyncStatus(PaySyncStatusEnum.TIMEOUT) + .setErrorMsg(MapUtil.getStr(result, SDKConstants.param_origRespMsg)); + } + + // 待支付 + if (Objects.equals(origRespCode, "05")) { + return syncResult.setSyncStatus(PaySyncStatusEnum.PROGRESS); + } // // // 已退款/退款中 // if (Objects.equals(tradeStatus, TRADE_REFUND)) { diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/dto/channel/union/UnionPayConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/dto/channel/union/UnionPayConfigDto.java index 5022835db..3b77c9041 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/dto/channel/union/UnionPayConfigDto.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/dto/channel/union/UnionPayConfigDto.java @@ -1,11 +1,15 @@ package cn.bootx.platform.daxpay.service.dto.channel.union; import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.daxpay.service.code.UnionPaySignTypeEnum; +import cn.bootx.platform.starter.data.perm.sensitive.SensitiveInfo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; +import java.util.List; + /** * @author xxm * @since 2022/3/11 @@ -16,4 +20,90 @@ import lombok.experimental.Accessors; @Schema(title = "云闪付配置") public class UnionPayConfigDto extends BaseDto { + /** 商户号 */ + @Schema(description = "商户号") + private String machId; + + /** 是否启用, 只影响支付和退款操作 */ + @Schema(description = "是否启用") + private Boolean enable; + + + /** + * 商户收款账号 + */ + @Schema(description = "商户收款账号") + private String seller; + + /** + * 签名类型 + * @see UnionPaySignTypeEnum + */ + @Schema(description = "签名类型") + public String signType; + + /** + * 是否为证书签名 + */ + @Schema(description = "是否为证书签名") + private boolean certSign; + + /** + * 应用私钥证书 字符串 + */ + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + @Schema(description = "应用私钥证书") + private String keyPrivateCert; + /** + * 私钥证书对应的密码 + */ + @SensitiveInfo(value = SensitiveInfo.SensitiveType.PASSWORD) + @Schema(description = "私钥证书对应的密码") + private String keyPrivateCertPwd; + + /** + * 中级证书 + */ + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + @Schema(description = "中级证书") + private String acpMiddleCert; + /** + * 根证书 + */ + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + @Schema(description = "根证书") + private String acpRootCert; + + /** 是否沙箱环境 */ + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + /** 支付网关地址 */ + @Schema(description = "支付网关地址") + private String serverUrl; + + /** + * 服务器异步通知页面路径, 需要填写本网关服务的地址, 不可以直接填写业务系统的地址 + * 1. 需http://或者https://格式的完整路径, + * 2. 不能加?id=123这类自定义参数,必须外网可以正常访问 + * 3. 消息顺序 银联网关 -> 本网关进行处理 -> 发送消息通知业务系统 + */ + @Schema(description = "异步通知路径") + private String notifyUrl; + + /** + * 服务器同步通知页面路径, 需要填写本网关服务的地址, 不可以直接填写业务系统的地址 + * 1. 需http://或者https://格式的完整路径, + * 2. 不能加?id=123这类自定义参数,必须外网可以正常访问 + * 3. 消息顺序 银联网关 -> 本网关进行处理 -> 重定向到业务系统中 + */ + @Schema(description = "同步通知页面路径") + private String returnUrl; + + /** 可用支付方式 */ + @Schema(description = "可用支付方式") + private List payWays; + + @Schema(description = "备注") + private String remark; } diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/param/channel/union/UnionPayConfigParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/param/channel/union/UnionPayConfigParam.java new file mode 100644 index 000000000..10cc47ef8 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/param/channel/union/UnionPayConfigParam.java @@ -0,0 +1,97 @@ +package cn.bootx.platform.daxpay.service.param.channel.union; + +import cn.bootx.platform.daxpay.service.code.UnionPaySignTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 云闪付支付配置参数 + * @author xxm + * @since 2024/3/9 + */ +@Data +@Accessors(chain = true) +@Schema(title = "云闪付支付配置参数") +public class UnionPayConfigParam { + + /** 商户号 */ + @Schema(description = "商户号") + private String machId; + + /** 是否启用, 只影响支付和退款操作 */ + @Schema(description = "是否启用") + private Boolean enable; + + /** + * 商户收款账号 + */ + @Schema(description = "商户收款账号") + private String seller; + + /** + * 签名类型 + * @see UnionPaySignTypeEnum + */ + @Schema(description = "签名类型") + public String signType; + + /** + * 是否为证书签名 + */ + @Schema(description = "是否为证书签名") + private boolean certSign; + + /** + * 应用私钥证书 + */ + @Schema(description = "应用私钥证书") + private String keyPrivateCert; + /** + * 私钥证书对应的密码 + */ + @Schema(description = "私钥证书对应的密码") + private String keyPrivateCertPwd; + + /** + * 中级证书 + */ + @Schema(description = "中级证书") + private String acpMiddleCert; + /** + * 根证书 + */ + @Schema(description = "根证书") + private String acpRootCert; + + /** 是否沙箱环境 */ + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + /** + * 服务器异步通知页面路径, 需要填写本网关服务的地址, 不可以直接填写业务系统的地址 + * 1. 需http://或者https://格式的完整路径, + * 2. 不能加?id=123这类自定义参数,必须外网可以正常访问 + * 3. 消息顺序 银联网关 -> 本网关进行处理 -> 发送消息通知业务系统 + */ + @Schema(description = "异步通知路径") + private String notifyUrl; + + /** + * 服务器同步通知页面路径, 需要填写本网关服务的地址, 不可以直接填写业务系统的地址 + * 1. 需http://或者https://格式的完整路径, + * 2. 不能加?id=123这类自定义参数,必须外网可以正常访问 + * 3. 消息顺序 银联网关 -> 本网关进行处理 -> 重定向到业务系统中 + */ + @Schema(description = "同步通知页面路径") + private String returnUrl; + + /** 可用支付方式 */ + @Schema(description = "可用支付方式") + private List payWays; + + @Schema(description = "备注") + private String remark; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/sdk/union/api/UnionPayKit.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/sdk/union/api/UnionPayKit.java index 273638c37..63245a5af 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/sdk/union/api/UnionPayKit.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/service/sdk/union/api/UnionPayKit.java @@ -1,8 +1,32 @@ package cn.bootx.platform.daxpay.service.sdk.union.api; +import com.alibaba.fastjson.JSONObject; +import com.egzosn.pay.common.bean.*; +import com.egzosn.pay.common.bean.outbuilder.PayTextOutMessage; +import com.egzosn.pay.common.bean.result.PayException; +import com.egzosn.pay.common.exception.PayErrorException; import com.egzosn.pay.common.http.HttpConfigStorage; +import com.egzosn.pay.common.http.UriVariables; +import com.egzosn.pay.common.util.DateUtils; +import com.egzosn.pay.common.util.Util; +import com.egzosn.pay.common.util.sign.CertDescriptor; +import com.egzosn.pay.common.util.sign.SignTextUtils; +import com.egzosn.pay.common.util.sign.SignUtils; +import com.egzosn.pay.common.util.sign.encrypt.RSA; +import com.egzosn.pay.common.util.sign.encrypt.RSA2; +import com.egzosn.pay.common.util.str.StringUtils; import com.egzosn.pay.union.api.UnionPayConfigStorage; import com.egzosn.pay.union.api.UnionPayService; +import com.egzosn.pay.union.bean.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.security.GeneralSecurityException; +import java.security.cert.*; +import java.sql.Timestamp; +import java.util.*; /** * 云闪付支付服务类重命名, 避免与系统中类名冲突 @@ -10,6 +34,29 @@ import com.egzosn.pay.union.api.UnionPayService; * @since 2024/3/8 */ public class UnionPayKit extends UnionPayService { + /** + * 测试域名 + */ + private static final String TEST_BASE_DOMAIN = "test.95516.com"; + /** + * 正式域名 + */ + private static final String RELEASE_BASE_DOMAIN = "95516.com"; + /** + * 交易请求地址 + */ + private static final String FRONT_TRANS_URL = "https://gateway.%s/gateway/api/frontTransReq.do"; + private static final String BACK_TRANS_URL = "https://gateway.%s/gateway/api/backTransReq.do"; + private static final String SINGLE_QUERY_URL = "https://gateway.%s/gateway/api/queryTrans.do"; + private static final String BATCH_TRANS_URL = "https://gateway.%s/gateway/api/batchTrans.do"; + private static final String FILE_TRANS_URL = "https://filedownload.%s/"; + private static final String APP_TRANS_URL = "https://gateway.%s/gateway/api/appTransReq.do"; + private static final String CARD_TRANS_URL = "https://gateway.%s/gateway/api/cardTransReq.do"; + /** + * 证书解释器 + */ + private volatile CertDescriptor certDescriptor; + /** * 构造函数 * @@ -22,4 +69,675 @@ public class UnionPayKit extends UnionPayService { public UnionPayKit(UnionPayConfigStorage payConfigStorage, HttpConfigStorage configStorage) { super(payConfigStorage, configStorage); } + + /** + * 设置支付配置 + * + * @param payConfigStorage 支付配置 + */ + @Override + public UnionPayService setPayConfigStorage(UnionPayConfigStorage payConfigStorage) { + this.payConfigStorage = payConfigStorage; + if (null != certDescriptor) { + return this; + } + try { + certDescriptor = new CertDescriptor(); + certDescriptor.initPrivateSignCert(payConfigStorage.getKeyPrivateCertInputStream(), payConfigStorage.getKeyPrivateCertPwd(), "PKCS12"); + certDescriptor.initPublicCert(payConfigStorage.getAcpMiddleCertInputStream()); + certDescriptor.initRootCert(payConfigStorage.getAcpRootCertInputStream()); + } + catch (IOException e) { + LOG.error("", e); + } + + + return this; + } + + /** + * 获取支付请求地址 + * + * @param transactionType 交易类型 + * @return 请求地址 + */ + @Override + public String getReqUrl(TransactionType transactionType) { + return (payConfigStorage.isTest() ? TEST_BASE_DOMAIN : RELEASE_BASE_DOMAIN); + } + + /** + * 根据是否为沙箱环境进行获取请求地址 + * + * @return 请求地址 + */ + public String getReqUrl() { + return getReqUrl(null); + } + + public String getFrontTransUrl() { + return String.format(FRONT_TRANS_URL, getReqUrl()); + } + + public String getBackTransUrl() { + return String.format(BACK_TRANS_URL, getReqUrl()); + } + + public String getAppTransUrl() { + return String.format(APP_TRANS_URL, getReqUrl()); + } + + public String getSingleQueryUrl() { + return String.format(SINGLE_QUERY_URL, getReqUrl()); + } + + + public String getFileTransUrl() { + return String.format(FILE_TRANS_URL, getReqUrl()); + } + + /** + * 后台通知地址 + * + * @param parameters 预订单信息 + * @param order 订单 + * @return 预订单信息 + */ + private Map initNotifyUrl(Map parameters, AssistOrder order) { + //后台通知地址 + OrderParaStructure.loadParameters(parameters, SDKConstants.param_backUrl, payConfigStorage.getNotifyUrl()); + OrderParaStructure.loadParameters(parameters, SDKConstants.param_backUrl, order.getNotifyUrl()); + OrderParaStructure.loadParameters(parameters, SDKConstants.param_backUrl, order); + return parameters; + } + + + /** + * 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 + * + * @return 返回参数集合 + */ + private Map getCommonParam() { + Map params = new TreeMap<>(); + UnionPayConfigStorage configStorage = payConfigStorage; + //银联接口版本 + params.put(SDKConstants.param_version, configStorage.getVersion()); + //编码方式 + params.put(SDKConstants.param_encoding, payConfigStorage.getInputCharset().toUpperCase()); + //商户代码 + params.put(SDKConstants.param_merId, payConfigStorage.getPid()); + + //订单发送时间 + params.put(SDKConstants.param_txnTime, DateUtils.formatDate(new Date(), DateUtils.YYYYMMDDHHMMSS)); + //后台通知地址 + params.put(SDKConstants.param_backUrl, payConfigStorage.getNotifyUrl()); + + //交易币种 + params.put(SDKConstants.param_currencyCode, "156"); + //接入类型,商户接入填0 ,不需修改(0:直连商户, 1: 收单机构 2:平台商户) + params.put(SDKConstants.param_accessType, configStorage.getAccessType()); + return params; + } + + + /** + * 回调校验 + * + * @param result 回调回来的参数集 + * @return 签名校验 true通过 + */ + @Deprecated + @Override + public boolean verify(Map result) { + + + return verify(new NoticeParams(result)); + } + + /** + * 回调校验 + * + * @param noticeParams 回调回来的参数集 + * @return 签名校验 true通过 + */ + @Override + public boolean verify(NoticeParams noticeParams) { + final Map result = noticeParams.getBody(); + if (null == result || result.get(SDKConstants.param_signature) == null) { + LOG.debug("银联支付验签异常:params:" + result); + return false; + } + return this.signVerify(result, (String) result.get(SDKConstants.param_signature)); + } + + /** + * 签名校验 + * + * @param params 参数集 + * @param sign 签名原文 + * @return 签名校验 true通过 + */ + public boolean signVerify(Map params, String sign) { + SignUtils signUtils = SignUtils.valueOf(payConfigStorage.getSignType()); + + String data = SignTextUtils.parameterText(params, "&", "signature"); + switch (signUtils) { + case RSA: + data = SignUtils.SHA1.createSign(data, "", payConfigStorage.getInputCharset()); + return RSA.verify(data, sign, verifyCertificate(genCertificateByStr((String) params.get(SDKConstants.param_signPubKeyCert))).getPublicKey(), payConfigStorage.getInputCharset()); + case RSA2: + data = SignUtils.SHA256.createSign(data, "", payConfigStorage.getInputCharset()); + return RSA2.verify(data, sign, verifyCertificate(genCertificateByStr((String) params.get(SDKConstants.param_signPubKeyCert))).getPublicKey(), payConfigStorage.getInputCharset()); + case SHA1: + case SHA256: + case SM3: + String before = signUtils.createSign(payConfigStorage.getKeyPublic(), "", payConfigStorage.getInputCharset()); + return signUtils.verify(data, sign, "&" + before, payConfigStorage.getInputCharset()); + default: + return false; + } + } + + + /** + * 订单超时时间。 + * 超过此时间后,除网银交易外,其他交易银联系统会拒绝受理,提示超时。 跳转银行网银交易如果超时后交易成功,会自动退款,大约5个工作日金额返还到持卡人账户。 + * 此时间建议取支付时的北京时间加15分钟。 + * 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。 + * + * @param expirationTime 超时时间 + * @return 具体的时间字符串 + */ + private String getPayTimeout(Date expirationTime) { + // + if (null != expirationTime) { + return DateUtils.formatDate(expirationTime, DateUtils.YYYYMMDDHHMMSS); + } + return DateUtils.formatDate(new Timestamp(System.currentTimeMillis() + 30 * 60 * 1000), DateUtils.YYYYMMDDHHMMSS); + } + + /** + * 返回创建的订单信息 + * + * @param order 支付订单 + * @return 订单信息 + * @see PayOrder 支付订单信息 + */ + @Override + public Map orderInfo(PayOrder order) { + Map params = this.getCommonParam(); + + UnionTransactionType type = (UnionTransactionType) order.getTransactionType(); + initNotifyUrl(params, order); + + //设置交易类型相关的参数 + type.convertMap(params); + + params.put(SDKConstants.param_orderId, order.getOutTradeNo()); + + if (StringUtils.isNotEmpty(order.getAddition())) { + params.put(SDKConstants.param_reqReserved, order.getAddition()); + } + switch (type) { + case WAP: + case WEB: + //todo PCwap网关跳转支付特殊用法.txt + case B2B: + params.put(SDKConstants.param_txnAmt, Util.conversionCentAmount(order.getPrice())); + params.put("orderDesc", order.getSubject()); + params.put(SDKConstants.param_payTimeout, getPayTimeout(order.getExpirationTime())); + + params.put(SDKConstants.param_frontUrl, payConfigStorage.getReturnUrl()); + break; + case CONSUME: + params.put(SDKConstants.param_txnAmt, Util.conversionCentAmount(order.getPrice())); + params.put(SDKConstants.param_qrNo, order.getAuthCode()); + break; + case APPLY_QR_CODE: + if (null != order.getPrice()) { + params.put(SDKConstants.param_txnAmt, Util.conversionCentAmount(order.getPrice())); + } + params.put(SDKConstants.param_payTimeout, getPayTimeout(order.getExpirationTime())); + break; + default: + params.put(SDKConstants.param_txnAmt, Util.conversionCentAmount(order.getPrice())); + params.put(SDKConstants.param_payTimeout, getPayTimeout(order.getExpirationTime())); + params.put("orderDesc", order.getSubject()); + } + params.putAll(order.getAttrs()); + params = preOrderHandler(params, order); + return setSign(params); + } + + + /** + * 生成并设置签名 + * + * @param parameters 请求参数 + * @return 请求参数 + */ + private Map setSign(Map parameters) { + + SignUtils signUtils = SignUtils.valueOf(payConfigStorage.getSignType()); + + String signStr; + switch (signUtils) { + case RSA: + parameters.put(SDKConstants.param_signMethod, SDKConstants.SIGNMETHOD_RSA); + parameters.put(SDKConstants.param_certId, certDescriptor.getSignCertId()); + signStr = SignUtils.SHA1.createSign(SignTextUtils.parameterText(parameters, "&", "signature"), "", payConfigStorage.getInputCharset()); + parameters.put(SDKConstants.param_signature, RSA.sign(signStr, certDescriptor.getSignCertPrivateKey(payConfigStorage.getKeyPrivateCertPwd()), payConfigStorage.getInputCharset())); + break; + case RSA2: + parameters.put(SDKConstants.param_signMethod, SDKConstants.SIGNMETHOD_RSA); + parameters.put(SDKConstants.param_certId, certDescriptor.getSignCertId()); + signStr = SignUtils.SHA256.createSign(SignTextUtils.parameterText(parameters, "&", "signature"), "", payConfigStorage.getInputCharset()); + parameters.put(SDKConstants.param_signature, RSA2.sign(signStr, certDescriptor.getSignCertPrivateKey(payConfigStorage.getKeyPrivateCertPwd()), payConfigStorage.getInputCharset())); + break; + case SHA1: + case SHA256: + case SM3: + String key = payConfigStorage.getKeyPrivate(); + signStr = SignTextUtils.parameterText(parameters, "&", "signature"); + key = signUtils.createSign(key, "", payConfigStorage.getInputCharset()) + "&"; + parameters.put(SDKConstants.param_signature, signUtils.createSign(signStr, key, payConfigStorage.getInputCharset())); + break; + default: + throw new PayErrorException(new PayException("sign fail", "未找到的签名类型")); + } + + + return parameters; + } + + + /** + * 验证证书链 + * + * @param cert 需要验证的证书 + */ + private X509Certificate verifyCertificate(X509Certificate cert) { + try { + cert.checkValidity();//验证有效期 + X509Certificate middleCert = certDescriptor.getPublicCert(); + X509Certificate rootCert = certDescriptor.getRootCert(); + + X509CertSelector selector = new X509CertSelector(); + selector.setCertificate(cert); + + Set trustAnchors = new HashSet(); + trustAnchors.add(new TrustAnchor(rootCert, null)); + PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector); + + Set intermediateCerts = new HashSet(); + intermediateCerts.add(rootCert); + intermediateCerts.add(middleCert); + intermediateCerts.add(cert); + + pkixParams.setRevocationEnabled(false); + + CertStore intermediateCertStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(intermediateCerts)); + pkixParams.addCertStore(intermediateCertStore); + + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + + /*PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult)*/ + builder.build(pkixParams); + return cert; + } + catch (java.security.cert.CertPathBuilderException e) { + LOG.error("verify certificate chain fail.", e); + } + catch (CertificateExpiredException e) { + LOG.error("", e); + } + catch (GeneralSecurityException e) { + LOG.error("", e); + } + return null; + } + + /** + * 发送订单 + * + * @param order 发起支付的订单信息 + * @return 返回支付结果 + */ + + public JSONObject postOrder(PayOrder order, String url) { + Map params = orderInfo(order); + String responseStr = getHttpRequestTemplate().postForObject(url, params, String.class); + JSONObject response = UriVariables.getParametersToMap(responseStr); + if (response.isEmpty()) { + throw new PayErrorException(new PayException("failure", "响应内容有误!", responseStr)); + } + return response; + } + + @Override + public String toPay(PayOrder order) { + + if (null == order.getTransactionType()) { + order.setTransactionType(UnionTransactionType.WEB); + } + else if (UnionTransactionType.WEB != order.getTransactionType() && UnionTransactionType.WAP != order.getTransactionType() && UnionTransactionType.B2B != order.getTransactionType()) { + throw new PayErrorException(new PayException("-1", "错误的交易类型:" + order.getTransactionType())); + } + + return super.toPay(order); + } + + /** + * 获取输出二维码,用户返回给支付端, + * + * @param order 发起支付的订单信息 + * @return 返回图片信息,支付时需要的 + */ + @Override + public String getQrPay(PayOrder order) { + order.setTransactionType(UnionTransactionType.APPLY_QR_CODE); + JSONObject response = postOrder(order, getBackTransUrl()); + if (this.verify(response)) { + if (SDKConstants.OK_RESP_CODE.equals(response.get(SDKConstants.param_respCode))) { + //成功 + return (String) response.get(SDKConstants.param_qrCode); + } + throw new PayErrorException(new PayException((String) response.get(SDKConstants.param_respCode), (String) response.get(SDKConstants.param_respMsg), response.toJSONString())); + } + throw new PayErrorException(new PayException("failure", "验证签名失败", response.toJSONString())); + } + + /** + * 刷卡付,pos主动扫码付款(条码付) + * + * @param order 发起支付的订单信息 + * @return 返回支付结果 + */ + @Override + public Map microPay(PayOrder order) { + order.setTransactionType(UnionTransactionType.CONSUME); + JSONObject response = postOrder(order, getBackTransUrl()); + return response; + } + + + /** + * 将字符串转换为X509Certificate对象. + * + * @param x509CertString 证书串 + * @return X509Certificate + */ + public static X509Certificate genCertificateByStr(String x509CertString) { + X509Certificate x509Cert = null; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream tIn = new ByteArrayInputStream(x509CertString.getBytes("ISO-8859-1")); + x509Cert = (X509Certificate) cf.generateCertificate(tIn); + } + catch (Exception e) { + throw new PayErrorException(new PayException("证书加载失败", "gen certificate error:" + e.getLocalizedMessage())); + } + return x509Cert; + } + + /** + * 获取输出消息,用户返回给支付端 + * + * @param code 状态 + * @param message 消息 + * @return 返回输出消息 + */ + @Override + public PayOutMessage getPayOutMessage(String code, String message) { + return PayTextOutMessage.TEXT().content(code.toLowerCase()).build(); + } + + /** + * 获取成功输出消息,用户返回给支付端 + * 主要用于拦截器中返回 + * + * @param payMessage 支付回调消息 + * @return 返回输出消息 + */ + @Override + public PayOutMessage successPayOutMessage(PayMessage payMessage) { + return getPayOutMessage("ok", null); + } + + /** + * 功能:生成自动跳转的Html表单 + * + * @param orderInfo 发起支付的订单信息 + * @param method 请求方式 "post" "get", + * @return 生成自动跳转的Html表单返回给支付端, 针对于PC端 + * @see MethodType 请求类型 + */ + @Override + public String buildRequest(Map orderInfo, MethodType method) { + StringBuffer sf = new StringBuffer(); + sf.append(""); + sf.append("
"); + if (null != orderInfo && 0 != orderInfo.size()) { + for (Map.Entry entry : orderInfo.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + sf.append(""); + } + } + sf.append(""); + sf.append(""); + sf.append(""); + sf.append(""); + return sf.toString(); + } + + /** + * 功能:将订单信息进行签名并提交请求 + * 业务范围:手机支付控件(含安卓Pay) + * + * @param order 订单信息 + * @return 成功:返回支付结果 失败:返回 + */ + @Override + public Map app(PayOrder order) { + if (null == order.getTransactionType()) { + order.setTransactionType(UnionTransactionType.APP); + } + JSONObject response = postOrder(order, getAppTransUrl()); + if (this.verify(response)) { + if (SDKConstants.OK_RESP_CODE.equals(response.get(SDKConstants.param_respCode))) { +// //成功,获取tn号 +// String tn = (String)response.get(SDKConstants.param_tn); +// //TODO + return response; + } + throw new PayErrorException(new PayException((String) response.get(SDKConstants.param_respCode), (String) response.get(SDKConstants.param_respMsg), response.toJSONString())); + } + throw new PayErrorException(new PayException("failure", "验证签名失败", response.toJSONString())); + } + + /** + * 交易查询接口 + * + * @param tradeNo 支付平台订单号 + * @param outTradeNo 商户单号 + * @return 返回查询回来的结果集,支付方原值返回 + */ + @Override + public Map query(String tradeNo, String outTradeNo) { + return query(new AssistOrder(tradeNo, outTradeNo)); + + } + + /** + * 交易查询接口 + * + * @param assistOrder 查询条件 + * @return 返回查询回来的结果集,支付方原值返回 + */ + @Override + public Map query(AssistOrder assistOrder) { + Map params = this.getCommonParam(); + UnionTransactionType.QUERY.convertMap(params); + params.put(SDKConstants.param_orderId, assistOrder.getOutTradeNo()); + this.setSign(params); + String responseStr = getHttpRequestTemplate().postForObject(this.getSingleQueryUrl(), params, String.class); + return UriVariables.getParametersToMap(responseStr); + } + + + /** + * 消费撤销/退货接口 + * + * @param origQryId 原交易查询流水号. + * @param orderId 退款单号 + * @param refundAmount 退款金额 + * @param type UnionTransactionType.REFUND 或者UnionTransactionType.CONSUME_UNDO + * @return 返回支付方申请退款后的结果 + */ + public UnionRefundResult unionRefundOrConsumeUndo(String origQryId, String orderId, BigDecimal refundAmount, UnionTransactionType type) { + return unionRefundOrConsumeUndo(new RefundOrder(orderId, origQryId, refundAmount), type); + + } + + /** + * 消费撤销/退货接口 + * + * @param refundOrder 退款订单信息 + * @param type UnionTransactionType.REFUND 或者UnionTransactionType.CONSUME_UNDO + * @return 返回支付方申请退款后的结果 + */ + public UnionRefundResult unionRefundOrConsumeUndo(RefundOrder refundOrder, UnionTransactionType type) { + Map params = this.getCommonParam(); + type.convertMap(params); + params.put(SDKConstants.param_orderId, refundOrder.getRefundNo()); + params.put(SDKConstants.param_txnAmt, Util.conversionCentAmount(refundOrder.getRefundAmount())); + params.put(SDKConstants.param_origQryId, refundOrder.getTradeNo()); + params.putAll(refundOrder.getAttrs()); + this.setSign(params); + String responseStr = getHttpRequestTemplate().postForObject(this.getBackTransUrl(), params, String.class); + JSONObject response = UriVariables.getParametersToMap(responseStr); + + if (this.verify(new NoticeParams(response))) { + final UnionRefundResult refundResult = UnionRefundResult.create(response); + if (SDKConstants.OK_RESP_CODE.equals(refundResult.getRespCode())) { + return refundResult; + + } + throw new PayErrorException(new PayException(response.getString(SDKConstants.param_respCode), response.getString(SDKConstants.param_respMsg), response.toJSONString())); + } + throw new PayErrorException(new PayException("failure", "验证签名失败", response.toJSONString())); + } + + /** + * 交易关闭接口 + * 使用冲正接口 + * + * @param tradeNo 支付平台订单号 + * @param outTradeNo 商户单号 + * @return 返回支付方交易关闭后的结果 + */ + @Override + public Map close(String tradeNo, String outTradeNo) { + return Collections.emptyMap(); + } + + /** + * 交易关闭接口 + * TODO 这个是冲正接口, 后续进行迁移 + * + * @param assistOrder 关闭订单 + * @return 返回支付方交易关闭后的结果 + */ + @Override + public Map close(AssistOrder assistOrder) { + Map params = this.getCommonParam(); + //交易类型 + params.put(SDKConstants.param_txnType, "99"); + //交易子类 + params.put(SDKConstants.param_txnSubType, "01"); + //业务类型 + params.put(SDKConstants.param_bizType,"000000"); + //渠道类型 + params.put(SDKConstants.param_channelType,"08"); + // 订单号 + params.put(SDKConstants.param_orderId, assistOrder.getOutTradeNo()); + this.setSign(params); + String responseStr = getHttpRequestTemplate().postForObject(this.getBackTransUrl(), params, String.class); + return UriVariables.getParametersToMap(responseStr); + } + + @Override + public UnionRefundResult refund(RefundOrder refundOrder) { + return unionRefundOrConsumeUndo(refundOrder, UnionTransactionType.REFUND); + } + + + /** + * 查询退款 + * + * @param refundOrder 退款订单单号信息 + * @return 返回支付方查询退款后的结果 + */ + @Override + public Map refundquery(RefundOrder refundOrder) { + return Collections.emptyMap(); + } + + /** + * 下载对账单 + * + * @param billDate 账单时间 + * @param fileType 文件类型 文件类型,一般商户填写00即可 + * @return 返回fileContent 请自行将数据落地 + */ + @Override + public Map downloadBill(Date billDate, String fileType) { + return downloadBill(billDate, new UnionPayBillType(fileType)); + } + + /** + * 下载对账单 + * + * @param billDate 账单时间 + * @param billType 账单类型 + * @return 返回fileContent 请自行将数据落地 + */ + @Override + public Map downloadBill(Date billDate, BillType billType) { + + Map params = this.getCommonParam(); + UnionTransactionType.FILE_TRANSFER.convertMap(params); + + params.put(SDKConstants.param_settleDate, DateUtils.formatDate(billDate, DateUtils.MMDD)); + params.put(SDKConstants.param_fileType, billType.getFileType()); + params.remove(SDKConstants.param_backUrl); + params.remove(SDKConstants.param_currencyCode); + this.setSign(params); + String responseStr = getHttpRequestTemplate().postForObject(this.getFileTransUrl(), params, String.class); + JSONObject response = UriVariables.getParametersToMap(responseStr); + if (this.verify(response)) { + if (SDKConstants.OK_RESP_CODE.equals(response.get(SDKConstants.param_respCode))) { + return response; + + } + throw new PayErrorException(new PayException(response.get(SDKConstants.param_respCode).toString(), response.get(SDKConstants.param_respMsg).toString(), response.toString())); + + } + throw new PayErrorException(new PayException("failure", "验证签名失败", response.toString())); + } + + + /** + * 创建消息 + * + * @param message 支付平台返回的消息 + * @return 支付消息对象 + */ + @Override + public PayMessage createMessage(Map message) { + return UnionPayMessage.create(message); + } }