Date:2025-07-31

author:Duanyuqing
comment:新增商户汇付注册信息管理
main
duanyuqing 6 months ago
parent 29795c059f
commit ac3978a404

@ -94,4 +94,9 @@ public interface ErrorCodeConstants {
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH = new ErrorCode(1_007_901_005, "更新示例提现单状态失败,支付转账单渠道不匹配"); ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH = new ErrorCode(1_007_901_005, "更新示例提现单状态失败,支付转账单渠道不匹配");
ErrorCode DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED = new ErrorCode(1_007_901_008, "发起转账失败,原因:示例提现单状态不是【等待提现】或【提现关闭】"); ErrorCode DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED = new ErrorCode(1_007_901_008, "发起转账失败,原因:示例提现单状态不是【等待提现】或【提现关闭】");
// ========== 商户服务注册 1-007-010-000 ==========
ErrorCode SERVICE_REGIST_NOT_EXISTS = new ErrorCode(1-007-010-000, "商户服务注册不存在");
ErrorCode SERVICE_REGIST_EXISTS = new ErrorCode(1-007-010-001, "商户服务注册已存在");
ErrorCode SERVICE_REGIST_PARAM_ERROR = new ErrorCode(1-007-010-002, "商户服务注册参数错误: ");
} }

@ -0,0 +1,110 @@
package cn.iocoder.hake.module.pay.controller.admin.serviceregist;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import javax.validation.*;
import javax.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.hake.framework.common.pojo.PageParam;
import cn.iocoder.hake.framework.common.pojo.PageResult;
import cn.iocoder.hake.framework.common.pojo.CommonResult;
import cn.iocoder.hake.framework.common.util.object.BeanUtils;
import static cn.iocoder.hake.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.hake.framework.common.pojo.CommonResult.success;
import cn.iocoder.hake.framework.excel.core.util.ExcelUtils;
import cn.iocoder.hake.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.hake.framework.apilog.core.enums.OperateTypeEnum.*;
import static cn.iocoder.hake.module.pay.enums.ErrorCodeConstants.SERVICE_REGIST_EXISTS;
import cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo.*;
import cn.iocoder.hake.module.pay.dal.dataobject.serviceregist.ServiceRegistDO;
import cn.iocoder.hake.module.pay.service.serviceregist.ServiceRegistService;
@Tag(name = "管理后台 - 商户服务注册")
@RestController
@RequestMapping("/mch/service-regist")
@Validated
public class ServiceRegistController {
@Resource
private ServiceRegistService serviceRegistService;
@PostMapping("/create")
@Operation(summary = "创建商户服务注册")
@PreAuthorize("@ss.hasPermission('mch:service-regist:create')")
public CommonResult<Long> createServiceRegist(@Valid @RequestBody ServiceRegistSaveReqVO createReqVO) {
ServiceRegistDO serviceRegistDO = serviceRegistService.getServiceRegist(createReqVO.getAppId(),createReqVO.getChannelId(),createReqVO.getMchNo());
if (serviceRegistDO == null) {
throw exception(SERVICE_REGIST_EXISTS);
}
return success(serviceRegistService.createServiceRegist(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新商户服务注册")
@PreAuthorize("@ss.hasPermission('mch:service-regist:update')")
public CommonResult<Boolean> updateServiceRegist(@Valid @RequestBody ServiceRegistSaveReqVO updateReqVO) {
serviceRegistService.updateServiceRegist(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除商户服务注册")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('mch:service-regist:delete')")
public CommonResult<Boolean> deleteServiceRegist(@RequestParam("id") Long id) {
serviceRegistService.deleteServiceRegist(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除商户服务注册")
@PreAuthorize("@ss.hasPermission('mch:service-regist:delete')")
public CommonResult<Boolean> deleteServiceRegistList(@RequestParam("ids") List<Long> ids) {
serviceRegistService.deleteServiceRegistListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得商户服务注册")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('mch:service-regist:query')")
public CommonResult<ServiceRegistRespVO> getServiceRegist(@RequestParam("id") Long id) {
ServiceRegistDO serviceRegist = serviceRegistService.getServiceRegist(id);
return success(BeanUtils.toBean(serviceRegist, ServiceRegistRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得商户服务注册分页")
@PreAuthorize("@ss.hasPermission('mch:service-regist:query')")
public CommonResult<PageResult<ServiceRegistRespVO>> getServiceRegistPage(@Valid ServiceRegistPageReqVO pageReqVO) {
PageResult<ServiceRegistDO> pageResult = serviceRegistService.getServiceRegistPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ServiceRegistRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出商户服务注册 Excel")
@PreAuthorize("@ss.hasPermission('mch:service-regist:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportServiceRegistExcel(@Valid ServiceRegistPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<ServiceRegistDO> list = serviceRegistService.getServiceRegistPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "商户服务注册.xls", "数据", ServiceRegistRespVO.class,
BeanUtils.toBean(list, ServiceRegistRespVO.class));
}
}

@ -0,0 +1,47 @@
package cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.hake.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.hake.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 商户服务注册分页 Request VO")
@Data
public class ServiceRegistPageReqVO extends PageParam {
@Schema(description = "应用ID", example = "25229")
private String appId;
@Schema(description = "应用名称", example = "王五")
private String appName;
@Schema(description = "渠道编号", example = "6018")
private String channelId;
@Schema(description = "商户号")
private String mchNo;
@Schema(description = "商户名称", example = "张三")
private String mchName;
@Schema(description = "应用状态: 0-停用, 1-正常")
private Integer state;
@Schema(description = "应用公钥")
private String appPubKey;
@Schema(description = "应用私钥")
private String appSecret;
@Schema(description = "备注", example = "你说的对")
private String remark;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

@ -0,0 +1,59 @@
package cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 商户服务注册 Response VO")
@Data
@ExcelIgnoreUnannotated
public class ServiceRegistRespVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1663")
@ExcelProperty("id")
private Long id;
@Schema(description = "应用ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25229")
@ExcelProperty("应用ID")
private String appId;
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
@ExcelProperty("应用名称")
private String appName;
@Schema(description = "渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6018")
@ExcelProperty("渠道编号")
private String channelId;
@Schema(description = "商户号", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("商户号")
private String mchNo;
@Schema(description = "商户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
@ExcelProperty("商户名称")
private String mchName;
@Schema(description = "应用状态: 0-停用, 1-正常", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("应用状态: 0-停用, 1-正常")
private Integer state;
@Schema(description = "应用公钥", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("应用公钥")
private String appPubKey;
@Schema(description = "应用私钥", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("应用私钥")
private String appSecret;
@Schema(description = "备注", example = "你说的对")
@ExcelProperty("备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

@ -0,0 +1,50 @@
package cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import javax.validation.constraints.*;
@Schema(description = "管理后台 - 商户服务注册新增/修改 Request VO")
@Data
public class ServiceRegistSaveReqVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1663")
private Long id;
@Schema(description = "应用ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25229")
@NotEmpty(message = "应用ID不能为空")
private String appId;
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
@NotEmpty(message = "应用名称不能为空")
private String appName;
@Schema(description = "渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6018")
@NotEmpty(message = "渠道编号不能为空")
private String channelId;
@Schema(description = "商户号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "商户号不能为空")
private String mchNo;
@Schema(description = "商户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
@NotEmpty(message = "商户名称不能为空")
private String mchName;
@Schema(description = "应用状态: 0-停用, 1-正常", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "应用状态: 0-停用, 1-正常不能为空")
private Integer state;
@Schema(description = "应用公钥", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "应用公钥不能为空")
private String appPubKey;
@Schema(description = "应用私钥", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "应用私钥不能为空")
private String appSecret;
@Schema(description = "备注", example = "你说的对")
private String remark;
}

@ -0,0 +1,65 @@
package cn.iocoder.hake.module.pay.dal.dataobject.serviceregist;
import lombok.*;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.hake.framework.mybatis.core.dataobject.BaseDO;
/**
* DO
*
* @author hake
*/
@TableName("mch_service_regist")
@KeySequence("mch_service_regist_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ServiceRegistDO extends BaseDO {
/**
* id
*/
@TableId
private Long id;
/**
* ID
*/
private String appId;
/**
*
*/
private String appName;
/**
*
*/
private String channelId;
/**
*
*/
private String mchNo;
/**
*
*/
private String mchName;
/**
* : 0-, 1-
*/
private Integer state;
/**
*
*/
private String appPubKey;
/**
*
*/
private String appSecret;
/**
*
*/
private String remark;
}

@ -0,0 +1,41 @@
package cn.iocoder.hake.module.pay.dal.mysql.serviceregist;
import cn.iocoder.hake.framework.common.pojo.PageResult;
import cn.iocoder.hake.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.hake.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.hake.module.pay.dal.dataobject.serviceregist.ServiceRegistDO;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo.*;
/**
* Mapper
*
* @author hake
*/
@Mapper
public interface ServiceRegistMapper extends BaseMapperX<ServiceRegistDO> {
default PageResult<ServiceRegistDO> selectPage(ServiceRegistPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ServiceRegistDO>()
.eqIfPresent(ServiceRegistDO::getAppId, reqVO.getAppId())
.likeIfPresent(ServiceRegistDO::getAppName, reqVO.getAppName())
.eqIfPresent(ServiceRegistDO::getChannelId, reqVO.getChannelId())
.eqIfPresent(ServiceRegistDO::getMchNo, reqVO.getMchNo())
.likeIfPresent(ServiceRegistDO::getMchName, reqVO.getMchName())
.eqIfPresent(ServiceRegistDO::getState, reqVO.getState())
.eqIfPresent(ServiceRegistDO::getAppPubKey, reqVO.getAppPubKey())
.eqIfPresent(ServiceRegistDO::getAppSecret, reqVO.getAppSecret())
.eqIfPresent(ServiceRegistDO::getRemark, reqVO.getRemark())
.betweenIfPresent(ServiceRegistDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ServiceRegistDO::getId));
}
default ServiceRegistDO selectByCondition(String appId, String channelId, String mchNo) {
return selectOne(Wrappers.<ServiceRegistDO>lambdaQuery()
.eq(ServiceRegistDO::getAppId, appId)
.eq(ServiceRegistDO::getChannelId, channelId)
.eq(ServiceRegistDO::getMchNo, mchNo));
}
}

@ -0,0 +1,69 @@
package cn.iocoder.hake.module.pay.framework.pay.config;
import cn.iocoder.hake.module.pay.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import javax.validation.Validator;
/**
* PayClientConfig
* {@link com.huifu.bspay.sdk.opps.core.config.MerConfig}
*
* @author duanyuqing
*/
@Data
public class HuifuClientConfig implements PayClientConfig {
@Value("${profiles.active}")
private String active;
/**
* debug
*/
private boolean debug;
/**
* prodMode
* MODE_PROD = "prod"; // 生产环境
* MODE_TEST = "test"; // 线上联调环境(针对商户联调测试)
*/
private String prodMode;
//产品编号
@Value("${huifu.procutId}")
private String procutId;
//系统编号
@Value("${huifu.sysId}")
private String sysId;
//私钥
@Value("${huifu.rsaPrivateKey}")
private String rsaPrivateKey;
//公钥
@Value("${huifu.rsaPublicKey}")
private String rsaPublicKey;
//自定义超时时间
@Value("${huifu.customConnectTimeout}")
private String customSocketTimeout;
@Value("${huifu.customConnectTimeout}")
private String customConnectTimeout;
@Value("${huifu.customConnectionRequestTimeout}")
private String customConnectionRequestTimeout;
@Override
public void validate(Validator validator) {
//读取环境配置,调用汇付接口
prodMode = active.equals("prod") ? "MODE_PROD" : "MODE_TEST";
debug = active.equals("prod") ? false : true;
}
}

@ -0,0 +1,597 @@
package cn.iocoder.hake.module.pay.framework.pay.core.client.impl.huifu;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.hake.framework.common.util.io.FileUtils;
import cn.iocoder.hake.framework.common.util.json.JsonUtils;
import cn.iocoder.hake.framework.common.util.object.ObjectUtils;
import cn.iocoder.hake.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.hake.module.pay.framework.pay.config.HuifuClientConfig;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import com.github.binarywang.wxpay.bean.notify.*;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.*;
import static cn.iocoder.hake.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
import static cn.iocoder.hake.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V3;
/**
*
*
* @author duanyuqing
*/
@Slf4j
public abstract class HuifuPayClient extends AbstractPayClient<HuifuClientConfig> {
protected TradePaymentMicropayRequest client;
public HuifuPayClient(Long channelId, String channelCode, HuifuClientConfig config) {
super(channelId, channelCode, config);
}
/**
* client
*
* @param tradeType
*/
protected void doInit(String tradeType) {
// 创建 config 配置
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent");
payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
} else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());
// 特殊:强制使用微信公用模式,避免灰度期间的问题!!!
payConfig.setStrictlyNeedWechatPaySerial(true);
}
// 创建 client 客户端
client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
// ============ 支付相关 ==========
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doUnifiedOrderV2(reqDTO);
case API_VERSION_V3:
// TODO @哈客:【可能是 wxjava 的 bug】参考 https://github.com/binarywang/WxJava/issues/1557
client.getConfig().setApiV3HttpClient(null);
return doUnifiedOrderV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[doUnifiedOrder][支付({}) 发起微信支付异常", reqDTO, e);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
reqDTO.getOutTradeNo(), e.getXmlString());
}
}
/**
* V2
*
* @param reqDTO
* @return
*/
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws Exception;
/**
* V3
*
* @param reqDTO
* @return
*/
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException;
/**
* V2
*
* @param reqDTO
* @return
*/
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
return WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.build();
}
/**
* V3
*
* @param reqDTO
* @return
*/
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
return request;
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case API_VERSION_V3:
return doParseOrderNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.CLOSED.getStatus();
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader);
WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = parseStatus(result.getTradeState());
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
result.getOutTradeNo(), body);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetOrderV2(outTradeNo);
case API_VERSION_V3:
return doGetOrderV3(outTradeNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
outTradeNo, e.getXmlString());
}
throw e;
}
}
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
.outTradeNo(outTradeNo).build();
// 执行请求
WxPayOrderQueryResult response = client.queryOrder(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
outTradeNo, response);
}
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
.setOutTradeNo(outTradeNo);
// 执行请求
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
outTradeNo, response);
}
private static Integer parseStatus(String tradeState) {
switch (tradeState) {
case "NOTPAY":
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
return PayOrderStatusEnum.WAITING.getStatus();
case "SUCCESS":
return PayOrderStatusEnum.SUCCESS.getStatus();
case "REFUND":
return PayOrderStatusEnum.REFUND.getStatus();
case "CLOSED":
case "REVOKED": // 已撤销(刷卡支付独有)
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
return PayOrderStatusEnum.CLOSED.getStatus();
default:
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
}
}
// ============ 退款相关 ==========
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doUnifiedRefundV2(reqDTO);
case API_VERSION_V3:
return doUnifiedRefundV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
reqDTO.getOutRefundNo(), e.getXmlString());
}
}
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRefundFee(reqDTO.getRefundPrice())
.setRefundDesc(reqDTO.getReason())
.setTotalFee(reqDTO.getPayPrice())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundResult response = client.refundV2(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
.setReason(reqDTO.getReason())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundV3Result response = client.refundV3(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getStatus())) {
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
reqDTO.getOutRefundNo(), response);
}
if (Objects.equals("PROCESSING", response.getStatus())) {
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseRefundNotifyV2(body);
case API_VERSION_V3:
return parseRefundNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
private PayRefundRespDTO parseRefundNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetRefundV2(outTradeNo, outRefundNo);
case API_VERSION_V3:
return doGetRefundV3(outTradeNo, outRefundNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
outRefundNo, e.getXmlString());
}
throw e;
}
}
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.build();
// 2.1 执行请求
WxPayRefundQueryResult response = client.refundQuery(request);
// 2.2 创建返回结果
if (!Objects.equals("SUCCESS", response.getResultCode())) {
return PayRefundRespDTO.waitingOf(null,
outRefundNo, response);
}
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
record -> record.getOutRefundNo().equals(outRefundNo));
if (refund == null) {
return PayRefundRespDTO.failureOf(outRefundNo, response);
}
switch (refund.getRefundStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
outRefundNo, response);
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
case "FAIL":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
}
}
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
request.setOutRefundNo(outRefundNo);
// 2.1 执行请求
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
// 2.2 创建返回结果
switch (response.getStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(response.getRefundId(),
outRefundNo, response);
case "ABNORMAL": // 退款异常
case "CLOSED":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
}
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 TransferBillsRequest 请求
TransferBillsRequest request = TransferBillsRequest.newBuilder()
.appid(this.config.getAppId())
.outBillNo(reqDTO.getOutTransferNo())
.transferAmount(reqDTO.getPrice())
.transferRemark(reqDTO.getSubject())
.transferSceneId(reqDTO.getChannelExtras().get("sceneId"))
.openid(reqDTO.getUserAccount())
.userName(reqDTO.getUserName())
.transferSceneReportInfos(JsonUtils.parseArray(reqDTO.getChannelExtras().get("sceneReportInfos"),
TransferBillsRequest.TransferSceneReportInfo.class))
.notifyUrl(reqDTO.getNotifyUrl())
.build();
// 特殊:微信转账,必须 0.3 元起,才允许传入姓名
if (reqDTO.getPrice() < 30) {
request.setUserName(null);
}
// 2.1 执行请求
try {
TransferBillsResult response = client.getTransferService().transferBills(request);
// 2.2 创建返回结果
String state = response.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response)
.setChannelPackageInfo(response.getPackageInfo()); // 一般情况下,只有 WAIT_USER_CONFIRM 会有!
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getCreateTime()),
response.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
response.getOutBillNo(), response);
} catch (WxPayException e) {
log.error("[doUnifiedTransfer][转账({}) 发起微信支付异常", reqDTO, e);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayTransferRespDTO.closedOf(errorCode, errorMessage,
reqDTO.getOutTransferNo(), e.getXmlString());
}
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 执行请求
TransferBillsGetResult response = client.getTransferService().getBillsByOutBillNo(outTradeNo);
// 2. 创建返回结果
String state = response.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response);
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()),
response.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
response.getOutBillNo(), response);
}
@Override
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V3:
return parseTransferNotifyV3(body, headers);
case API_VERSION_V2:
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
TransferBillsNotifyResult response = client.getTransferService().parseTransferBillsNotifyResult(body, signatureHeader);
TransferBillsNotifyResult.DecryptNotifyResult result = response.getResult();
// 2. 创建返回结果
String state = result.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(result.getTransferBillNo(), result.getOutBillNo(), response);
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(result.getTransferBillNo(), parseDateV3(result.getUpdateTime()),
result.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, result.getFailReason(),
result.getOutBillNo(), response);
}
// ========== 各种工具方法 ==========
/**
*
*
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221"></a>
*/
private SignatureHeader getRequestHeader(Map<String, String> headers) {
return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce"))
.serial(headers.get("wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp"))
.build();
}
// TODO @哈客:可能是 wxjava 的 bughttps://github.com/binarywang/WxJava/issues/1557
private void fixV3HttpClientConnectionPoolShutDown() {
client.getConfig().setApiV3HttpClient(null);
}
static String formatDateV2(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2(String time) {
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2B(String time) {
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
}
static String formatDateV3(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
}
static LocalDateTime parseDateV3(String time) {
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
}
static String getErrorCode(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCode();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return "CUSTOM_ERROR";
}
return e.getReturnCode();
}
static String getErrorMessage(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCodeDes();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return e.getCustomErrorMsg();
}
return e.getReturnMsg();
}
}

@ -0,0 +1,72 @@
package cn.iocoder.hake.module.pay.service.serviceregist;
import java.util.*;
import javax.validation.*;
import cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo.*;
import cn.iocoder.hake.module.pay.dal.dataobject.serviceregist.ServiceRegistDO;
import cn.iocoder.hake.framework.common.pojo.PageResult;
/**
* Service
*
* @author hake
*/
public interface ServiceRegistService {
/**
*
*
* @param createReqVO
* @return
*/
Long createServiceRegist(@Valid ServiceRegistSaveReqVO createReqVO);
/**
*
*
* @param updateReqVO
*/
void updateServiceRegist(@Valid ServiceRegistSaveReqVO updateReqVO);
/**
*
*
* @param id
*/
void deleteServiceRegist(Long id);
/**
*
*
* @param ids
*/
void deleteServiceRegistListByIds(List<Long> ids);
/**
*
*
* @param id
* @return
*/
ServiceRegistDO getServiceRegist(Long id);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<ServiceRegistDO> getServiceRegistPage(ServiceRegistPageReqVO pageReqVO);
/**
*
*
* @param appId
* @param channelId
* @param mchNo
* @return
*/
ServiceRegistDO getServiceRegist(String appId, String channelId, String mchNo);
}

@ -0,0 +1,113 @@
package cn.iocoder.hake.module.pay.service.serviceregist;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.hake.framework.security.core.util.SecurityFrameworkUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.*;
import cn.iocoder.hake.module.pay.controller.admin.serviceregist.vo.*;
import cn.iocoder.hake.module.pay.dal.dataobject.serviceregist.ServiceRegistDO;
import cn.iocoder.hake.framework.common.pojo.PageResult;
import cn.iocoder.hake.framework.common.util.object.BeanUtils;
import cn.iocoder.hake.module.pay.dal.mysql.serviceregist.ServiceRegistMapper;
import static cn.iocoder.hake.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.hake.module.pay.enums.ErrorCodeConstants.*;
/**
* Service
*
* @author hake
*/
@Service
@Validated
public class ServiceRegistServiceImpl implements ServiceRegistService {
@Resource
private ServiceRegistMapper serviceRegistMapper;
@Override
public Long createServiceRegist(ServiceRegistSaveReqVO createReqVO) {
// 插入
ServiceRegistDO serviceRegist = BeanUtils.toBean(createReqVO, ServiceRegistDO.class);
serviceRegist.setCreator(SecurityFrameworkUtils.getLoginUserId().toString());
serviceRegist.setUpdater(SecurityFrameworkUtils.getLoginUserId().toString());
serviceRegist.setDeleted(false);
serviceRegistMapper.insert(serviceRegist);
// 返回
return serviceRegist.getId();
}
@Override
public void updateServiceRegist(ServiceRegistSaveReqVO updateReqVO) {
// 校验存在
validateServiceRegistExists(updateReqVO.getId());
// 更新
ServiceRegistDO updateObj = BeanUtils.toBean(updateReqVO, ServiceRegistDO.class);
updateObj.setUpdater(SecurityFrameworkUtils.getLoginUserId().toString());
updateObj.setUpdateTime(LocalDateTime.now());
serviceRegistMapper.updateById(updateObj);
}
@Override
public void deleteServiceRegist(Long id) {
// 校验存在
validateServiceRegistExists(id);
// 删除
serviceRegistMapper.deleteById(id);
}
@Override
public void deleteServiceRegistListByIds(List<Long> ids) {
// 校验存在
validateServiceRegistExists(ids);
// 删除
serviceRegistMapper.deleteByIds(ids);
}
private void validateServiceRegistExists(List<Long> ids) {
List<ServiceRegistDO> list = serviceRegistMapper.selectByIds(ids);
if (CollUtil.isEmpty(list) || list.size() != ids.size()) {
throw exception(SERVICE_REGIST_NOT_EXISTS);
}
}
private void validateServiceRegistExists(Long id) {
if (serviceRegistMapper.selectById(id) == null) {
throw exception(SERVICE_REGIST_NOT_EXISTS);
}
}
@Override
public ServiceRegistDO getServiceRegist(Long id) {
return serviceRegistMapper.selectById(id);
}
@Override
public PageResult<ServiceRegistDO> getServiceRegistPage(ServiceRegistPageReqVO pageReqVO) {
return serviceRegistMapper.selectPage(pageReqVO);
}
/**
*
*
* @param appId
* @param channelId
* @param mchNo
* @return
*/
@Override
public ServiceRegistDO getServiceRegist(String appId, String channelId, String mchNo) {
return serviceRegistMapper.selectByCondition(appId, channelId, mchNo);
}
}

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save