diff --git a/hake-module-pay/hake-module-pay-api/src/main/java/cn/iocoder/hake/module/pay/enums/ErrorCodeConstants.java b/hake-module-pay/hake-module-pay-api/src/main/java/cn/iocoder/hake/module/pay/enums/ErrorCodeConstants.java index 7f11682..eb0cfc7 100644 --- a/hake-module-pay/hake-module-pay-api/src/main/java/cn/iocoder/hake/module/pay/enums/ErrorCodeConstants.java +++ b/hake-module-pay/hake-module-pay-api/src/main/java/cn/iocoder/hake/module/pay/enums/ErrorCodeConstants.java @@ -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_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, "商户服务注册参数错误: "); + } diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/ServiceRegistController.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/ServiceRegistController.java new file mode 100644 index 0000000..75e6f92 --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/ServiceRegistController.java @@ -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 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 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 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 deleteServiceRegistList(@RequestParam("ids") List 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 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> getServiceRegistPage(@Valid ServiceRegistPageReqVO pageReqVO) { + PageResult 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 list = serviceRegistService.getServiceRegistPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "商户服务注册.xls", "数据", ServiceRegistRespVO.class, + BeanUtils.toBean(list, ServiceRegistRespVO.class)); + } + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistPageReqVO.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistPageReqVO.java new file mode 100644 index 0000000..7d825a7 --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistRespVO.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistRespVO.java new file mode 100644 index 0000000..e22e2bc --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistSaveReqVO.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistSaveReqVO.java new file mode 100644 index 0000000..edf7a57 --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/controller/admin/serviceregist/vo/ServiceRegistSaveReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/dataobject/serviceregist/ServiceRegistDO.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/dataobject/serviceregist/ServiceRegistDO.java new file mode 100644 index 0000000..0266a80 --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/dataobject/serviceregist/ServiceRegistDO.java @@ -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; + + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/mysql/serviceregist/ServiceRegistMapper.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/mysql/serviceregist/ServiceRegistMapper.java new file mode 100644 index 0000000..f3224df --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/dal/mysql/serviceregist/ServiceRegistMapper.java @@ -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 { + + default PageResult selectPage(ServiceRegistPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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.lambdaQuery() + .eq(ServiceRegistDO::getAppId, appId) + .eq(ServiceRegistDO::getChannelId, channelId) + .eq(ServiceRegistDO::getMchNo, mchNo)); + } + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/config/HuifuClientConfig.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/config/HuifuClientConfig.java new file mode 100644 index 0000000..7d37a81 --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/config/HuifuClientConfig.java @@ -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; + } + +} diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/core/client/impl/huifu/HuifuPayClient.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/core/client/impl/huifu/HuifuPayClient.java new file mode 100644 index 0000000..2586a7b --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/framework/pay/core/client/impl/huifu/HuifuPayClient.java @@ -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 { + + 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 params, String body, Map 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 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 params, String body, Map 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 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 params, String body, Map 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 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 官方示例 + */ + private SignatureHeader getRequestHeader(Map 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 的 bug:https://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(); + } + +} diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistService.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistService.java new file mode 100644 index 0000000..a7260de --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistService.java @@ -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 ids); + + /** + * 获得商户服务注册 + * + * @param id 编号 + * @return 商户服务注册 + */ + ServiceRegistDO getServiceRegist(Long id); + + /** + * 获得商户服务注册分页 + * + * @param pageReqVO 分页查询 + * @return 商户服务注册分页 + */ + PageResult getServiceRegistPage(ServiceRegistPageReqVO pageReqVO); + + /** + * 获得商户服务注册信息 + * + * @param appId 应用编号 + * @param channelId 渠道编号 + * @param mchNo 商户编号 + * @return 商户服务注册信息 + */ + ServiceRegistDO getServiceRegist(String appId, String channelId, String mchNo); + +} \ No newline at end of file diff --git a/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistServiceImpl.java b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistServiceImpl.java new file mode 100644 index 0000000..f10be8a --- /dev/null +++ b/hake-module-pay/hake-module-pay-server/src/main/java/cn/iocoder/hake/module/pay/service/serviceregist/ServiceRegistServiceImpl.java @@ -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 ids) { + // 校验存在 + validateServiceRegistExists(ids); + // 删除 + serviceRegistMapper.deleteByIds(ids); + } + + private void validateServiceRegistExists(List ids) { + List 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 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); + } + +} \ No newline at end of file diff --git a/sql/mysql/pay.sql b/sql/mysql/pay.sql index 58aaf09..d190ccd 100644 --- a/sql/mysql/pay.sql +++ b/sql/mysql/pay.sql @@ -479,18 +479,42 @@ BEGIN; INSERT INTO `pay_wallet_transaction` (`id`, `wallet_id`, `biz_type`, `biz_id`, `no`, `title`, `price`, `balance`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 1, 1, '4', 'W202309301852101', '充值', 1000, 1100, NULL, '2023-09-30 18:52:10', NULL, '2023-09-30 18:52:10', b'0', 1), (2, 1, 1, '5', 'W202309301852251', '充值', 1000, 2100, NULL, '2023-09-30 18:52:26', NULL, '2023-09-30 18:52:26', b'0', 1), (3, 1, 3, '338', 'W202309302207411', '支付', -559920, 440079, '247', '2023-09-30 22:07:42', '247', '2023-09-30 22:07:42', b'0', 1), (4, 1, 3, '341', 'W202310021021111', '支付', -384945, 55134, '247', '2023-10-02 10:21:11', '247', '2023-10-02 10:21:11', b'0', 1), (5, 1, 3, '345', 'W202310052305281', '支付', -201, 54933, '247', '2023-10-05 23:05:28', '247', '2023-10-05 23:05:28', b'0', 1), (6, 1, 3, '349', 'W202310052322591', '支付', -201, 54732, '247', '2023-10-05 23:23:00', '247', '2023-10-05 23:23:00', b'0', 1), (7, 1, 3, '370', 'W202312122004591', '支付', -206, 54526, '247', '2023-12-12 20:04:59', '247', '2023-12-12 20:04:59', b'0', 1), (8, 1, 3, '371', 'W202312122012151', '支付', -700100, 53825900, '247', '2023-12-12 20:12:15', '247', '2023-12-12 20:12:15', b'0', 1), (9, 1, 3, '372', 'W202312122049431', '支付', -255, 53825645, '247', '2023-12-12 20:49:43', '247', '2023-12-12 20:49:43', b'0', 1), (10, 1, 3, '373', 'W202312122051121', '支付', -385145, 53440500, '247', '2023-12-12 20:51:12', '247', '2023-12-12 20:51:12', b'0', 1), (11, 1, 3, '375', 'W202312160001251', '支付', -206, 53440294, '247', '2023-12-16 00:01:26', '247', '2023-12-16 00:01:26', b'0', 1), (12, 1, 1, '10', 'W202312182308291', '充值', 11, 53440305, NULL, '2023-12-18 23:08:29', NULL, '2023-12-18 23:08:29', b'0', 1), (13, 1, 1, '11', 'W202312182309441', '充值', 120, 53440425, NULL, '2023-12-18 23:09:44', NULL, '2023-12-18 23:09:44', b'0', 1), (14, 1, 1, '12', 'W202312182311511', '充值', 11, 53440436, NULL, '2023-12-18 23:11:51', NULL, '2023-12-18 23:11:51', b'0', 1), (15, 18, 1, '15', 'W202407312339531', '充值', 120, 120, NULL, '2024-07-31 23:39:54', NULL, '2024-07-31 23:39:54', b'0', 1), (16, 18, 1, '16', 'W202407312343371', '充值', 120, 240, NULL, '2024-07-31 23:43:37', NULL, '2024-07-31 23:43:37', b'0', 1), (17, 18, 1, '17', 'W202407312345171', '充值', 120, 360, NULL, '2024-07-31 23:45:17', NULL, '2024-07-31 23:45:17', b'0', 1), (18, 18, 1, '18', 'W202408011247011', '充值', 120, 480, NULL, '2024-08-01 12:47:01', NULL, '2024-08-01 12:47:01', b'0', 1), (19, 18, 1, '19', 'W202408011250141', '充值', 120, 600, NULL, '2024-08-01 12:50:14', NULL, '2024-08-01 12:50:14', b'0', 1), (20, 18, 1, '21', 'W202408011256251', '充值', 12300, 12900, NULL, '2024-08-01 12:56:25', NULL, '2024-08-01 12:56:25', b'0', 1), (21, 18, 1, '22', 'W202408011257001', '充值', 12300, 25200, NULL, '2024-08-01 12:57:01', NULL, '2024-08-01 12:57:01', b'0', 1), (22, 18, 1, '23', 'W202408011257211', '充值', 120, 25320, NULL, '2024-08-01 12:57:21', NULL, '2024-08-01 12:57:21', b'0', 1), (23, 18, 1, '24', 'W202408011259431', '充值', 120, 25440, NULL, '2024-08-01 12:59:44', NULL, '2024-08-01 12:59:44', b'0', 1), (24, 18, 1, '26', 'W202408011302551', '充值', 32100, 57540, NULL, '2024-08-01 13:02:55', NULL, '2024-08-01 13:02:55', b'0', 1), (25, 18, 1, '27', 'W202408011305181', '充值', 100, 57640, NULL, '2024-08-01 13:05:18', NULL, '2024-08-01 13:05:18', b'0', 1), (26, 18, 1, '28', 'W202408011305541', '充值', 120, 57760, NULL, '2024-08-01 13:05:55', NULL, '2024-08-01 13:05:55', b'0', 1), (27, 18, 1, '29', 'W202408011306351', '充值', 22200, 79960, NULL, '2024-08-01 13:06:36', NULL, '2024-08-01 13:06:36', b'0', 1), (28, 18, 1, '30', 'W202408011307501', '充值', 3213100, 3293060, NULL, '2024-08-01 13:07:50', NULL, '2024-08-01 13:07:50', b'0', 1), (29, 1, 1, '33', 'W202409181912491', '充值', 120, 53440556, NULL, '2024-09-18 19:12:49', NULL, '2024-09-18 19:12:49', b'0', 1), (30, 1, 1, '34', 'W202409181923081', '充值', 120, 53440676, NULL, '2024-09-18 19:23:08', NULL, '2024-09-18 19:23:08', b'0', 1), (31, 1, 3, '477', 'W202409231344171', '支付', -7300, 53433376, '247', '2024-09-23 13:44:18', '247', '2024-09-23 13:44:18', b'0', 1), (32, 1, 1, '35', 'W202409240933571', '充值', 11, 53433387, NULL, '2024-09-24 09:33:58', NULL, '2024-09-24 09:33:58', b'0', 1), (33, 1, 1, '36', 'W202409240934351', '充值', 11, 53433398, NULL, '2024-09-24 09:34:36', NULL, '2024-09-24 09:34:36', b'0', 1), (34, 1, 3, '486', 'W202409300908431', '支付', -19840, 53413558, '247', '2024-09-30 09:08:44', '247', '2024-09-30 09:08:44', b'0', 1), (35, 1, 4, '98', 'W202409300909381', '支付退款', 19840, 53433398, '1', '2024-09-30 09:09:39', '1', '2024-09-30 09:09:39', b'0', 1), (36, 1, 6, '9', 'W202411251036261', '分佣提现', 10000, 53443398, '1', '2024-11-25 10:36:26', '1', '2024-11-25 10:36:26', b'0', 1), (37, 1, 6, '8', 'W202411251036401', '分佣提现', 4000, 53447398, '1', '2024-11-25 10:36:40', '1', '2024-11-25 10:36:40', b'0', 1), (38, 1, 6, '6', 'W202411251036561', '分佣提现', 80000, 53527398, '1', '2024-11-25 10:36:56', '1', '2024-11-25 10:36:56', b'0', 1), (39, 1, 6, '5', 'W202503161108241', '分佣提现', 40000, 53567398, '1', '2025-03-16 11:08:24', '1', '2025-03-16 11:08:24', b'0', 1), (40, 1, 6, '5', 'W202503161108491', '分佣提现', 40000, 53607398, '1', '2025-03-16 11:08:50', '1', '2025-03-16 11:08:50', b'0', 1), (41, 1, 3, '491', 'W202504261924171', '支付', -3500, 53603898, '247', '2025-04-26 19:24:18', '247', '2025-04-26 19:24:18', b'0', 1), (42, 1, 4, '99', 'W202504261928021', '支付退款', 3500, 53607398, '1', '2025-04-26 19:28:02', '1', '2025-04-26 19:28:02', b'0', 1), (43, 1, 3, '494', 'W202504280000491', '支付', -3500, 53603898, '247', '2025-04-28 00:00:49', '247', '2025-04-28 00:00:49', b'0', 1), (44, 1, 3, '497', 'W202504282245581', '支付', -3500, 53600398, '247', '2025-04-28 22:45:58', '247', '2025-04-28 22:45:58', b'0', 1), (45, 1, 6, 'T202505082321011', 'W202505082321011', '转账', 1, 53600399, '1', '2025-05-08 23:21:01', '1', '2025-05-08 23:21:01', b'0', 1), (46, 1, 6, 'T202505082321201', 'W202505082321201', '转账', 1, 53600400, '1', '2025-05-08 23:21:21', '1', '2025-05-08 23:21:21', b'0', 1), (47, 1, 3, '526', 'W202505082329051', '支付', -3500, 53596900, '247', '2025-05-08 23:29:06', '247', '2025-05-08 23:29:06', b'0', 1), (48, 1, 6, 'T202505092021201', 'W202505092021201', '转账', 1, 53596901, '1', '2025-05-09 20:21:20', '1', '2025-05-09 20:21:20', b'0', 1), (49, 1, 6, 'T202505092021551', 'W202505092021551', '转账', 1, 53596902, '1', '2025-05-09 20:21:55', '1', '2025-05-09 20:21:55', b'0', 1), (50, 1, 6, 'T202505092025561', 'W202505092025561', '转账', 1, 53596903, '1', '2025-05-09 20:25:56', '1', '2025-05-09 20:25:56', b'0', 1), (51, 1, 6, 'T202505092026271', 'W202505092026271', '转账', 1, 53596904, '1', '2025-05-09 20:26:28', '1', '2025-05-09 20:26:28', b'0', 1), (52, 1, 6, 'T202505092027501', 'W202505092027501', '转账', 1, 53596905, '1', '2025-05-09 20:27:50', '1', '2025-05-09 20:27:50', b'0', 1), (53, 1, 6, 'T202505092142231', 'W202505092142231', '转账', 1, 53596906, '1', '2025-05-09 21:42:23', '1', '2025-05-09 21:42:23', b'0', 1), (54, 1, 6, 'T202505101003511', 'W202505101003511', '转账', 100, 53597006, '1', '2025-05-10 10:03:51', '1', '2025-05-10 10:03:51', b'0', 1), (55, 1, 3, '527', 'W202505101352071', '支付', -3500, 53593506, '247', '2025-05-10 13:52:07', '247', '2025-05-10 13:52:07', b'0', 1), (56, 1, 4, '100', 'W202505101352311', '支付退款', 3500, 53597006, '1', '2025-05-10 13:52:31', '1', '2025-05-10 13:52:31', b'0', 1), (57, 1, 3, '528', 'W202505101353451', '支付', -3500, 53593506, '247', '2025-05-10 13:53:45', '247', '2025-05-10 13:53:45', b'0', 1), (58, 1, 4, '101', 'W202505101354181', '支付退款', 3500, 53597006, '1', '2025-05-10 13:54:19', '1', '2025-05-10 13:54:19', b'0', 1), (59, 1, 6, 'T202505101558111', 'W202505101558111', '转账', 1, 53597007, '1', '2025-05-10 15:58:11', '1', '2025-05-10 15:58:11', b'0', 1), (60, 1, 6, 'T202505101600231', 'W202505101600231', '转账', 1, 53597008, '1', '2025-05-10 16:00:23', '1', '2025-05-10 16:00:23', b'0', 1), (61, 1, 3, '540', 'W202505101609421', '支付', -3500, 53593508, '247', '2025-05-10 16:09:42', '247', '2025-05-10 16:09:42', b'0', 1), (62, 1, 4, '113', 'W202505101610201', '支付退款', 3500, 53597008, '1', '2025-05-10 16:10:20', '1', '2025-05-10 16:10:20', b'0', 1), (63, 1, 3, '541', 'W202505101616461', '支付', -3500, 53593508, '247', '2025-05-10 16:16:47', '247', '2025-05-10 16:16:47', b'0', 1), (64, 1, 4, '114', 'W202505101617011', '支付退款', 3500, 53597008, '1', '2025-05-10 16:17:02', '1', '2025-05-10 16:17:02', b'0', 1), (65, 1, 3, '545', 'W202505101639371', '支付', -3500, 53593508, '247', '2025-05-10 16:39:37', '247', '2025-05-10 16:39:37', b'0', 1), (66, 1, 4, '116', 'W202505101640241', '支付退款', 3500, 53597008, '1', '2025-05-10 16:40:24', '1', '2025-05-10 16:40:24', b'0', 1), (67, 1, 6, 'T202505111730161', 'W202505111730161', '转账', 1, 53597009, '1', '2025-05-11 17:30:17', '1', '2025-05-11 17:30:17', b'0', 1); COMMIT; -DROP TABLE IF EXISTS pay_mch_app; -CREATE TABLE `pay_mch_app` ( - `app_id` varchar(64) NOT NULL COMMENT '应用ID', - `app_name` varchar(64) NOT NULL DEFAULT '' COMMENT '应用名称', - `mch_no` VARCHAR(64) NOT NULL COMMENT '商户号', - `state` TINYINT(6) NOT NULL DEFAULT 1 COMMENT '应用状态: 0-停用, 1-正常', - `app_secret` VARCHAR(128) NOT NULL COMMENT '应用私钥', - `remark` varchar(128) DEFAULT NULL COMMENT '备注', - `created_uid` BIGINT(20) COMMENT '创建者用户ID', - `created_by` VARCHAR(64) 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 (`app_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户应用表'; + +DROP TABLE IF EXISTS mch_service_regist; +CREATE TABLE `mch_service_regist` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'id', + `app_id` varchar(64) NOT NULL COMMENT '应用ID', + `app_name` varchar(64) NOT NULL DEFAULT '' COMMENT '应用名称', + `channel_id` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道编号', + `mch_no` VARCHAR(64) NOT NULL COMMENT '商户号', + `mch_name` VARCHAR(64) NOT NULL COMMENT '商户名称', + `state` TINYINT(6) NOT NULL DEFAULT 1 COMMENT '应用状态: 0-停用, 1-正常', + `app_pub_key` TEXT NOT NULL COMMENT '应用公钥', + `app_secret` TEXT NOT NULL COMMENT '应用私钥', + `remark` varchar(128) DEFAULT NULL COMMENT '备注', + `creator` varchar(64) default '' null comment '创建者', + `create_time` datetime default CURRENT_TIMESTAMP not null comment '创建时间', + `updater` varchar(64) default '' null comment '更新者', + `update_time` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', + `deleted` bit default b'0' not null comment '是否删除', + `tenant_id` bigint default 0 not null comment '租户编号', + PRIMARY KEY (`id`), + UNIQUE (`app_id`,`channel_id`,`mch_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户服务注册表'; + +DROP TABLE IF EXISTS huifu_mcc; +CREATE TABLE `huifu_mcc` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'id', + `major_category` varchar(64) NOT NULL COMMENT '大类名称', + `sub_category` varchar(64) NOT NULL COMMENT '小类名称', + `category` varchar(64) NOT NULL COMMENT '名称', + `code` VARCHAR(64) NOT NULL COMMENT '编码', + `creator` varchar(64) default '' null comment '创建者', + `create_time` datetime default CURRENT_TIMESTAMP not null comment '创建时间', + `updater` varchar(64) default '' null comment '更新者', + `update_time` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', + `deleted` bit default b'0' not null comment '是否删除', + PRIMARY KEY (`id`), + UNIQUE (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='汇付MCC表'; SET FOREIGN_KEY_CHECKS = 1;