init
This commit is contained in:
3
sl-express-ms-trade-service/.gitignore
vendored
Normal file
3
sl-express-ms-trade-service/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
target/
|
||||
*.iml
|
13
sl-express-ms-trade-service/Dockerfile
Normal file
13
sl-express-ms-trade-service/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM openjdk:11-jdk
|
||||
LABEL maintainer="研究院研发组 <research@itcast.cn>"
|
||||
|
||||
# 时区修改为东八区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
WORKDIR /app
|
||||
ARG JAR_FILE=target/*.jar
|
||||
ADD ${JAR_FILE} app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["sh","-c","java -Djava.security.egd=file:/dev/./urandom -jar $JAVA_OPTS app.jar"]
|
125
sl-express-ms-trade-service/pom.xml
Normal file
125
sl-express-ms-trade-service/pom.xml
Normal file
@@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.sl-express</groupId>
|
||||
<artifactId>sl-express-parent</artifactId>
|
||||
<version>1.4</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.sl-express.ms.trade</groupId>
|
||||
<artifactId>sl-express-ms-trade-service</artifactId>
|
||||
<version>1.1-SNAPSHOT</version>
|
||||
<description>交易(支付)微服务</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<sl-express-common.version>1.2-SNAPSHOT</sl-express-common.version>
|
||||
<sl-express-trade.version>1.1-SNAPSHOT</sl-express-trade.version>
|
||||
<sl-express-base.version>1.1-SNAPSHOT</sl-express-base.version>
|
||||
<!--支付宝-->
|
||||
<alipay.easysdk.version>2.2.2</alipay.easysdk.version>
|
||||
<!--微信支付-->
|
||||
<wechatpay.version>0.4.7</wechatpay.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.sl-express.common</groupId>
|
||||
<artifactId>sl-express-common</artifactId>
|
||||
<version>${sl-express-common.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sl-express.ms.trade</groupId>
|
||||
<artifactId>sl-express-ms-trade-domain</artifactId>
|
||||
<version>${sl-express-trade.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sl-express.ms.base</groupId>
|
||||
<artifactId>sl-express-ms-base-api</artifactId>
|
||||
<version>${sl-express-base.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-easysdk</artifactId>
|
||||
<version>${alipay.easysdk.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.wechatpay-apiv3</groupId>
|
||||
<artifactId>wechatpay-apache-httpclient</artifactId>
|
||||
<version>${wechatpay.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson</artifactId>
|
||||
</dependency>
|
||||
<!--二维码生成工具包-->
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
<goal>build-info</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<!--指定主类-->
|
||||
<mainClass>com.sl.TradeApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,15 @@
|
||||
package com.sl;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
@EnableFeignClients
|
||||
@SpringBootApplication
|
||||
public class TradeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TradeApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.sl.ms.trade.annotation;
|
||||
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented //标记注解
|
||||
public @interface PayChannel {
|
||||
|
||||
PayChannelEnum type();
|
||||
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package com.sl.ms.trade.config;
|
||||
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 二维码生成参数配置
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "sl.qrcode")
|
||||
public class QRCodeConfig {
|
||||
|
||||
private static Image WECHAT_LOGO;
|
||||
private static Image ALIPAY_LOGO;
|
||||
|
||||
static {
|
||||
WECHAT_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/wechat.png"));
|
||||
ALIPAY_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/alipay.png"));
|
||||
}
|
||||
|
||||
//边距,二维码和背景之间的边距
|
||||
private Integer margin = 2;
|
||||
// 二维码颜色,默认黑色
|
||||
private String foreColor = "#000000";
|
||||
//背景色,默认白色
|
||||
private String backColor = "#ffffff";
|
||||
//纠错级别,可选参数:L、M、Q、H,默认:M
|
||||
//低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
|
||||
private String errorCorrectionLevel = "M";
|
||||
//宽
|
||||
private Integer width = 300;
|
||||
//高
|
||||
private Integer height = 300;
|
||||
|
||||
public Image getLogo(PayChannelEnum payChannelEnum) {
|
||||
switch (payChannelEnum) {
|
||||
case ALI_PAY: {
|
||||
return ALIPAY_LOGO;
|
||||
}
|
||||
case WECHAT_PAY: {
|
||||
return WECHAT_LOGO;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.sl.ms.trade.config;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Data;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(RedisProperties.class)
|
||||
@Data
|
||||
public class RedissonConfiguration {
|
||||
|
||||
@Resource
|
||||
private RedisProperties redisProperties;
|
||||
|
||||
@Bean
|
||||
public RedissonClient redissonSingle() {
|
||||
Config config = new Config();
|
||||
SingleServerConfig serverConfig = config.useSingleServer()
|
||||
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
|
||||
if (null != (redisProperties.getTimeout())) {
|
||||
serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds()));
|
||||
}
|
||||
if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
|
||||
serverConfig.setPassword(redisProperties.getPassword());
|
||||
}
|
||||
return Redisson.create(config);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package com.sl.ms.trade.config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* xxl-job config
|
||||
*/
|
||||
@Configuration
|
||||
public class XxlJobConfig {
|
||||
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
|
||||
|
||||
@Value("${xxl.job.admin.addresses}")
|
||||
private String adminAddresses;
|
||||
|
||||
@Value("${xxl.job.accessToken:}")
|
||||
private String accessToken;
|
||||
|
||||
@Value("${xxl.job.executor.appname}")
|
||||
private String appname;
|
||||
|
||||
@Value("${xxl.job.executor.address:}")
|
||||
private String address;
|
||||
|
||||
@Value("${xxl.job.executor.ip:}")
|
||||
private String ip;
|
||||
|
||||
@Value("${xxl.job.executor.port:0}")
|
||||
private int port;
|
||||
|
||||
@Value("${xxl.job.executor.logpath:}")
|
||||
private String logPath;
|
||||
|
||||
@Value("${xxl.job.executor.logretentiondays:}")
|
||||
private int logRetentionDays;
|
||||
|
||||
|
||||
@Bean
|
||||
public XxlJobSpringExecutor xxlJobExecutor() {
|
||||
logger.info(">>>>>>>>>>> xxl-job config init.");
|
||||
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
|
||||
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
|
||||
xxlJobSpringExecutor.setAppname(appname);
|
||||
xxlJobSpringExecutor.setAddress(address);
|
||||
xxlJobSpringExecutor.setIp(ip);
|
||||
xxlJobSpringExecutor.setPort(port);
|
||||
xxlJobSpringExecutor.setAccessToken(accessToken);
|
||||
xxlJobSpringExecutor.setLogPath(logPath);
|
||||
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
|
||||
return xxlJobSpringExecutor;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.sl.ms.trade.constant;
|
||||
|
||||
/**
|
||||
* 静态变量
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface Constants {
|
||||
/**
|
||||
* 常量是
|
||||
*/
|
||||
String YES = "YES";
|
||||
|
||||
/**
|
||||
* 常量否
|
||||
*/
|
||||
String NO = "NO";
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.sl.ms.trade.constant;
|
||||
|
||||
/**
|
||||
* @ClassName TradingCacheConstant.java
|
||||
* @Description 交易缓存维护
|
||||
*/
|
||||
public class TradingCacheConstant {
|
||||
|
||||
//默认redis等待时间
|
||||
public static final int REDIS_WAIT_TIME = 5;
|
||||
|
||||
//默认redis自动释放时间
|
||||
public static final int REDIS_LEASETIME = 4;
|
||||
|
||||
//安全组前缀
|
||||
public static final String PREFIX = "trading:";
|
||||
|
||||
//分布式锁前缀
|
||||
public static final String LOCK_PREFIX = PREFIX + "lock:";
|
||||
|
||||
//创建交易加锁
|
||||
public static final String CREATE_PAY = LOCK_PREFIX + "create_pay";
|
||||
|
||||
//查询交易状态加锁
|
||||
public static final String QUERY_PAY = LOCK_PREFIX + "query_pay";
|
||||
|
||||
//创建退款加锁
|
||||
public static final String REFUND_PAY = LOCK_PREFIX + "refund_pay";
|
||||
|
||||
//退款查询加锁
|
||||
public static final String REFUND_QUERY_PAY = LOCK_PREFIX + "refund_query_pay";
|
||||
|
||||
//创建退款加锁
|
||||
public static final String PAY_CHANNEL_LIST = PREFIX + "pay_channel_list&ttl=-1";
|
||||
|
||||
//page分页
|
||||
public static final String PAGE = PREFIX + "page";
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.sl.ms.trade.constant;
|
||||
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
|
||||
/**
|
||||
* @ClassName TardingConstant.java
|
||||
* @Description 交易常量类
|
||||
*/
|
||||
public class TradingConstant {
|
||||
|
||||
//【阿里云退款返回状态】
|
||||
//REFUND_SUCCESS:成功
|
||||
public static final String REFUND_SUCCESS = "REFUND_SUCCESS";
|
||||
|
||||
//【阿里云返回付款状态】
|
||||
//TRADE_CLOSED:未付款交易超时关闭,或支付完成后全额退款
|
||||
public static final String ALI_TRADE_CLOSED = "TRADE_CLOSED";
|
||||
//TRADE_SUCCESS:交易支付成功
|
||||
public static final String ALI_TRADE_SUCCESS = "TRADE_SUCCESS";
|
||||
//TRADE_FINISHED:交易结束不可退款
|
||||
public static final String ALI_TRADE_FINISHED = "TRADE_FINISHED";
|
||||
|
||||
|
||||
//【微信退款返回状态】
|
||||
//SUCCESS:退款成功
|
||||
public static final String WECHAT_REFUND_SUCCESS = "SUCCESS";
|
||||
//CLOSED:退款关闭
|
||||
public static final String WECHAT_REFUND_CLOSED = "CLOSED";
|
||||
//PROCESSING:退款处理中
|
||||
public static final String WECHAT_REFUND_PROCESSING = "PROCESSING";
|
||||
//ABNORMAL:退款异常
|
||||
public static final String WECHAT_REFUND_ABNORMAL = "TRADE_CLOSED";
|
||||
|
||||
//【微信返回付款状态】
|
||||
//SUCCESS:支付成功
|
||||
public static final String WECHAT_TRADE_SUCCESS = "SUCCESS";
|
||||
//REFUND:转入退款
|
||||
public static final String WECHAT_TRADE_REFUND = "REFUND";
|
||||
//NOTPAY:未支付
|
||||
public static final String WECHAT_TRADE_NOTPAY = "NOTPAY";
|
||||
//CLOSED:已关闭
|
||||
public static final String WECHAT_TRADE_CLOSED = "CLOSED";
|
||||
//REVOKED:已撤销(仅付款码支付会返回)
|
||||
public static final String WECHAT_TRADE_REVOKED = "REVOKED";
|
||||
//USERPAYING:用户支付中(仅付款码支付会返回)
|
||||
public static final String WECHAT_TRADE_USERPAYING = "USERPAYING";
|
||||
//PAYERROR:支付失败(仅付款码支付会返回)
|
||||
public static final String WECHAT_TRADE_PAYERROR = "PAYERROR";
|
||||
|
||||
//【平台:交易渠道】
|
||||
//阿里支付
|
||||
public static final String TRADING_CHANNEL_ALI_PAY = PayChannelEnum.ALI_PAY.name();
|
||||
//微信支付
|
||||
public static final String TRADING_CHANNEL_WECHAT_PAY = PayChannelEnum.WECHAT_PAY.name();
|
||||
//现金
|
||||
public static final String TRADING_CHANNEL_CASH_PAY = "CASH_PAY";
|
||||
//免单挂账【信用渠道】
|
||||
public static final String TRADING_CHANNEL_CREDIT_PAY = "CREDIT_PAY";
|
||||
|
||||
//【平台:交易动作】
|
||||
//付款
|
||||
public static final String TRADING_TYPE_FK = "FK";
|
||||
//退款
|
||||
public static final String TRADING_TYPE_TK = "TK";
|
||||
//免单
|
||||
public static final String TRADING_TYPE_MD = "MD";
|
||||
//挂账
|
||||
public static final String TRADING_TYPE_GZ = "GZ";
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import com.sl.ms.trade.domain.RefundRecordDTO;
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.service.BasicPayService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 基础支付控制器
|
||||
*/
|
||||
@RequestMapping("basic")
|
||||
@RestController
|
||||
@Api(tags = "Basic支付")
|
||||
public class BasicPayController {
|
||||
|
||||
@Resource
|
||||
private BasicPayService basicPayService;
|
||||
|
||||
/***
|
||||
* 统一收单线下交易查询
|
||||
* 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 交易单
|
||||
*/
|
||||
@PostMapping("query/{tradingOrderNo}")
|
||||
@ApiOperation(value = "查询统一收单线下交易", notes = "查询统一收单线下交易")
|
||||
@ApiImplicitParam(name = "tradingOrderNo", value = "交易单", required = true)
|
||||
public TradingDTO queryTrading(@PathVariable("tradingOrderNo") Long tradingOrderNo) {
|
||||
return this.basicPayService.queryTrading(tradingOrderNo);
|
||||
}
|
||||
|
||||
/***
|
||||
* 统一收单交易退款接口
|
||||
* 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
|
||||
* 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @param refundAmount 退款金额
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("refund")
|
||||
@ApiOperation(value = "统一收单交易退款", notes = "统一收单交易退款")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true),
|
||||
@ApiImplicitParam(name = "refundAmount", value = "退款金额", required = true)
|
||||
})
|
||||
public void refundTrading(@RequestParam("tradingOrderNo") Long tradingOrderNo,
|
||||
@RequestParam("refundAmount") BigDecimal refundAmount) {
|
||||
Boolean result = this.basicPayService.refundTrading(tradingOrderNo, refundAmount);
|
||||
if (!result) {
|
||||
throw new SLException(TradingEnum.BASIC_REFUND_COUNT_OUT_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***
|
||||
* 统一收单交易退款查询接口
|
||||
* @param refundNo 退款交易单号
|
||||
* @return 退款记录
|
||||
*/
|
||||
@PostMapping("refund/{refundNo}")
|
||||
@ApiOperation(value = "查询统一收单交易退款", notes = "查询统一收单交易退款")
|
||||
@ApiImplicitParam(name = "refundNo", value = "退款交易单", required = true)
|
||||
public RefundRecordDTO queryRefundDownLineTrading(@PathVariable("refundNo") Long refundNo) {
|
||||
return this.basicPayService.queryRefundTrading(refundNo);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.sl.ms.trade.domain.request.JsapiPayDTO;
|
||||
import com.sl.ms.trade.domain.response.JsapiPayResponseDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.service.JsapiPayService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* Native支付方式Face接口:商户生成二维码,用户扫描支付
|
||||
*/
|
||||
@RequestMapping("jsapi")
|
||||
@RestController
|
||||
@Api(tags = "Jsapi支付")
|
||||
public class JsapiPayController {
|
||||
|
||||
@Resource
|
||||
private JsapiPayService jsapiPayService;
|
||||
|
||||
/***
|
||||
* 统一jsapi交易预创建
|
||||
* 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、
|
||||
* JSAPI、APP等不同场景生成交易串调起支付。
|
||||
* @param jsapiPayDTO jsapi提交支付请求对象
|
||||
*
|
||||
* @return 交易单,支付串码
|
||||
*/
|
||||
@PostMapping
|
||||
@ApiOperation(value = "jsapi预交易", notes = "jsapi预交易")
|
||||
@ApiImplicitParam(name = "jsapiPayDTO", value = "交易单", required = true)
|
||||
public JsapiPayResponseDTO createJsapiTrading(@RequestBody JsapiPayDTO jsapiPayDTO) {
|
||||
TradingEntity tradingEntity = BeanUtil.toBean(jsapiPayDTO, TradingEntity.class);
|
||||
TradingEntity trading = this.jsapiPayService.createJsapiTrading(tradingEntity);
|
||||
return BeanUtil.toBean(trading, JsapiPayResponseDTO.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.sl.ms.trade.domain.request.NativePayDTO;
|
||||
import com.sl.ms.trade.domain.response.NativePayResponseDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.service.NativePayService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* Native支付方式Face接口:商户生成二维码,用户扫描支付
|
||||
*/
|
||||
@Validated
|
||||
@RestController
|
||||
@Api(tags = "Native支付")
|
||||
@RequestMapping("native")
|
||||
public class NativePayController {
|
||||
|
||||
@Resource
|
||||
private NativePayService nativePayService;
|
||||
|
||||
/***
|
||||
* 扫码支付,收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。
|
||||
*
|
||||
* @param nativePayDTO 扫码支付提交参数
|
||||
* @return 扫码支付响应数据,其中包含二维码路径
|
||||
*/
|
||||
@PostMapping
|
||||
@ApiOperation(value = "统一收单线下交易", notes = "统一收单线下交易")
|
||||
@ApiImplicitParam(name = "nativePayDTO", value = "扫码支付提交参数", required = true)
|
||||
public NativePayResponseDTO createDownLineTrading(@RequestBody NativePayDTO nativePayDTO) {
|
||||
TradingEntity tradingEntity = BeanUtil.toBean(nativePayDTO, TradingEntity.class);
|
||||
TradingEntity trading = this.nativePayService.createDownLineTrading(tradingEntity);
|
||||
return BeanUtil.toBean(trading, NativePayResponseDTO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看二维码
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 二维码图片 base64格式
|
||||
*/
|
||||
@GetMapping("qrcode/{tradingOrderNo}")
|
||||
@ApiOperation(value = "查看二维码", notes = "查看二维码")
|
||||
@ApiImplicitParam(name = "tradingOrderNo", value = "查看二维码")
|
||||
public String queryQrCode(@PathVariable("tradingOrderNo") Long tradingOrderNo) {
|
||||
return this.nativePayService.queryQrCodeUrl(tradingOrderNo);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.sl.ms.trade.service.NotifyService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
|
||||
import io.swagger.annotations.Api;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付结果的通知
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@RestController
|
||||
@Api(tags = "支付通知")
|
||||
@RequestMapping("notify")
|
||||
public class NotifyController {
|
||||
|
||||
@Resource
|
||||
private NotifyService notifyService;
|
||||
|
||||
/**
|
||||
* 微信支付成功回调(成功后无需响应内容)
|
||||
*
|
||||
* @param httpEntity 微信请求信息
|
||||
* @param enterpriseId 商户id
|
||||
* @return 正常响应200,否则响应500
|
||||
*/
|
||||
@PostMapping("wx/{enterpriseId}")
|
||||
public ResponseEntity<Object> wxPayNotify(HttpEntity<String> httpEntity, @PathVariable("enterpriseId") Long enterpriseId) {
|
||||
try {
|
||||
//获取请求头
|
||||
HttpHeaders headers = httpEntity.getHeaders();
|
||||
|
||||
//构建微信请求数据对象
|
||||
NotificationRequest request = new NotificationRequest.Builder()
|
||||
.withSerialNumber(headers.getFirst("Wechatpay-Serial")) //证书序列号(微信平台)
|
||||
.withNonce(headers.getFirst("Wechatpay-Nonce")) //随机串
|
||||
.withTimestamp(headers.getFirst("Wechatpay-Timestamp")) //时间戳
|
||||
.withSignature(headers.getFirst("Wechatpay-Signature")) //签名字符串
|
||||
.withBody(httpEntity.getBody())
|
||||
.build();
|
||||
|
||||
//微信通知的业务处理
|
||||
this.notifyService.wxPayNotify(request, enterpriseId);
|
||||
|
||||
} catch (SLException e) {
|
||||
Map<String, Object> result = MapUtil.<String, Object>builder()
|
||||
.put("code", "FAIL")
|
||||
.put("message", e.getMsg())
|
||||
.build();
|
||||
//响应500
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
|
||||
}
|
||||
return ResponseEntity.ok(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝支付成功回调(成功后需要响应success)
|
||||
*
|
||||
* @param enterpriseId 商户id
|
||||
* @return 正常响应200,否则响应500
|
||||
*/
|
||||
@PostMapping("alipay/{enterpriseId}")
|
||||
public ResponseEntity<String> aliPayNotify(HttpServletRequest request,
|
||||
@PathVariable("enterpriseId") Long enterpriseId) {
|
||||
try {
|
||||
//支付宝通知的业务处理
|
||||
this.notifyService.aliPayNotify(request, enterpriseId);
|
||||
} catch (SLException e) {
|
||||
//响应500
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.sl.ms.trade.domain.PayChannelDTO;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
import com.sl.ms.trade.service.PayChannelService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.sl.transport.common.util.PageResponse;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @ClassName PayChannelController.java
|
||||
* @Description 支付通道
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("payChannel")
|
||||
@Slf4j
|
||||
@Api(tags = "支付通道")
|
||||
public class PayChannelController {
|
||||
|
||||
@Resource
|
||||
private PayChannelService payChannelService;
|
||||
|
||||
/**
|
||||
* 支付通道列表
|
||||
*
|
||||
* @param payChannelDTO 查询条件
|
||||
* @return 分页数据对象
|
||||
*/
|
||||
@PostMapping("page/{pageNum}/{pageSize}")
|
||||
@ApiOperation(value = "查询支付通道分页", notes = "查询支付通道分页")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true),
|
||||
@ApiImplicitParam(name = "pageNum", value = "页码"),
|
||||
@ApiImplicitParam(name = "pageSize", value = "每页条数")
|
||||
})
|
||||
public PageResponse<PayChannelDTO> findPayChannelPage(
|
||||
@RequestBody PayChannelDTO payChannelDTO,
|
||||
@PathVariable("pageNum") int pageNum,
|
||||
@PathVariable("pageSize") int pageSize) {
|
||||
Page<PayChannelEntity> payChannelVoPage = payChannelService.findPayChannelPage(payChannelDTO, pageNum, pageSize);
|
||||
return new PageResponse<>(payChannelVoPage, PayChannelDTO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加支付通道
|
||||
*
|
||||
* @param payChannelDTO 对象信息
|
||||
*/
|
||||
@PostMapping
|
||||
@ApiOperation(value = "添加支付通道", notes = "添加支付通道")
|
||||
@ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
|
||||
public void createPayChannel(@RequestBody PayChannelDTO payChannelDTO) {
|
||||
PayChannelEntity payChannel = this.payChannelService.createPayChannel(payChannelDTO);
|
||||
if (null != payChannel) {
|
||||
return;
|
||||
}
|
||||
throw new SLException("添加支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付通道
|
||||
*
|
||||
* @param payChannelDTO 对象信息
|
||||
*/
|
||||
@PatchMapping
|
||||
@ApiOperation(value = "修改支付通道", notes = "修改支付通道")
|
||||
@ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
|
||||
public void updatePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
|
||||
Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
|
||||
if (flag) {
|
||||
return;
|
||||
}
|
||||
throw new SLException("修改支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付通道
|
||||
*
|
||||
* @param payChannelDTO 查询对象
|
||||
*/
|
||||
@DeleteMapping
|
||||
@ApiOperation(value = "删除支付通道", notes = "删除支付通道")
|
||||
@ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
|
||||
public void deletePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
|
||||
String[] checkedIds = payChannelDTO.getCheckedIds();
|
||||
Boolean flag = this.payChannelService.deletePayChannel(checkedIds);
|
||||
if (flag) {
|
||||
return;
|
||||
}
|
||||
throw new SLException("删除支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
}
|
||||
|
||||
@PostMapping("update-payChannel-enableFlag")
|
||||
@ApiOperation(value = "修改支付通道状态", notes = "修改支付通道状态")
|
||||
@ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
|
||||
public void updatePayChannelEnableFlag(@RequestBody PayChannelDTO payChannelDTO) {
|
||||
Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
|
||||
if (flag) {
|
||||
return;
|
||||
}
|
||||
throw new SLException("修改支付通道状态失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.sl.ms.trade.domain.RefundRecordDTO;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.service.RefundRecordService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@Api(tags = "退款单服务")
|
||||
@RequestMapping("refund/records")
|
||||
public class RefundRecordController {
|
||||
|
||||
@Resource
|
||||
private RefundRecordService refundRecordService;
|
||||
|
||||
/**
|
||||
* 根据业务系统订单号 或 交易单号查询退款单 (二个至少传递一个,优先按照交易单号查询)
|
||||
*
|
||||
* @param productOrderNo 业务订单号
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 退款单列表
|
||||
*/
|
||||
@GetMapping
|
||||
@ApiOperation(value = "查询退款单", notes = "根据业务系统订单号 或 交易单号查询退款单 (二个至少传递一个,优先按照交易单号查询)")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "productOrderNo", value = "业务订单号"),
|
||||
@ApiImplicitParam(name = "tradingOrderNo", value = "交易单号")
|
||||
})
|
||||
public List<RefundRecordDTO> findList(@RequestParam(value = "productOrderNo", required = false) Long productOrderNo,
|
||||
@RequestParam(value = "tradingOrderNo", required = false) Long tradingOrderNo) {
|
||||
if (ObjectUtil.isAllEmpty(productOrderNo, tradingOrderNo)) {
|
||||
throw new SLException(TradingEnum.REFUND_QUERY_PARAM_ERROR);
|
||||
}
|
||||
List<RefundRecordEntity> list;
|
||||
if (ObjectUtil.isNotEmpty(tradingOrderNo)) {
|
||||
list = this.refundRecordService.findListByTradingOrderNo(tradingOrderNo);
|
||||
} else {
|
||||
list = this.refundRecordService.findListByProductOrderNo(productOrderNo);
|
||||
}
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
throw new SLException(TradingEnum.REFUND_NOT_FOUND);
|
||||
}
|
||||
return BeanUtil.copyToList(list, RefundRecordDTO.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package com.sl.ms.trade.controller;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@RestController
|
||||
@Api(tags = "交易单服务")
|
||||
@RequestMapping("tradings")
|
||||
public class TradingController {
|
||||
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
|
||||
/**
|
||||
* 根据业务系统订单号 或 交易单号查询交易单 (二个至少传递一个,优先按照交易单号查询)
|
||||
*
|
||||
* @param productOrderNo 业务订单号
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 交易单数据
|
||||
*/
|
||||
@GetMapping
|
||||
@ApiOperation(value = "查询交易单", notes = "根据业务系统订单号 或 交易单号查询交易单 (二个至少传递一个,优先按照交易单号查询)")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "productOrderNo", value = "业务订单号"),
|
||||
@ApiImplicitParam(name = "tradingOrderNo", value = "交易单号")
|
||||
})
|
||||
public TradingDTO queryTrading(@RequestParam(value = "productOrderNo", required = false) Long productOrderNo,
|
||||
@RequestParam(value = "tradingOrderNo", required = false) Long tradingOrderNo) {
|
||||
if (ObjectUtil.isAllEmpty(productOrderNo, tradingOrderNo)) {
|
||||
throw new SLException(TradingEnum.TRADING_QUERY_PARAM_ERROR);
|
||||
}
|
||||
TradingEntity tradingEntity;
|
||||
if (ObjectUtil.isNotEmpty(tradingOrderNo)) {
|
||||
tradingEntity = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
|
||||
} else {
|
||||
tradingEntity = this.tradingService.findTradByProductOrderNo(productOrderNo);
|
||||
}
|
||||
if (ObjectUtil.isEmpty(tradingEntity)) {
|
||||
throw new SLException(TradingEnum.NOT_FOUND);
|
||||
}
|
||||
return BeanUtil.toBean(tradingEntity, TradingDTO.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.sl.ms.trade.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.sl.transport.common.entity.BaseEntity;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Description:交易渠道表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sl_pay_channel")
|
||||
public class PayChannelEntity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = -1452774366739615656L;
|
||||
|
||||
@ApiModelProperty(value = "通道名称")
|
||||
private String channelName;
|
||||
|
||||
@ApiModelProperty(value = "通道唯一标记")
|
||||
private String channelLabel;
|
||||
|
||||
@ApiModelProperty(value = "域名")
|
||||
private String domain;
|
||||
|
||||
@ApiModelProperty(value = "商户appid")
|
||||
private String appId;
|
||||
|
||||
@ApiModelProperty(value = "支付公钥")
|
||||
private String publicKey;
|
||||
|
||||
@ApiModelProperty(value = "商户私钥")
|
||||
private String merchantPrivateKey;
|
||||
|
||||
@ApiModelProperty(value = "其他配置")
|
||||
private String otherConfig;
|
||||
|
||||
@ApiModelProperty(value = "AES混淆密钥")
|
||||
private String encryptKey;
|
||||
|
||||
@ApiModelProperty(value = "说明")
|
||||
private String remark;
|
||||
|
||||
@ApiModelProperty(value = "回调地址")
|
||||
private String notifyUrl;
|
||||
|
||||
@ApiModelProperty(value = "是否有效")
|
||||
protected String enableFlag;
|
||||
|
||||
@ApiModelProperty(value = "商户号")
|
||||
private Long enterpriseId;
|
||||
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package com.sl.ms.trade.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.transport.common.entity.BaseEntity;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* @Description:退款记录表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sl_refund_record")
|
||||
public class RefundRecordEntity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = -3998253241655800061L;
|
||||
|
||||
@ApiModelProperty(value = "交易系统订单号【对于三方来说:商户订单】")
|
||||
private Long tradingOrderNo;
|
||||
|
||||
@ApiModelProperty(value = "业务系统订单号")
|
||||
private Long productOrderNo;
|
||||
|
||||
@ApiModelProperty(value = "本次退款订单号")
|
||||
private Long refundNo;
|
||||
|
||||
@ApiModelProperty(value = "商户号")
|
||||
private Long enterpriseId;
|
||||
|
||||
@ApiModelProperty(value = "退款渠道【支付宝、微信、现金】")
|
||||
private String tradingChannel;
|
||||
|
||||
@ApiModelProperty(value = "退款状态")
|
||||
private RefundStatusEnum refundStatus;
|
||||
|
||||
@ApiModelProperty(value = "返回编码")
|
||||
private String refundCode;
|
||||
|
||||
@ApiModelProperty(value = "返回信息")
|
||||
private String refundMsg;
|
||||
|
||||
@ApiModelProperty(value = "备注【订单门店,桌台信息】")
|
||||
private String memo;
|
||||
|
||||
@ApiModelProperty(value = "本次退款金额")
|
||||
private BigDecimal refundAmount;
|
||||
|
||||
@ApiModelProperty(value = "原订单金额")
|
||||
private BigDecimal total;
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
package com.sl.ms.trade.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.transport.common.entity.BaseEntity;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* @Description:交易订单表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sl_trading")
|
||||
public class TradingEntity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = -3427581867070559590L;
|
||||
|
||||
@ApiModelProperty(value = "openId标识")
|
||||
private String openId;
|
||||
|
||||
@ApiModelProperty(value = "业务系统订单号")
|
||||
private Long productOrderNo;
|
||||
|
||||
@ApiModelProperty(value = "交易系统订单号【对于三方来说:商户订单】")
|
||||
private Long tradingOrderNo;
|
||||
|
||||
@ApiModelProperty(value = "支付渠道【支付宝、微信、现金、免单挂账】")
|
||||
private String tradingChannel;
|
||||
|
||||
@ApiModelProperty(value = "交易类型【付款、退款、免单、挂账】")
|
||||
private String tradingType;
|
||||
|
||||
@ApiModelProperty(value = "交易单状态【DFK待付款,FKZ付款中,QXDD取消订单,YJS已结算,MD免单,GZ挂账】")
|
||||
private TradingStateEnum tradingState;
|
||||
|
||||
@ApiModelProperty(value = "收款人姓名")
|
||||
private String payeeName;
|
||||
|
||||
@ApiModelProperty(value = "收款人账户ID")
|
||||
private Long payeeId;
|
||||
|
||||
@ApiModelProperty(value = "付款人姓名")
|
||||
private String payerName;
|
||||
|
||||
@ApiModelProperty(value = "付款人Id")
|
||||
private Long payerId;
|
||||
|
||||
@ApiModelProperty(value = "交易金额")
|
||||
private BigDecimal tradingAmount;
|
||||
|
||||
@ApiModelProperty(value = "退款金额【付款后】")
|
||||
private BigDecimal refund;
|
||||
|
||||
@ApiModelProperty(value = "是否有退款:YES,NO")
|
||||
private String isRefund;
|
||||
|
||||
@ApiModelProperty(value = "第三方交易返回编码【最终确认交易结果】")
|
||||
private String resultCode;
|
||||
|
||||
@ApiModelProperty(value = "第三方交易返回提示消息【最终确认交易信息】")
|
||||
private String resultMsg;
|
||||
|
||||
@ApiModelProperty(value = "第三方交易返回信息json【分析交易最终信息】")
|
||||
private String resultJson;
|
||||
|
||||
@ApiModelProperty(value = "统一下单返回编码")
|
||||
private String placeOrderCode;
|
||||
|
||||
@ApiModelProperty(value = "统一下单返回信息")
|
||||
private String placeOrderMsg;
|
||||
|
||||
@ApiModelProperty(value = "统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】")
|
||||
private String placeOrderJson;
|
||||
|
||||
@ApiModelProperty(value = "商户号")
|
||||
private Long enterpriseId;
|
||||
|
||||
@ApiModelProperty(value = "备注【订单门店,桌台信息】")
|
||||
private String memo;
|
||||
|
||||
@ApiModelProperty(value = "二维码base64数据")
|
||||
private String qrCode;
|
||||
|
||||
@ApiModelProperty(value = "是否有效")
|
||||
protected String enableFlag;
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
package com.sl.ms.trade.enums;
|
||||
|
||||
import com.sl.transport.common.enums.BaseExceptionEnum;
|
||||
|
||||
/**
|
||||
* 交易枚举
|
||||
*/
|
||||
public enum TradingEnum implements BaseExceptionEnum {
|
||||
|
||||
SUCCEED(1001, 200, "操作成功"),
|
||||
ERROR(1002, "操作失败"),
|
||||
CHECK_TRADING_FAIL(1003, "交易单校验失败"),
|
||||
TRY_LOCK_TRADING_FAIL(1004, "交易单加锁失败"),
|
||||
PAYING_TRADING_FAIL(1005, "交易单支付失败"),
|
||||
TRADING_STATE_SUCCEED(1006, "交易单已完成"),
|
||||
TRADING_STATE_PAYING(1007, "交易单交易中"),
|
||||
CONFIG_EMPTY(1008, "支付配置为空"),
|
||||
CONFIG_ERROR(1009, "支付配置错误"),
|
||||
NATIVE_PAY_FAIL(1010, "统一下单交易失败"),
|
||||
NATIVE_QRCODE_FAIL(1011, "生成二维码失败"),
|
||||
REFUND_FAIL(1012, "查询统一下单交易退款失败"),
|
||||
SAVE_OR_UPDATE_FAIL(1013, "交易单保存或修改失败"),
|
||||
TRADING_TYPE_FAIL(1014, "未定义的交易类型"),
|
||||
NATIVE_QUERY_FAIL(1015, "查询统一下单交易失败"),
|
||||
NATIVE_REFUND_FAIL(1016, "统一下单退款交易失败"),
|
||||
NATIVE_QUERY_REFUND_FAIL(1017, "统一下单查询退款失败"),
|
||||
CASH_PAY_FAIL(1018, "现金交易失败"),
|
||||
CASH_REFUND_FAIL(1019, "统一下单退款交易失败"),
|
||||
CREDIT_PAY_FAIL(1020, "信用交易失败"),
|
||||
LIST_TRADE_STATE_FAIL(1021, "按交易状态查询交易单失败"),
|
||||
NOT_FOUND(1022, "交易单不存在"),
|
||||
CLOSE_FAIL(1023, "关闭交易单失败"),
|
||||
BASIC_REFUND_OUT_FAIL(1024, "退款金额超过订单总金额"),
|
||||
REFUND_NOT_FOUND(1025, "退款记录不存在"),
|
||||
REFUND_ALREADY_COMPLETED(1026, "退款记录已经完成"),
|
||||
BASIC_REFUND_COUNT_OUT_FAIL(1027, "退款次数超出限制,最多20次"),
|
||||
TRADING_QUERY_PARAM_ERROR(1028, "查询交易单错误,订单号或交易单号至少传递一个"),
|
||||
REFUND_QUERY_PARAM_ERROR(1029, "查询退款单错误,订单号或交易单号至少传递一个");
|
||||
|
||||
private Integer code;
|
||||
private Integer status;
|
||||
private String value;
|
||||
|
||||
TradingEnum(Integer code, String value) {
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
this.status = 500;
|
||||
}
|
||||
|
||||
TradingEnum(Integer code, Integer status, String value) {
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package com.sl.ms.trade.handler;
|
||||
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
/**
|
||||
* 基础支付功能的定义,具体业务由不同的支付渠道实现
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface BasicPayHandler {
|
||||
|
||||
/***
|
||||
* 统一收单线下交易查询
|
||||
* 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
|
||||
* @return 是否有变化
|
||||
*/
|
||||
Boolean queryTrading(TradingEntity trading) throws SLException;
|
||||
|
||||
/***
|
||||
* 关闭交易
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean closeTrading(TradingEntity trading) throws SLException;
|
||||
|
||||
/***
|
||||
* 统一收单交易退款接口
|
||||
* 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
|
||||
* 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
|
||||
* @param refundRecord 退款记录对象
|
||||
* @return 是否有变化
|
||||
*/
|
||||
Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException;
|
||||
|
||||
/***
|
||||
* 统一收单交易退款查询接口
|
||||
*
|
||||
* @param refundRecord 退款交易单号
|
||||
* @return 是否有变化
|
||||
*/
|
||||
Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException;
|
||||
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package com.sl.ms.trade.handler;
|
||||
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 交易前置处理接口
|
||||
*/
|
||||
public interface BeforePayHandler {
|
||||
|
||||
|
||||
/***
|
||||
* 支付的交易幂等性处理,如果有问题抛出异常
|
||||
*
|
||||
* @param tradingEntity 交易订单
|
||||
*/
|
||||
void idempotentCreateTrading(TradingEntity tradingEntity) throws SLException;
|
||||
|
||||
/***
|
||||
* 交易单参数校验
|
||||
* @param tradingEntity 交易订单
|
||||
* @return 是否符合要求
|
||||
*/
|
||||
Boolean checkCreateTrading(TradingEntity tradingEntity);
|
||||
|
||||
/***
|
||||
* QueryTrading交易单参数校验
|
||||
* @param trading 交易订单
|
||||
*/
|
||||
void checkQueryTrading(TradingEntity trading);
|
||||
|
||||
/***
|
||||
* RefundTrading退款交易幂等性
|
||||
*/
|
||||
RefundRecordEntity idempotentRefundTrading(TradingEntity trading, BigDecimal refundAmount);
|
||||
|
||||
/***
|
||||
* RefundTrading退款交易单参数校验
|
||||
* @param trading 交易订单
|
||||
*/
|
||||
void checkRefundTrading(TradingEntity trading);
|
||||
|
||||
|
||||
/***
|
||||
* QueryRefundTrading交易单参数校验
|
||||
* @param refundRecord 退款记录
|
||||
*/
|
||||
void checkQueryRefundTrading(RefundRecordEntity refundRecord);
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.sl.ms.trade.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handler工厂,用于获取指定类型的具体渠道的实例对象
|
||||
*/
|
||||
public class HandlerFactory {
|
||||
|
||||
private HandlerFactory() {
|
||||
|
||||
}
|
||||
|
||||
public static <T> T get(PayChannelEnum payChannel, Class<T> handler) {
|
||||
Map<String, T> beans = SpringUtil.getBeansOfType(handler);
|
||||
for (Map.Entry<String, T> entry : beans.entrySet()) {
|
||||
PayChannel payChannelAnnotation = entry.getValue().getClass().getAnnotation(PayChannel.class);
|
||||
if (ObjectUtil.isNotEmpty(payChannelAnnotation) && ObjectUtil.equal(payChannel, payChannelAnnotation.type())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> T get(String payChannel, Class<T> handler) {
|
||||
return get(PayChannelEnum.valueOf(payChannel), handler);
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.sl.ms.trade.handler;
|
||||
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
|
||||
/**
|
||||
* jsapi下单处理
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface JsapiPayHandler {
|
||||
|
||||
/**
|
||||
* 创建交易
|
||||
*
|
||||
* @param tradingEntity 交易单
|
||||
*/
|
||||
void createJsapiTrading(TradingEntity tradingEntity);
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.sl.ms.trade.handler;
|
||||
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
/**
|
||||
* @ClassName NativePayHandler.java
|
||||
* @Description Native支付方式Handler:商户生成二维码,用户扫描支付
|
||||
*/
|
||||
public interface NativePayHandler {
|
||||
|
||||
|
||||
/***
|
||||
* @description 统一收单线下交易预创建
|
||||
* 收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。
|
||||
* @param tradingEntity 交易单
|
||||
*/
|
||||
void createDownLineTrading(TradingEntity tradingEntity) throws SLException;
|
||||
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package com.sl.ms.trade.handler.alipay;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.easysdk.factory.Factory;
|
||||
import com.alipay.easysdk.kernel.Config;
|
||||
import com.alipay.easysdk.kernel.util.ResponseChecker;
|
||||
import com.alipay.easysdk.payment.common.models.AlipayTradeCloseResponse;
|
||||
import com.alipay.easysdk.payment.common.models.AlipayTradeFastpayRefundQueryResponse;
|
||||
import com.alipay.easysdk.payment.common.models.AlipayTradeQueryResponse;
|
||||
import com.alipay.easysdk.payment.common.models.AlipayTradeRefundResponse;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.BasicPayHandler;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付宝基础支付功能的实现
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("aliBasicPayHandler")
|
||||
@PayChannel(type = PayChannelEnum.ALI_PAY)
|
||||
public class AliBasicPayHandler implements BasicPayHandler {
|
||||
|
||||
@Override
|
||||
public Boolean queryTrading(TradingEntity trading) throws SLException {
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(trading.getEnterpriseId());
|
||||
//Factory使用配置
|
||||
Factory.setOptions(config);
|
||||
AlipayTradeQueryResponse queryResponse;
|
||||
try {
|
||||
//调用支付宝API:通用查询支付情况
|
||||
queryResponse = Factory
|
||||
.Payment
|
||||
.Common()
|
||||
.query(String.valueOf(trading.getTradingOrderNo()));
|
||||
} catch (Exception e) {
|
||||
String msg = StrUtil.format("查询支付宝统一下单失败:trading = {}", trading);
|
||||
log.error(msg, e);
|
||||
throw new SLException(msg, TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
|
||||
}
|
||||
|
||||
//修改交易单状态
|
||||
trading.setResultCode(queryResponse.getSubCode());
|
||||
trading.setResultMsg(queryResponse.getSubMsg());
|
||||
trading.setResultJson(JSONUtil.toJsonStr(queryResponse));
|
||||
|
||||
boolean success = ResponseChecker.success(queryResponse);
|
||||
//响应成功,分析交易状态
|
||||
if (success) {
|
||||
String tradeStatus = queryResponse.getTradeStatus();
|
||||
if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatus)) {
|
||||
//支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
|
||||
trading.setTradingState(TradingStateEnum.QXDD);
|
||||
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) {
|
||||
// TRADE_SUCCESS(交易支付成功)
|
||||
// TRADE_FINISHED(交易结束,不可退款)
|
||||
trading.setTradingState(TradingStateEnum.YJS);
|
||||
} else {
|
||||
//非最终状态不处理,当前交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)不处理
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
throw new SLException(trading.getResultJson(), TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean closeTrading(TradingEntity trading) throws SLException {
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(trading.getEnterpriseId());
|
||||
//Factory使用配置
|
||||
Factory.setOptions(config);
|
||||
try {
|
||||
//调用支付宝API:通用查询支付情况
|
||||
AlipayTradeCloseResponse closeResponse = Factory
|
||||
.Payment
|
||||
.Common()
|
||||
.close(String.valueOf(trading.getTradingOrderNo()));
|
||||
boolean success = ResponseChecker.success(closeResponse);
|
||||
if (success) {
|
||||
trading.setTradingState(TradingStateEnum.QXDD);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
throw new SLException(TradingEnum.CLOSE_FAIL, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
|
||||
//Factory使用配置
|
||||
Factory.setOptions(config);
|
||||
//调用支付宝API:通用查询支付情况
|
||||
AlipayTradeRefundResponse refundResponse;
|
||||
try {
|
||||
// 支付宝easy sdk
|
||||
refundResponse = Factory
|
||||
.Payment
|
||||
.Common()
|
||||
//扩展参数:退款单号
|
||||
.optional("out_request_no", refundRecord.getRefundNo())
|
||||
.refund(Convert.toStr(refundRecord.getTradingOrderNo()),
|
||||
Convert.toStr(refundRecord.getRefundAmount()));
|
||||
} catch (Exception e) {
|
||||
String msg = StrUtil.format("调用支付宝退款接口出错!refundRecord = {}", refundRecord);
|
||||
log.error(msg, e);
|
||||
throw new SLException(msg, TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
|
||||
}
|
||||
refundRecord.setRefundCode(refundResponse.getSubCode());
|
||||
refundRecord.setRefundMsg(JSONUtil.toJsonStr(refundResponse));
|
||||
boolean success = ResponseChecker.success(refundResponse);
|
||||
if (success) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
|
||||
return true;
|
||||
}
|
||||
throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
|
||||
//Factory使用配置
|
||||
Factory.setOptions(config);
|
||||
AlipayTradeFastpayRefundQueryResponse response;
|
||||
try {
|
||||
response = Factory.Payment.Common().queryRefund(
|
||||
Convert.toStr(refundRecord.getTradingOrderNo()),
|
||||
Convert.toStr(refundRecord.getRefundNo()));
|
||||
} catch (Exception e) {
|
||||
log.error("调用支付宝查询退款接口出错!refundRecord = {}", refundRecord, e);
|
||||
throw new SLException(TradingEnum.NATIVE_REFUND_FAIL, e);
|
||||
}
|
||||
|
||||
refundRecord.setRefundCode(response.getSubCode());
|
||||
refundRecord.setRefundMsg(JSONUtil.toJsonStr(response));
|
||||
boolean success = ResponseChecker.success(response);
|
||||
if (success) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
|
||||
return true;
|
||||
}
|
||||
throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package com.sl.ms.trade.handler.alipay;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.easysdk.factory.Factory;
|
||||
import com.alipay.easysdk.kernel.Config;
|
||||
import com.alipay.easysdk.kernel.util.ResponseChecker;
|
||||
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.NativePayHandler;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付宝的扫描支付的具体实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("aliNativePayHandler")
|
||||
@PayChannel(type = PayChannelEnum.ALI_PAY)
|
||||
public class AliNativePayHandler implements NativePayHandler {
|
||||
|
||||
@Override
|
||||
public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(tradingEntity.getEnterpriseId());
|
||||
//Factory使用配置
|
||||
Factory.setOptions(config);
|
||||
AlipayTradePrecreateResponse response;
|
||||
try {
|
||||
//调用支付宝API面对面支付
|
||||
response = Factory
|
||||
.Payment
|
||||
.FaceToFace()
|
||||
.preCreate(tradingEntity.getMemo(), //订单描述
|
||||
Convert.toStr(tradingEntity.getTradingOrderNo()), //业务订单号
|
||||
Convert.toStr(tradingEntity.getTradingAmount())); //金额
|
||||
} catch (Exception e) {
|
||||
log.error("支付宝统一下单创建失败:tradingEntity = {}", tradingEntity, e);
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL, e);
|
||||
}
|
||||
|
||||
//受理结果【只表示请求是否成功,而不是支付是否成功】
|
||||
boolean isSuccess = ResponseChecker.success(response);
|
||||
//6.1、受理成功:修改交易单
|
||||
if (isSuccess) {
|
||||
String subCode = response.getSubCode();
|
||||
String subMsg = response.getQrCode();
|
||||
tradingEntity.setPlaceOrderCode(subCode); //返回的编码
|
||||
tradingEntity.setPlaceOrderMsg(subMsg); //二维码需要展现的信息
|
||||
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
|
||||
tradingEntity.setTradingState(TradingStateEnum.FKZ);
|
||||
return;
|
||||
}
|
||||
throw new SLException(JSONUtil.toJsonStr(response), TradingEnum.NATIVE_PAY_FAIL.getCode(), TradingEnum.NATIVE_PAY_FAIL.getStatus());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
package com.sl.ms.trade.handler.alipay;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.alipay.easysdk.kernel.Config;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.service.PayChannelService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
/**
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public class AlipayConfig {
|
||||
|
||||
/**
|
||||
* 将支付渠道配置转化为支付宝的配置
|
||||
*
|
||||
* @param enterpriseId 商户ID
|
||||
* @return 支付宝的配置
|
||||
*/
|
||||
public static Config getConfig(Long enterpriseId) {
|
||||
// 查询配置
|
||||
PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
|
||||
PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_ALI_PAY);
|
||||
|
||||
if (ObjectUtil.isEmpty(payChannel)) {
|
||||
throw new SLException(TradingEnum.CONFIG_EMPTY);
|
||||
}
|
||||
|
||||
Config config = new Config();
|
||||
config.protocol = "https";
|
||||
config.gatewayHost = payChannel.getDomain();
|
||||
config.signType = "RSA2";
|
||||
config.appId = payChannel.getAppId();
|
||||
//配置应用私钥
|
||||
config.merchantPrivateKey = payChannel.getMerchantPrivateKey();
|
||||
//配置支付宝公钥
|
||||
config.alipayPublicKey = payChannel.getPublicKey();
|
||||
//可设置异步通知接收服务地址(可选)
|
||||
config.notifyUrl = payChannel.getNotifyUrl();
|
||||
//设置AES密钥,调用AES加解密相关接口时需要(可选)
|
||||
config.encryptKey = payChannel.getEncryptKey();
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
package com.sl.ms.trade.handler.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.BeforePayHandler;
|
||||
import com.sl.ms.trade.service.RefundRecordService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.sl.transport.common.util.ObjectUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 交易前置处理接口
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
public class BeforePayHandlerImpl implements BeforePayHandler {
|
||||
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private IdentifierGenerator identifierGenerator;
|
||||
@Resource
|
||||
private RefundRecordService refundRecordService;
|
||||
|
||||
@Override
|
||||
public void idempotentCreateTrading(TradingEntity tradingEntity) throws SLException {
|
||||
TradingEntity trading = tradingService.findTradByProductOrderNo(tradingEntity.getProductOrderNo());
|
||||
if (ObjectUtil.isEmpty(trading)) {
|
||||
//新交易单,生成交易号
|
||||
tradingEntity.setTradingOrderNo((Long) identifierGenerator.nextId(tradingEntity));
|
||||
return;
|
||||
}
|
||||
|
||||
TradingStateEnum tradingState = trading.getTradingState();
|
||||
if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.YJS, TradingStateEnum.MD)) {
|
||||
//已结算、免单:直接抛出重复支付异常
|
||||
throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
|
||||
} else if (ObjectUtil.equals(TradingStateEnum.FKZ, tradingState)) {
|
||||
//付款中,如果支付渠道一致,说明是重复,抛出支付中异常,否则需要更换支付渠道
|
||||
//举例:第一次通过支付宝付款,付款中用户取消,改换了微信支付
|
||||
if (StrUtil.equals(trading.getTradingChannel(), tradingEntity.getTradingChannel())) {
|
||||
throw new SLException(TradingEnum.TRADING_STATE_PAYING);
|
||||
} else {
|
||||
tradingEntity.setId(trading.getId()); // id设置为原订单的id
|
||||
//重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
|
||||
tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
|
||||
}
|
||||
} else if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.QXDD, TradingStateEnum.GZ)) {
|
||||
//取消订单,挂账:创建交易号,对原交易单发起支付
|
||||
tradingEntity.setId(trading.getId()); // id设置为原订单的id
|
||||
//重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
|
||||
tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
|
||||
} else {
|
||||
//其他情况:直接交易失败
|
||||
throw new SLException(TradingEnum.PAYING_TRADING_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean checkCreateTrading(TradingEntity tradingEntity) {
|
||||
//校验不为为空,订单备注、订单号、企业号、交易金额、支付渠道
|
||||
boolean flag = ObjectUtil.isAllNotEmpty(tradingEntity,
|
||||
tradingEntity.getMemo(),
|
||||
tradingEntity.getProductOrderNo(),
|
||||
tradingEntity.getEnterpriseId(),
|
||||
tradingEntity.getTradingAmount(),
|
||||
tradingEntity.getTradingChannel());
|
||||
|
||||
if (!flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//金额不能小于等于0
|
||||
return !NumberUtil.isLessOrEqual(tradingEntity.getTradingAmount(), BigDecimal.valueOf(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkQueryTrading(TradingEntity trading) {
|
||||
if (ObjectUtil.isEmpty(trading)) {
|
||||
throw new SLException(TradingEnum.NOT_FOUND);
|
||||
}
|
||||
|
||||
//校验交易单是否已经完成或已取消
|
||||
TradingStateEnum tradingState = trading.getTradingState();
|
||||
if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.YJS, TradingStateEnum.QXDD)) {
|
||||
throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundRecordEntity idempotentRefundTrading(TradingEntity trading, BigDecimal refundAmount) {
|
||||
//查询退款次数,不能大于20次
|
||||
List<RefundRecordEntity> recordList = this.refundRecordService.findListByTradingOrderNo(trading.getTradingOrderNo());
|
||||
int size = CollUtil.size(recordList);
|
||||
if (size >= 20) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RefundRecordEntity refundRecord = new RefundRecordEntity();
|
||||
//退款单号
|
||||
refundRecord.setRefundNo(Convert.toLong(this.identifierGenerator.nextId(refundRecord)));
|
||||
refundRecord.setTradingOrderNo(trading.getTradingOrderNo());
|
||||
refundRecord.setProductOrderNo(trading.getProductOrderNo());
|
||||
refundRecord.setRefundAmount(refundAmount);
|
||||
refundRecord.setEnterpriseId(trading.getEnterpriseId());
|
||||
refundRecord.setTradingChannel(trading.getTradingChannel());
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
|
||||
refundRecord.setTotal(trading.getTradingAmount());
|
||||
String memo = StrUtil.format("退款({})", size + 1);
|
||||
refundRecord.setMemo(memo);
|
||||
|
||||
return refundRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkRefundTrading(TradingEntity trading) {
|
||||
if (ObjectUtil.isEmpty(trading)) {
|
||||
throw new SLException(TradingEnum.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (trading.getTradingState() != TradingStateEnum.YJS) {
|
||||
throw new SLException(TradingEnum.NATIVE_REFUND_FAIL);
|
||||
}
|
||||
|
||||
//退款总金额不可超实付总金额
|
||||
if (NumberUtil.isGreater(trading.getRefund(), trading.getTradingAmount())) {
|
||||
throw new SLException(TradingEnum.BASIC_REFUND_OUT_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkQueryRefundTrading(RefundRecordEntity refundRecord) {
|
||||
if (ObjectUtil.isEmpty(refundRecord)) {
|
||||
throw new SLException(TradingEnum.REFUND_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (ObjectUtil.equals(refundRecord.getRefundStatus(), RefundStatusEnum.SUCCESS)) {
|
||||
throw new SLException(TradingEnum.REFUND_ALREADY_COMPLETED);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
package com.sl.ms.trade.handler.wechat;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.BasicPayHandler;
|
||||
import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.sl.ms.trade.enums.TradingEnum.*;
|
||||
|
||||
/**
|
||||
* 微信基础支付功能的实现
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("weChatBasicPayHandler")
|
||||
@PayChannel(type = PayChannelEnum.WECHAT_PAY)
|
||||
public class WeChatBasicPayHandler implements BasicPayHandler {
|
||||
|
||||
@Override
|
||||
public Boolean queryTrading(TradingEntity trading) throws SLException {
|
||||
// 获取微信支付的client对象
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId());
|
||||
|
||||
//请求地址
|
||||
String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}", trading.getTradingOrderNo());
|
||||
|
||||
//请求参数
|
||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
||||
.put("mchid", client.getMchId())
|
||||
.build();
|
||||
|
||||
WeChatResponse response;
|
||||
try {
|
||||
response = client.doGet(apiPath, params);
|
||||
} catch (Exception e) {
|
||||
log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
|
||||
throw new SLException(NATIVE_REFUND_FAIL, e);
|
||||
}
|
||||
if (response.isOk()) {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
|
||||
// 交易状态,枚举值:
|
||||
// SUCCESS:支付成功
|
||||
// REFUND:转入退款
|
||||
// NOTPAY:未支付
|
||||
// CLOSED:已关闭
|
||||
// REVOKED:已撤销(仅付款码支付会返回)
|
||||
// USERPAYING:用户支付中(仅付款码支付会返回)
|
||||
// PAYERROR:支付失败(仅付款码支付会返回)
|
||||
String tradeStatus = jsonObject.getStr("trade_state");
|
||||
if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) {
|
||||
trading.setTradingState(TradingStateEnum.QXDD);
|
||||
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_REFUND_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) {
|
||||
trading.setTradingState(TradingStateEnum.YJS);
|
||||
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_NOTPAY)) {
|
||||
//如果是未支付,需要判断下时间,超过2小时未知的订单需要关闭订单以及设置状态为QXDD
|
||||
long between = LocalDateTimeUtil.between(trading.getCreated(), LocalDateTimeUtil.now(), ChronoUnit.HOURS);
|
||||
if (between >= 2) {
|
||||
return this.closeTrading(trading);
|
||||
}
|
||||
} else {
|
||||
//非最终状态不处理
|
||||
return false;
|
||||
}
|
||||
//修改交易单状态
|
||||
trading.setResultCode(tradeStatus);
|
||||
trading.setResultMsg(jsonObject.getStr("trade_state_desc"));
|
||||
trading.setResultJson(response.getBody());
|
||||
return true;
|
||||
}
|
||||
throw new SLException(response.getBody(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean closeTrading(TradingEntity trading) throws SLException {
|
||||
// 获取微信支付的client对象
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId());
|
||||
//请求地址
|
||||
String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}/close", trading.getTradingOrderNo());
|
||||
//请求参数
|
||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
||||
.put("mchid", client.getMchId())
|
||||
.build();
|
||||
try {
|
||||
WeChatResponse response = client.doPost(apiPath, params);
|
||||
if (response.getStatus() == 204) {
|
||||
trading.setTradingState(TradingStateEnum.QXDD);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
throw new SLException(CLOSE_FAIL, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
|
||||
// 获取微信支付的client对象
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
|
||||
//请求地址
|
||||
String apiPath = "/v3/refund/domestic/refunds";
|
||||
//请求参数
|
||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
||||
.put("out_refund_no", Convert.toStr(refundRecord.getRefundNo()))
|
||||
.put("out_trade_no", Convert.toStr(refundRecord.getTradingOrderNo()))
|
||||
.put("amount", MapUtil.<String, Object>builder()
|
||||
.put("refund", NumberUtil.mul(refundRecord.getRefundAmount(), 100)) //本次退款金额
|
||||
.put("total", NumberUtil.mul(refundRecord.getTotal(), 100)) //原订单金额
|
||||
.put("currency", "CNY") //币种
|
||||
.build())
|
||||
.build();
|
||||
WeChatResponse response;
|
||||
try {
|
||||
response = client.doPost(apiPath, params);
|
||||
} catch (Exception e) {
|
||||
log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
|
||||
throw new SLException(NATIVE_REFUND_FAIL, e);
|
||||
}
|
||||
refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
|
||||
refundRecord.setRefundMsg(response.getBody());
|
||||
if (response.isOk()) {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
|
||||
// SUCCESS:退款成功
|
||||
// CLOSED:退款关闭
|
||||
// PROCESSING:退款处理中
|
||||
// ABNORMAL:退款异常
|
||||
String status = jsonObject.getStr("status");
|
||||
if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
|
||||
} else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
|
||||
} else {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
throw new SLException(refundRecord.getRefundMsg(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
|
||||
// 获取微信支付的client对象
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
|
||||
|
||||
//请求地址
|
||||
String apiPath = StrUtil.format("/v3/refund/domestic/refunds/{}", refundRecord.getRefundNo());
|
||||
|
||||
WeChatResponse response;
|
||||
try {
|
||||
response = client.doGet(apiPath);
|
||||
} catch (Exception e) {
|
||||
log.error("调用微信接口出错!apiPath = {}", apiPath, e);
|
||||
throw new SLException(NATIVE_QUERY_REFUND_FAIL, e);
|
||||
}
|
||||
|
||||
refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
|
||||
refundRecord.setRefundMsg(response.getBody());
|
||||
if (response.isOk()) {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
|
||||
// SUCCESS:退款成功
|
||||
// CLOSED:退款关闭
|
||||
// PROCESSING:退款处理中
|
||||
// ABNORMAL:退款异常
|
||||
String status = jsonObject.getStr("status");
|
||||
if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
|
||||
} else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
|
||||
} else {
|
||||
refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
throw new SLException(response.getBody(), NATIVE_QUERY_REFUND_FAIL.getCode(), NATIVE_QUERY_REFUND_FAIL.getStatus());
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
package com.sl.ms.trade.handler.wechat;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.JsapiPayHandler;
|
||||
import com.sl.ms.trade.handler.wechat.bean.JsapiPayParam;
|
||||
import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信jsapi的实现
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component("wechatJsapiPayHandler")
|
||||
@PayChannel(type = PayChannelEnum.WECHAT_PAY)
|
||||
public class WechatJsapiPayHandler implements JsapiPayHandler {
|
||||
|
||||
@Override
|
||||
public void createJsapiTrading(TradingEntity tradingEntity) {
|
||||
// 查询配置
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId());
|
||||
//请求地址
|
||||
String apiPath = "/v3/pay/transactions/jsapi";
|
||||
//请求参数
|
||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
||||
.put("mchid", client.getMchId())
|
||||
.put("appid", client.getAppId())
|
||||
.put("description", tradingEntity.getMemo())
|
||||
.put("notify_url", client.getNotifyUrl())
|
||||
.put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo()))
|
||||
.put("amount", MapUtil.<String, Object>builder()
|
||||
.put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分
|
||||
.put("currency", "CNY") //人民币
|
||||
.build())
|
||||
.put("payer", MapUtil.<String, Object>builder()
|
||||
.put("openid", tradingEntity.getOpenId()) //用户识别标识
|
||||
.build())
|
||||
.build();
|
||||
try {
|
||||
WeChatResponse response = client.doPost(apiPath, params);
|
||||
if (!response.isOk()) {
|
||||
//下单失败
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
}
|
||||
//指定统一下单code
|
||||
tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));
|
||||
//jsapi发起支付需要的预支付id
|
||||
tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("prepay_id"));
|
||||
|
||||
//指定交易状态
|
||||
tradingEntity.setTradingState(TradingStateEnum.FKZ);
|
||||
|
||||
//封装JSAPI调起支付的参数(给前端使用)
|
||||
Long timeStamp = System.currentTimeMillis() / 1000;
|
||||
String nonceStr = IdUtil.simpleUUID();
|
||||
String packages = "prepay_id=" + tradingEntity.getPlaceOrderMsg();
|
||||
JsapiPayParam jsapiPayParam = JsapiPayParam.builder()
|
||||
.timeStamp(timeStamp)
|
||||
.appId(client.getAppId())
|
||||
.nonceStr(nonceStr)
|
||||
.packages(packages)
|
||||
.paySign(this.createPaySign(client, timeStamp, nonceStr, packages))
|
||||
.build();
|
||||
|
||||
//设置jsapi调起支付的参数
|
||||
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(jsapiPayParam));
|
||||
} catch (Exception e) {
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成
|
||||
*
|
||||
* @param client 微信client对象
|
||||
* @param timeStamp 时间戳
|
||||
* @param nonceStr 随机数
|
||||
* @param packages 预支付字符串
|
||||
* @return 签名字符串
|
||||
* @throws Exception 不处理异常,全部抛出
|
||||
*/
|
||||
private String createPaySign(WechatPayHttpClient client, Long timeStamp, String nonceStr, String packages) throws Exception {
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
// 加载商户私钥
|
||||
PrivateKey privateKey = PemUtil
|
||||
.loadPrivateKey(new ByteArrayInputStream(client.getPrivateKey().getBytes(CharsetUtil.CHARSET_UTF_8)));
|
||||
sign.initSign(privateKey);
|
||||
String message = StrUtil.format("{}\n{}\n{}\n{}\n",
|
||||
client.getAppId(),
|
||||
timeStamp,
|
||||
nonceStr,
|
||||
packages);
|
||||
sign.update(message.getBytes());
|
||||
return Base64.getEncoder().encodeToString(sign.sign());
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package com.sl.ms.trade.handler.wechat;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.sl.ms.trade.annotation.PayChannel;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.NativePayHandler;
|
||||
import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信二维码支付
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component("wechatNativePayHandler")
|
||||
@PayChannel(type = PayChannelEnum.WECHAT_PAY)
|
||||
public class WechatNativePayHandler implements NativePayHandler {
|
||||
|
||||
@Override
|
||||
public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
|
||||
// 查询配置
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId());
|
||||
//请求地址
|
||||
String apiPath = "/v3/pay/transactions/native";
|
||||
|
||||
//请求参数
|
||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
||||
.put("mchid", client.getMchId())
|
||||
.put("appid", client.getAppId())
|
||||
.put("description", tradingEntity.getMemo())
|
||||
.put("notify_url", client.getNotifyUrl())
|
||||
.put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo()))
|
||||
.put("amount", MapUtil.<String, Object>builder()
|
||||
.put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分
|
||||
.put("currency", "CNY") //人民币
|
||||
.build())
|
||||
.build();
|
||||
|
||||
try {
|
||||
WeChatResponse response = client.doPost(apiPath, params);
|
||||
if (!response.isOk()) {
|
||||
//下单失败
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
}
|
||||
//指定统一下单code
|
||||
tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));
|
||||
//二维码需要展现的信息
|
||||
tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url"));
|
||||
//指定统一下单json字符串
|
||||
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
|
||||
//指定交易状态
|
||||
tradingEntity.setTradingState(TradingStateEnum.FKZ);
|
||||
} catch (Exception e) {
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
package com.sl.ms.trade.handler.wechat;
|
||||
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.net.url.UrlPath;
|
||||
import cn.hutool.core.net.url.UrlQuery;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
|
||||
import com.sl.ms.trade.service.PayChannelService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
|
||||
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
|
||||
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
|
||||
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
|
||||
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URI;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付远程调用对象
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WechatPayHttpClient {
|
||||
|
||||
private String mchId; //商户号
|
||||
private String appId; //商户号
|
||||
private String privateKey; //私钥字符串
|
||||
private String mchSerialNo; //商户证书序列号
|
||||
private String apiV3Key; //V3密钥
|
||||
private String domain; //请求域名
|
||||
private String notifyUrl; //请求地址
|
||||
|
||||
public static WechatPayHttpClient get(Long enterpriseId) {
|
||||
// 查询配置
|
||||
PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
|
||||
PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_WECHAT_PAY);
|
||||
|
||||
if (ObjectUtil.isEmpty(payChannel)) {
|
||||
throw new SLException(TradingEnum.CONFIG_EMPTY);
|
||||
}
|
||||
|
||||
//通过渠道对象转化成微信支付的client对象
|
||||
JSONObject otherConfig = JSONUtil.parseObj(payChannel.getOtherConfig());
|
||||
return WechatPayHttpClient.builder()
|
||||
.appId(payChannel.getAppId())
|
||||
.domain(payChannel.getDomain())
|
||||
.privateKey(payChannel.getMerchantPrivateKey())
|
||||
.mchId(otherConfig.getStr("mchId"))
|
||||
.mchSerialNo(otherConfig.getStr("mchSerialNo"))
|
||||
.apiV3Key(otherConfig.getStr("apiV3Key"))
|
||||
.notifyUrl(payChannel.getNotifyUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
/***
|
||||
* 构建CloseableHttpClient远程请求对象
|
||||
* @return {@link CloseableHttpClient}
|
||||
*/
|
||||
public CloseableHttpClient createHttpClient() throws Exception {
|
||||
// 加载商户私钥(privateKey:私钥字符串)
|
||||
PrivateKey merchantPrivateKey = PemUtil
|
||||
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
|
||||
|
||||
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
|
||||
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey);
|
||||
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(
|
||||
mchId, privateKeySigner);
|
||||
|
||||
// 向证书管理器增加需要自动更新平台证书的商户信息
|
||||
CertificatesManager certificatesManager = CertificatesManager.getInstance();
|
||||
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes("utf-8"));
|
||||
|
||||
// 初始化httpClient
|
||||
return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create()
|
||||
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
|
||||
.withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId)))
|
||||
.build();
|
||||
}
|
||||
|
||||
/***
|
||||
* 支持post请求的远程调用
|
||||
* @param apiPath api地址
|
||||
* @param params 携带请求参数
|
||||
* @return 返回字符串
|
||||
*/
|
||||
public WeChatResponse doPost(String apiPath, Map<String, Object> params) throws Exception {
|
||||
String url = StrUtil.format("https://{}{}", this.domain, apiPath);
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.addHeader("Accept", "application/json");
|
||||
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
|
||||
|
||||
String body = JSONUtil.toJsonStr(params);
|
||||
httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8));
|
||||
|
||||
CloseableHttpResponse response = this.createHttpClient().execute(httpPost);
|
||||
return new WeChatResponse(response);
|
||||
}
|
||||
|
||||
/***
|
||||
* 支持get请求的远程调用
|
||||
* @param apiPath api地址
|
||||
* @param params 在路径中请求的参数
|
||||
* @return 返回字符串
|
||||
*/
|
||||
public WeChatResponse doGet(String apiPath, Map<String, Object> params) throws Exception {
|
||||
URI uri = UrlBuilder.create()
|
||||
.setHost(this.domain)
|
||||
.setScheme("https")
|
||||
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
|
||||
.setQuery(UrlQuery.of(params))
|
||||
.setCharset(CharsetUtil.CHARSET_UTF_8)
|
||||
.toURI();
|
||||
return this.doGet(uri);
|
||||
}
|
||||
|
||||
/***
|
||||
* 支持get请求的远程调用
|
||||
* @param apiPath api地址
|
||||
* @return 返回字符串
|
||||
*/
|
||||
public WeChatResponse doGet(String apiPath) throws Exception {
|
||||
URI uri = UrlBuilder.create()
|
||||
.setHost(this.domain)
|
||||
.setScheme("https")
|
||||
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
|
||||
.setCharset(CharsetUtil.CHARSET_UTF_8)
|
||||
.toURI();
|
||||
return this.doGet(uri);
|
||||
}
|
||||
|
||||
private WeChatResponse doGet(URI uri) throws Exception {
|
||||
HttpGet httpGet = new HttpGet(uri);
|
||||
httpGet.addHeader("Accept", "application/json");
|
||||
CloseableHttpResponse response = this.createHttpClient().execute(httpGet);
|
||||
return new WeChatResponse(response);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
package com.sl.ms.trade.handler.wechat.bean;
|
||||
|
||||
import cn.hutool.core.annotation.Alias;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JsapiPayParam {
|
||||
|
||||
/**
|
||||
* 由微信生成的应用ID,全局唯一。
|
||||
* 请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的服务号APPID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 时间戳,标准北京时间,时区为东八区,
|
||||
* 自1970年1月1日 0点0分0秒以来的秒数。
|
||||
* 注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。
|
||||
*/
|
||||
private Long timeStamp;
|
||||
|
||||
/**
|
||||
* 随机字符串,不长于32位。
|
||||
*/
|
||||
private String nonceStr;
|
||||
|
||||
/**
|
||||
* JSAPI下单接口返回的prepay_id参数值,提交格式如:
|
||||
* prepay_id=wx201410272009395522657a690389285100
|
||||
*/
|
||||
@Alias("package")
|
||||
private String packages;
|
||||
|
||||
/**
|
||||
* 签名类型,默认为RSA,仅支持RSA。
|
||||
*/
|
||||
private String signType = "RSA";
|
||||
|
||||
/**
|
||||
* 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
|
||||
*/
|
||||
private String paySign;
|
||||
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.sl.ms.trade.handler.wechat.response;
|
||||
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import lombok.Data;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
/**
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Data
|
||||
public class WeChatResponse {
|
||||
|
||||
private int status; //响应状态码
|
||||
private String body; //响应体数据
|
||||
|
||||
public WeChatResponse() {
|
||||
|
||||
}
|
||||
|
||||
public WeChatResponse(CloseableHttpResponse response) {
|
||||
this.status = response.getStatusLine().getStatusCode();
|
||||
try {
|
||||
this.body = EntityUtils.toString(response.getEntity(), CharsetUtil.UTF_8);
|
||||
} catch (Exception e) {
|
||||
// 如果出现异常,响应体为null
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean isOk() {
|
||||
return this.status == 200;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
package com.sl.ms.trade.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.sl.ms.base.api.common.MQFeign;
|
||||
import com.sl.ms.trade.domain.RefundRecordDTO;
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.service.BasicPayService;
|
||||
import com.sl.ms.trade.service.RefundRecordService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.constant.Constants;
|
||||
import com.sl.transport.common.vo.TradeStatusMsg;
|
||||
import com.xxl.job.core.context.XxlJobHelper;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 交易任务,主要是查询订单的支付状态 和 退款的成功状态
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TradeJob {
|
||||
|
||||
@Value("${sl.job.trading.count:100}")
|
||||
private Integer tradingCount;
|
||||
@Value("${sl.job.refund.count:100}")
|
||||
private Integer refundCount;
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private RefundRecordService refundRecordService;
|
||||
@Resource
|
||||
private BasicPayService basicPayService;
|
||||
@Resource
|
||||
private MQFeign mqFeign;
|
||||
|
||||
/**
|
||||
* 分片广播方式查询支付状态
|
||||
* 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理
|
||||
*/
|
||||
@XxlJob("tradingJob")
|
||||
public void tradingJob() {
|
||||
// 分片参数
|
||||
int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
|
||||
int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
|
||||
|
||||
List<TradingEntity> list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
|
||||
return;
|
||||
}
|
||||
|
||||
//定义消息通知列表,只要是状态不为【付款中】就需要通知其他系统
|
||||
List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
|
||||
for (TradingEntity trading : list) {
|
||||
if (trading.getTradingOrderNo() % shardTotal != shardIndex) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
//查询交易单
|
||||
TradingDTO tradingDTO = this.basicPayService.queryTrading(trading.getTradingOrderNo());
|
||||
if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {
|
||||
TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
|
||||
.tradingOrderNo(trading.getTradingOrderNo())
|
||||
.productOrderNo(trading.getProductOrderNo())
|
||||
.statusCode(tradingDTO.getTradingState().getCode())
|
||||
.statusName(tradingDTO.getTradingState().name())
|
||||
.build();
|
||||
tradeMsgList.add(tradeStatusMsg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollUtil.isEmpty(tradeMsgList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//发送消息通知其他系统
|
||||
String msg = JSONUtil.toJsonStr(tradeMsgList);
|
||||
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片广播方式查询退款状态
|
||||
*/
|
||||
@XxlJob("refundJob")
|
||||
public void refundJob() {
|
||||
// 分片参数
|
||||
int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
|
||||
int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
|
||||
|
||||
List<RefundRecordEntity> list = this.refundRecordService.findListByRefundStatus(RefundStatusEnum.SENDING, refundCount);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
XxlJobHelper.log("查询到退款单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
|
||||
return;
|
||||
}
|
||||
|
||||
//定义消息通知列表,只要是状态不为【退款中】就需要通知其他系统
|
||||
List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
|
||||
|
||||
for (RefundRecordEntity refundRecord : list) {
|
||||
if (refundRecord.getRefundNo() % shardTotal != shardIndex) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
//查询退款单
|
||||
RefundRecordDTO refundRecordDTO = this.basicPayService.queryRefundTrading(refundRecord.getRefundNo());
|
||||
if (RefundStatusEnum.SENDING != refundRecordDTO.getRefundStatus()) {
|
||||
TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
|
||||
.tradingOrderNo(refundRecord.getTradingOrderNo())
|
||||
.productOrderNo(refundRecord.getProductOrderNo())
|
||||
.refundNo(refundRecord.getRefundNo())
|
||||
.statusCode(refundRecord.getRefundStatus().getCode())
|
||||
.statusName(refundRecord.getRefundStatus().name())
|
||||
.build();
|
||||
tradeMsgList.add(tradeStatusMsg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
XxlJobHelper.log("查询退款单出错!shardIndex = {}, shardTotal = {}, refundRecord = {}", shardIndex, shardTotal, refundRecord, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollUtil.isEmpty(tradeMsgList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//发送消息通知其他系统
|
||||
String msg = JSONUtil.toJsonStr(tradeMsgList);
|
||||
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.REFUND_UPDATE_STATUS, msg);
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.sl.ms.trade.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 交易渠道表Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface PayChannelMapper extends BaseMapper<PayChannelEntity> {
|
||||
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.sl.ms.trade.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 退款记录表Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface RefundRecordMapper extends BaseMapper<RefundRecordEntity> {
|
||||
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.sl.ms.trade.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 交易订单表Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface TradingMapper extends BaseMapper<TradingEntity> {
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.sl.ms.trade.domain.RefundRecordDTO;
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 支付的基础功能
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface BasicPayService {
|
||||
|
||||
/***
|
||||
* 统一收单线下交易查询
|
||||
* 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 交易数据对象
|
||||
*/
|
||||
TradingDTO queryTrading(Long tradingOrderNo) throws SLException;
|
||||
|
||||
/***
|
||||
* 统一收单交易退款接口
|
||||
* 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
|
||||
* 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
|
||||
* 支持一个交易单分多次退款,退款金额总额不能大于总支付的总金额,并且最多20次退款
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @param refundAmount 退款金额,不能大于总支付的总金额
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws SLException;
|
||||
|
||||
/***
|
||||
* 统一收单交易退款查询接口
|
||||
* @param refundNo 退款单号
|
||||
* @return 退款记录数据
|
||||
*/
|
||||
RefundRecordDTO queryRefundTrading(Long refundNo) throws SLException;
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
|
||||
/**
|
||||
* jsapi支付、微信小程序支付
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface JsapiPayService {
|
||||
|
||||
/***
|
||||
* 统一jsapi交易预创建
|
||||
* 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、
|
||||
* JSAPI、APP等不同场景生成交易串调起支付。
|
||||
* @param tradingEntity 交易单
|
||||
*
|
||||
* @return 交易单,支付串码
|
||||
*/
|
||||
TradingEntity createJsapiTrading(TradingEntity tradingEntity);
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
|
||||
/**
|
||||
* 二维码支付
|
||||
*/
|
||||
public interface NativePayService {
|
||||
|
||||
|
||||
/***
|
||||
* 查看二维码信息
|
||||
* 收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,商户可以多次展示二维码
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 交易单
|
||||
*/
|
||||
String queryQrCodeUrl(Long tradingOrderNo);
|
||||
|
||||
/***
|
||||
* 扫码支付,收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。
|
||||
*
|
||||
* @param tradingEntity 扫码支付提交参数
|
||||
* @return 交易数据
|
||||
*/
|
||||
TradingEntity createDownLineTrading(TradingEntity tradingEntity);
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 支付通知
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface NotifyService {
|
||||
|
||||
|
||||
/**
|
||||
* 微信支付通知,官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
|
||||
*
|
||||
* @param request 微信请求对象
|
||||
* @param enterpriseId 商户id
|
||||
* @throws SLException 抛出SL异常,通过异常决定是否响应200
|
||||
*/
|
||||
void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException;
|
||||
|
||||
/**
|
||||
* 支付宝支付通知,官方文档:https://opendocs.alipay.com/open/194/103296?ref=api
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @param enterpriseId 商户id
|
||||
* @throws SLException 抛出SL异常,通过异常决定是否响应200
|
||||
*/
|
||||
void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException;
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.sl.ms.trade.domain.PayChannelDTO;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 支付通道服务类
|
||||
*/
|
||||
public interface PayChannelService extends IService<PayChannelEntity> {
|
||||
|
||||
/**
|
||||
* @param payChannelDTO 查询条件
|
||||
* @param pageNum 当前页
|
||||
* @param pageSize 当前页
|
||||
* @return Page<PayChannel> 分页对象
|
||||
* @Description 支付通道列表
|
||||
*/
|
||||
Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize);
|
||||
|
||||
/**
|
||||
* 根据商户id查询渠道配置,该配置会被缓存10分钟
|
||||
*
|
||||
* @param enterpriseId 商户id
|
||||
* @param channelLabel 通道唯一标记
|
||||
* @return PayChannelEntity 交易渠道对象
|
||||
*/
|
||||
PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel);
|
||||
|
||||
/**
|
||||
* @param payChannelDTO 对象信息
|
||||
* @return PayChannelEntity 交易渠道对象
|
||||
* @Description 创建支付通道
|
||||
*/
|
||||
PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO);
|
||||
|
||||
/**
|
||||
* @param payChannelDTO 对象信息
|
||||
* @return Boolean 是否成功
|
||||
* @Description 修改支付通道
|
||||
*/
|
||||
Boolean updatePayChannel(PayChannelDTO payChannelDTO);
|
||||
|
||||
/**
|
||||
* @param checkedIds 选择的支付通道ID
|
||||
* @return Boolean 是否成功
|
||||
* @Description 删除支付通道
|
||||
*/
|
||||
Boolean deletePayChannel(String[] checkedIds);
|
||||
|
||||
/**
|
||||
* @param channelLabel 支付通道标识
|
||||
* @return 支付通道列表
|
||||
* @Description 查找渠道标识
|
||||
*/
|
||||
List<PayChannelEntity> findPayChannelList(String channelLabel);
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
|
||||
/**
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
public interface QRCodeService {
|
||||
|
||||
/**
|
||||
* 生成二维码
|
||||
*
|
||||
* @param content 二维码中的内容
|
||||
* @return 图片base64数据
|
||||
*/
|
||||
String generate(String content);
|
||||
|
||||
/**
|
||||
* 生成二维码,带logo
|
||||
*
|
||||
* @param content 二维码中的内容
|
||||
* @param payChannel 付款渠道
|
||||
* @return 图片base64数据
|
||||
*/
|
||||
String generate(String content, PayChannelEnum payChannel);
|
||||
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 退款记录表服务类
|
||||
*/
|
||||
public interface RefundRecordService extends IService<RefundRecordEntity> {
|
||||
|
||||
/**
|
||||
* 根据退款单号查询退款记录
|
||||
*
|
||||
* @param refundNo 退款单号
|
||||
* @return 退款记录数据
|
||||
*/
|
||||
RefundRecordEntity findByRefundNo(Long refundNo);
|
||||
|
||||
/**
|
||||
* 根据交易单号查询退款列表
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 退款列表
|
||||
*/
|
||||
List<RefundRecordEntity> findListByTradingOrderNo(Long tradingOrderNo);
|
||||
|
||||
/**
|
||||
* 根据订单号查询退款列表
|
||||
*
|
||||
* @param productOrderNo 订单号
|
||||
* @return 退款列表
|
||||
*/
|
||||
List<RefundRecordEntity> findListByProductOrderNo(Long productOrderNo);
|
||||
|
||||
/***
|
||||
* 按状态查询退款单,按照时间正序排序
|
||||
*
|
||||
* @param refundStatus 状态
|
||||
* @param count 查询数量,默认查询10条
|
||||
* @return 退款单数据列表
|
||||
*/
|
||||
List<RefundRecordEntity> findListByRefundStatus(RefundStatusEnum refundStatus, Integer count);
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description:交易订单表 服务类
|
||||
*/
|
||||
public interface TradingService extends IService<TradingEntity> {
|
||||
|
||||
/***
|
||||
* 按交易单号查询交易单
|
||||
*
|
||||
* @param tradingOrderNo 交易单号
|
||||
* @return 交易单数据
|
||||
*/
|
||||
TradingEntity findTradByTradingOrderNo(Long tradingOrderNo);
|
||||
|
||||
/***
|
||||
* 按订单单号查询交易单
|
||||
* @param productOrderNo 交易单号
|
||||
* @return 交易单数据
|
||||
*/
|
||||
TradingEntity findTradByProductOrderNo(Long productOrderNo);
|
||||
|
||||
/***
|
||||
* 按交易状态查询交易单,按照时间正序排序
|
||||
* @param tradingState 状态
|
||||
* @param count 查询数量,默认查询10条
|
||||
* @return 交易单数据列表
|
||||
*/
|
||||
List<TradingEntity> findListByTradingState(TradingStateEnum tradingState, Integer count);
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.sl.ms.trade.constant.Constants;
|
||||
import com.sl.ms.trade.constant.TradingCacheConstant;
|
||||
import com.sl.ms.trade.domain.RefundRecordDTO;
|
||||
import com.sl.ms.trade.domain.TradingDTO;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.BasicPayHandler;
|
||||
import com.sl.ms.trade.handler.BeforePayHandler;
|
||||
import com.sl.ms.trade.handler.HandlerFactory;
|
||||
import com.sl.ms.trade.service.BasicPayService;
|
||||
import com.sl.ms.trade.service.RefundRecordService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.sl.transport.common.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 支付的基础功能
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BasicPayServiceImpl implements BasicPayService {
|
||||
|
||||
@Resource
|
||||
private BeforePayHandler beforePayHandler;
|
||||
@Resource
|
||||
private RedissonClient redissonClient;
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private RefundRecordService refundRecordService;
|
||||
|
||||
@Override
|
||||
public TradingDTO queryTrading(Long tradingOrderNo) throws SLException {
|
||||
//通过单号查询交易单数据
|
||||
TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
|
||||
//查询前置处理:检测交易单参数
|
||||
this.beforePayHandler.checkQueryTrading(trading);
|
||||
|
||||
String key = TradingCacheConstant.QUERY_PAY + tradingOrderNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
//选取不同的支付渠道实现
|
||||
BasicPayHandler handler = HandlerFactory.get(trading.getTradingChannel(), BasicPayHandler.class);
|
||||
Boolean result = handler.queryTrading(trading);
|
||||
if (result) {
|
||||
//如果交易单已经完成,需要将二维码数据删除,节省数据库空间,如果有需要可以再次生成
|
||||
if (ObjectUtil.equalsAny(trading.getTradingState(), TradingStateEnum.YJS, TradingStateEnum.QXDD)) {
|
||||
trading.setQrCode("");
|
||||
}
|
||||
//更新数据
|
||||
this.tradingService.saveOrUpdate(trading);
|
||||
}
|
||||
return BeanUtil.toBean(trading, TradingDTO.class);
|
||||
}
|
||||
throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
|
||||
} catch (SLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("查询交易单数据异常: trading = {}", trading, e);
|
||||
throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Boolean refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws SLException {
|
||||
//通过单号查询交易单数据
|
||||
TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
|
||||
//设置退款金额
|
||||
trading.setRefund(NumberUtil.add(refundAmount, trading.getRefund()));
|
||||
|
||||
//入库前置检查
|
||||
this.beforePayHandler.checkRefundTrading(trading);
|
||||
|
||||
String key = TradingCacheConstant.REFUND_PAY + tradingOrderNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
//幂等性的检查
|
||||
RefundRecordEntity refundRecord = this.beforePayHandler.idempotentRefundTrading(trading, refundAmount);
|
||||
if (null == refundRecord) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//选取不同的支付渠道实现
|
||||
BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
|
||||
Boolean result = handler.refundTrading(refundRecord);
|
||||
if (result) {
|
||||
//更新退款记录数据
|
||||
this.refundRecordService.saveOrUpdate(refundRecord);
|
||||
|
||||
//设置交易单是退款订单
|
||||
trading.setIsRefund(Constants.YES);
|
||||
this.tradingService.saveOrUpdate(trading);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
|
||||
} catch (SLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("查询交易单数据异常:{}", ExceptionUtil.stacktraceToString(e));
|
||||
throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundRecordDTO queryRefundTrading(Long refundNo) throws SLException {
|
||||
//通过单号查询交易单数据
|
||||
RefundRecordEntity refundRecord = this.refundRecordService.findByRefundNo(refundNo);
|
||||
//查询前置处理
|
||||
this.beforePayHandler.checkQueryRefundTrading(refundRecord);
|
||||
|
||||
String key = TradingCacheConstant.REFUND_QUERY_PAY + refundNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
|
||||
//选取不同的支付渠道实现
|
||||
BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
|
||||
Boolean result = handler.queryRefundTrading(refundRecord);
|
||||
if (result) {
|
||||
//更新数据
|
||||
this.refundRecordService.saveOrUpdate(refundRecord);
|
||||
}
|
||||
return BeanUtil.toBean(refundRecord, RefundRecordDTO.class);
|
||||
}
|
||||
throw new SLException(TradingEnum.REFUND_FAIL);
|
||||
} catch (SLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("查询退款交易单数据异常: refundRecord = {}", refundRecord, e);
|
||||
throw new SLException(TradingEnum.REFUND_FAIL);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import com.sl.ms.trade.constant.Constants;
|
||||
import com.sl.ms.trade.constant.TradingCacheConstant;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.handler.BeforePayHandler;
|
||||
import com.sl.ms.trade.handler.HandlerFactory;
|
||||
import com.sl.ms.trade.handler.JsapiPayHandler;
|
||||
import com.sl.ms.trade.service.JsapiPayService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JsapiPayServiceImpl implements JsapiPayService {
|
||||
|
||||
@Resource
|
||||
private RedissonClient redissonClient;
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private BeforePayHandler beforePayHandler;
|
||||
|
||||
@Override
|
||||
public TradingEntity createJsapiTrading(TradingEntity tradingEntity) {
|
||||
//交易前置处理:检测交易单参数
|
||||
Boolean flag = beforePayHandler.checkCreateTrading(tradingEntity);
|
||||
if (!flag) {
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
}
|
||||
tradingEntity.setEnableFlag(Constants.YES);
|
||||
tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK);
|
||||
|
||||
//对交易订单加锁
|
||||
Long productOrderNo = tradingEntity.getProductOrderNo();
|
||||
String key = TradingCacheConstant.CREATE_PAY + productOrderNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
|
||||
//交易前置处理:幂等性处理
|
||||
this.beforePayHandler.idempotentCreateTrading(tradingEntity);
|
||||
|
||||
//调用不同的支付渠道进行处理
|
||||
JsapiPayHandler jsapiPayHandler = HandlerFactory.get(tradingEntity.getTradingChannel(), JsapiPayHandler.class);
|
||||
jsapiPayHandler.createJsapiTrading(tradingEntity);
|
||||
|
||||
//新增或更新交易数据
|
||||
flag = this.tradingService.saveOrUpdate(tradingEntity);
|
||||
if (!flag) {
|
||||
throw new SLException(TradingEnum.SAVE_OR_UPDATE_FAIL);
|
||||
}
|
||||
|
||||
return tradingEntity;
|
||||
}
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
} catch (SLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Jsapi预创建异常: tradingDTO = {}", tradingEntity, e);
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.sl.ms.trade.constant.Constants;
|
||||
import com.sl.ms.trade.constant.TradingCacheConstant;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.enums.TradingEnum;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.BeforePayHandler;
|
||||
import com.sl.ms.trade.handler.HandlerFactory;
|
||||
import com.sl.ms.trade.handler.NativePayHandler;
|
||||
import com.sl.ms.trade.service.NativePayService;
|
||||
import com.sl.ms.trade.service.QRCodeService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.sl.transport.common.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Native支付方式Face接口:商户生成二维码,用户扫描支付
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class NativePayServiceImpl implements NativePayService {
|
||||
|
||||
@Resource
|
||||
private RedissonClient redissonClient;
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private BeforePayHandler beforePayHandler;
|
||||
@Resource
|
||||
private QRCodeService qrCodeService;
|
||||
|
||||
@Override
|
||||
public String queryQrCodeUrl(Long tradingOrderNo) {
|
||||
TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
|
||||
if (ObjectUtil.equals(trading.getTradingState(), TradingStateEnum.YJS)) {
|
||||
//订单已完成,不返回二维码
|
||||
throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
|
||||
}
|
||||
return trading.getQrCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradingEntity createDownLineTrading(TradingEntity tradingEntity) {
|
||||
//交易前置处理:检测交易单参数
|
||||
Boolean flag = beforePayHandler.checkCreateTrading(tradingEntity);
|
||||
if (!flag) {
|
||||
throw new SLException(TradingEnum.CONFIG_ERROR);
|
||||
}
|
||||
|
||||
tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK);
|
||||
tradingEntity.setEnableFlag(Constants.YES);
|
||||
|
||||
//对交易订单加锁
|
||||
Long productOrderNo = tradingEntity.getProductOrderNo();
|
||||
String key = TradingCacheConstant.CREATE_PAY + productOrderNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
//交易前置处理:幂等性处理
|
||||
this.beforePayHandler.idempotentCreateTrading(tradingEntity);
|
||||
|
||||
//调用不同的支付渠道进行处理
|
||||
PayChannelEnum payChannel = PayChannelEnum.valueOf(tradingEntity.getTradingChannel());
|
||||
NativePayHandler nativePayHandler = HandlerFactory.get(payChannel, NativePayHandler.class);
|
||||
nativePayHandler.createDownLineTrading(tradingEntity);
|
||||
|
||||
//生成统一收款二维码
|
||||
String placeOrderMsg = tradingEntity.getPlaceOrderMsg();
|
||||
String qrCode = this.qrCodeService.generate(placeOrderMsg, payChannel);
|
||||
tradingEntity.setQrCode(qrCode);
|
||||
|
||||
//新增或更新交易数据
|
||||
flag = this.tradingService.saveOrUpdate(tradingEntity);
|
||||
if (!flag) {
|
||||
throw new SLException(TradingEnum.SAVE_OR_UPDATE_FAIL);
|
||||
}
|
||||
|
||||
return tradingEntity;
|
||||
}
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
} catch (SLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("统一收单线下交易预创建异常:{}", ExceptionUtil.stacktraceToString(e));
|
||||
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,171 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.easysdk.factory.Factory;
|
||||
import com.alipay.easysdk.kernel.Config;
|
||||
import com.sl.ms.base.api.common.MQFeign;
|
||||
import com.sl.ms.trade.constant.TradingCacheConstant;
|
||||
import com.sl.ms.trade.constant.TradingConstant;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.handler.alipay.AlipayConfig;
|
||||
import com.sl.ms.trade.handler.wechat.WechatPayHttpClient;
|
||||
import com.sl.ms.trade.service.NotifyService;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import com.sl.transport.common.constant.Constants;
|
||||
import com.sl.transport.common.exception.SLException;
|
||||
import com.sl.transport.common.vo.TradeStatusMsg;
|
||||
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
|
||||
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
|
||||
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
|
||||
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
|
||||
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 支付成功的通知处理
|
||||
*
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class NotifyServiceImpl implements NotifyService {
|
||||
|
||||
@Resource
|
||||
private TradingService tradingService;
|
||||
@Resource
|
||||
private RedissonClient redissonClient;
|
||||
@Resource
|
||||
private MQFeign mqFeign;
|
||||
|
||||
@Override
|
||||
public void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException {
|
||||
// 查询配置
|
||||
WechatPayHttpClient client = WechatPayHttpClient.get(enterpriseId);
|
||||
|
||||
JSONObject jsonData;
|
||||
|
||||
//验证签名,确保请求来自微信
|
||||
try {
|
||||
//确保在管理器中存在自动更新的商户证书
|
||||
client.createHttpClient();
|
||||
|
||||
CertificatesManager certificatesManager = CertificatesManager.getInstance();
|
||||
Verifier verifier = certificatesManager.getVerifier(client.getMchId());
|
||||
|
||||
//验签和解析请求数据
|
||||
NotificationHandler notificationHandler = new NotificationHandler(verifier, client.getApiV3Key().getBytes(StandardCharsets.UTF_8));
|
||||
Notification notification = notificationHandler.parse(request);
|
||||
|
||||
if (!StrUtil.equals("TRANSACTION.SUCCESS", notification.getEventType())) {
|
||||
//非成功请求直接返回,理论上都是成功的请求
|
||||
return;
|
||||
}
|
||||
|
||||
//获取解密后的数据
|
||||
jsonData = JSONUtil.parseObj(notification.getDecryptData());
|
||||
} catch (Exception e) {
|
||||
throw new SLException("验签失败");
|
||||
}
|
||||
|
||||
if (!StrUtil.equals(jsonData.getStr("trade_state"), TradingConstant.WECHAT_TRADE_SUCCESS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//交易单号
|
||||
Long tradingOrderNo = jsonData.getLong("out_trade_no");
|
||||
log.info("微信支付通知:tradingOrderNo = {}, data = {}", tradingOrderNo, jsonData);
|
||||
|
||||
//更新交易单
|
||||
this.updateTrading(tradingOrderNo, jsonData.getStr("trade_state_desc"), jsonData.toString());
|
||||
}
|
||||
|
||||
private void updateTrading(Long tradingOrderNo, String resultMsg, String resultJson) {
|
||||
String key = TradingCacheConstant.CREATE_PAY + tradingOrderNo;
|
||||
RLock lock = redissonClient.getFairLock(key);
|
||||
try {
|
||||
//获取锁
|
||||
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
|
||||
TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
|
||||
if (trading.getTradingState() == TradingStateEnum.YJS) {
|
||||
// 已付款
|
||||
return;
|
||||
}
|
||||
|
||||
//设置成付款成功
|
||||
trading.setTradingState(TradingStateEnum.YJS);
|
||||
//清空二维码数据
|
||||
trading.setQrCode("");
|
||||
trading.setResultMsg(resultMsg);
|
||||
trading.setResultJson(resultJson);
|
||||
this.tradingService.saveOrUpdate(trading);
|
||||
|
||||
// 发消息通知其他系统支付成功
|
||||
TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
|
||||
.tradingOrderNo(trading.getTradingOrderNo())
|
||||
.productOrderNo(trading.getProductOrderNo())
|
||||
.statusCode(TradingStateEnum.YJS.getCode())
|
||||
.statusName(TradingStateEnum.YJS.name())
|
||||
.build();
|
||||
|
||||
String msg = JSONUtil.toJsonStr(Collections.singletonList(tradeStatusMsg));
|
||||
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SLException("处理业务失败");
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
throw new SLException("处理业务失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException {
|
||||
//获取参数
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
Map<String, String> param = new HashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
|
||||
param.put(entry.getKey(), StrUtil.join(",", entry.getValue()));
|
||||
}
|
||||
|
||||
String tradeStatus = param.get("trade_status");
|
||||
if (!StrUtil.equals(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//查询配置
|
||||
Config config = AlipayConfig.getConfig(enterpriseId);
|
||||
Factory.setOptions(config);
|
||||
try {
|
||||
Boolean result = Factory
|
||||
.Payment
|
||||
.Common().verifyNotify(param);
|
||||
if (!result) {
|
||||
throw new SLException("验签失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SLException("验签失败");
|
||||
}
|
||||
|
||||
//获取交易单号
|
||||
Long tradingOrderNo = Convert.toLong(param.get("out_trade_no"));
|
||||
//更新交易单
|
||||
this.updateTrading(tradingOrderNo, "支付成功", JSONUtil.toJsonStr(param));
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.sl.ms.trade.constant.Constants;
|
||||
import com.sl.ms.trade.domain.PayChannelDTO;
|
||||
import com.sl.ms.trade.entity.PayChannelEntity;
|
||||
import com.sl.ms.trade.mapper.PayChannelMapper;
|
||||
import com.sl.ms.trade.service.PayChannelService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 服务实现类
|
||||
*/
|
||||
@Service
|
||||
public class PayChannelServiceImpl extends ServiceImpl<PayChannelMapper, PayChannelEntity> implements PayChannelService {
|
||||
|
||||
@Override
|
||||
public Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize) {
|
||||
Page<PayChannelEntity> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
//设置条件
|
||||
queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getChannelLabel()), PayChannelEntity::getChannelLabel, payChannelDTO.getChannelLabel());
|
||||
queryWrapper.likeRight(StrUtil.isNotEmpty(payChannelDTO.getChannelName()), PayChannelEntity::getChannelName, payChannelDTO.getChannelName());
|
||||
queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getEnableFlag()), PayChannelEntity::getEnableFlag, payChannelDTO.getEnableFlag());
|
||||
//设置排序
|
||||
queryWrapper.orderByAsc(PayChannelEntity::getCreated);
|
||||
|
||||
return super.page(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel) {
|
||||
LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(PayChannelEntity::getEnterpriseId, enterpriseId)
|
||||
.eq(PayChannelEntity::getChannelLabel, channelLabel)
|
||||
.eq(PayChannelEntity::getEnableFlag, Constants.YES);
|
||||
//TODO 缓存
|
||||
return super.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO) {
|
||||
PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
|
||||
boolean flag = super.save(payChannel);
|
||||
if (flag) {
|
||||
return payChannel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean updatePayChannel(PayChannelDTO payChannelDTO) {
|
||||
PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
|
||||
return super.updateById(payChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deletePayChannel(String[] checkedIds) {
|
||||
List<String> ids = Arrays.asList(checkedIds);
|
||||
return super.removeByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayChannelEntity> findPayChannelList(String channelLabel) {
|
||||
LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(PayChannelEntity::getChannelLabel, channelLabel)
|
||||
.eq(PayChannelEntity::getEnableFlag, Constants.YES);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||
import cn.hutool.extra.qrcode.QrConfig;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import com.sl.ms.trade.config.QRCodeConfig;
|
||||
import com.sl.ms.trade.enums.PayChannelEnum;
|
||||
import com.sl.ms.trade.service.QRCodeService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Service
|
||||
public class QRCodeServiceImpl implements QRCodeService {
|
||||
|
||||
@Resource
|
||||
private QRCodeConfig qrCodeConfig;
|
||||
|
||||
@Override
|
||||
public String generate(String content, PayChannelEnum payChannel) {
|
||||
QrConfig qrConfig = new QrConfig();
|
||||
//设置边距
|
||||
qrConfig.setMargin(this.qrCodeConfig.getMargin());
|
||||
//二维码颜色
|
||||
qrConfig.setForeColor(HexUtil.decodeColor(this.qrCodeConfig.getForeColor()));
|
||||
//设置背景色
|
||||
qrConfig.setBackColor(HexUtil.decodeColor(this.qrCodeConfig.getBackColor()));
|
||||
//纠错级别
|
||||
qrConfig.setErrorCorrection(ErrorCorrectionLevel.valueOf(this.qrCodeConfig.getErrorCorrectionLevel()));
|
||||
//设置宽
|
||||
qrConfig.setWidth(this.qrCodeConfig.getWidth());
|
||||
//设置高
|
||||
qrConfig.setHeight(this.qrCodeConfig.getHeight());
|
||||
if (ObjectUtil.isNotEmpty(payChannel)) {
|
||||
//设置logo
|
||||
qrConfig.setImg(this.qrCodeConfig.getLogo(payChannel));
|
||||
}
|
||||
return QrCodeUtil.generateAsBase64(content, qrConfig, ImgUtil.IMAGE_TYPE_PNG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String content) {
|
||||
return generate(content, null);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
QRCodeServiceImpl qrCodeService = new QRCodeServiceImpl();
|
||||
qrCodeService.qrCodeConfig = new QRCodeConfig();
|
||||
System.out.println(qrCodeService.generate("http://192.168.33.38:18096/qr", PayChannelEnum.ALI_PAY));
|
||||
System.out.println(qrCodeService.generate("http://192.168.33.38:18096/qr", PayChannelEnum.WECHAT_PAY));
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.sl.ms.trade.entity.RefundRecordEntity;
|
||||
import com.sl.ms.trade.enums.RefundStatusEnum;
|
||||
import com.sl.ms.trade.mapper.RefundRecordMapper;
|
||||
import com.sl.ms.trade.service.RefundRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 退款记录服务实现类
|
||||
*/
|
||||
@Service
|
||||
public class RefundRecordServiceImpl extends ServiceImpl<RefundRecordMapper, RefundRecordEntity> implements RefundRecordService {
|
||||
|
||||
@Override
|
||||
public RefundRecordEntity findByRefundNo(Long refundNo) {
|
||||
LambdaQueryWrapper<RefundRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefundRecordEntity::getRefundNo, refundNo);
|
||||
return super.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RefundRecordEntity> findListByTradingOrderNo(Long tradingOrderNo) {
|
||||
LambdaQueryWrapper<RefundRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefundRecordEntity::getTradingOrderNo, tradingOrderNo);
|
||||
queryWrapper.orderByDesc(RefundRecordEntity::getCreated);
|
||||
return super.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RefundRecordEntity> findListByProductOrderNo(Long productOrderNo) {
|
||||
LambdaQueryWrapper<RefundRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefundRecordEntity::getProductOrderNo, productOrderNo);
|
||||
queryWrapper.orderByDesc(RefundRecordEntity::getCreated);
|
||||
return super.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RefundRecordEntity> findListByRefundStatus(RefundStatusEnum refundStatus, Integer count) {
|
||||
count = NumberUtil.max(count, 10);
|
||||
LambdaQueryWrapper<RefundRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefundRecordEntity::getRefundStatus, refundStatus)
|
||||
.orderByAsc(RefundRecordEntity::getCreated)
|
||||
.last("LIMIT " + count);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package com.sl.ms.trade.service.impl;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.sl.ms.trade.constant.Constants;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import com.sl.ms.trade.enums.TradingStateEnum;
|
||||
import com.sl.ms.trade.mapper.TradingMapper;
|
||||
import com.sl.ms.trade.service.TradingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description:交易订单表 服务实现类
|
||||
*/
|
||||
@Service
|
||||
public class TradingServiceImpl extends ServiceImpl<TradingMapper, TradingEntity> implements TradingService {
|
||||
|
||||
@Override
|
||||
public TradingEntity findTradByTradingOrderNo(Long tradingOrderNo) {
|
||||
LambdaQueryWrapper<TradingEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(TradingEntity::getTradingOrderNo, tradingOrderNo);
|
||||
return super.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradingEntity findTradByProductOrderNo(Long productOrderNo) {
|
||||
LambdaQueryWrapper<TradingEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(TradingEntity::getProductOrderNo, productOrderNo);
|
||||
return super.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TradingEntity> findListByTradingState(TradingStateEnum tradingState, Integer count) {
|
||||
count = NumberUtil.max(count, 10);
|
||||
LambdaQueryWrapper<TradingEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(TradingEntity::getTradingState, tradingState)
|
||||
.eq(TradingEntity::getEnableFlag, Constants.YES)
|
||||
.orderByAsc(TradingEntity::getCreated)
|
||||
.last("LIMIT " + count);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
|
||||
_ ${spring.application.name} ${application.version}
|
||||
___ | | ___ __ __ _ __ _ __ ___ ___ ___ Port: ${server.port}
|
||||
/ __|| | _____ / _ \\ \/ /| '_ \ | '__|/ _ \/ __|/ __| Pid: ${pid} Profile(s): ${AnsiColor.GREEN}${spring.profiles.active}${AnsiColor.DEFAULT}
|
||||
\__ \| ||_____|| __/ > < | |_) || | | __/\__ \\__ \
|
||||
|___/|_| \___|/_/\_\| .__/ |_| \___||___/|___/ https://sl-express.itheima.net/
|
||||
|_|
|
@@ -0,0 +1,34 @@
|
||||
server:
|
||||
port: 18096
|
||||
tomcat:
|
||||
uri-encoding: UTF-8
|
||||
threads:
|
||||
max: 1000
|
||||
min-spare: 30
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
username: nacos
|
||||
password: nacos
|
||||
server-addr: 192.168.150.101:8848
|
||||
discovery:
|
||||
namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
|
||||
ip: 192.168.150.1
|
||||
config:
|
||||
namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
|
||||
shared-configs: #共享配置
|
||||
- data-id: shared-spring-seata.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mysql.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mybatis-plus.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-redis.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-xxl-job.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
@@ -0,0 +1,33 @@
|
||||
server:
|
||||
port: 18096
|
||||
tomcat:
|
||||
uri-encoding: UTF-8
|
||||
threads:
|
||||
max: 1000
|
||||
min-spare: 30
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
username: nacos
|
||||
password: vO5/dZ9,iL
|
||||
server-addr: nacos-service.yjy-public-slwl-java-prod.svc.cluster.local:8848
|
||||
discovery:
|
||||
namespace: 92312ba8-1119-440f-81af-c29618df303b
|
||||
config:
|
||||
namespace: 92312ba8-1119-440f-81af-c29618df303b
|
||||
shared-configs: #共享配置
|
||||
- data-id: shared-spring-seata.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mysql.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mybatis-plus.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-redis.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-xxl-job.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
@@ -0,0 +1,33 @@
|
||||
server:
|
||||
port: 18096
|
||||
tomcat:
|
||||
uri-encoding: UTF-8
|
||||
threads:
|
||||
max: 1000
|
||||
min-spare: 30
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
username: nacos
|
||||
password: nacos
|
||||
server-addr: 192.168.150.101:8848
|
||||
discovery:
|
||||
namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
|
||||
config:
|
||||
namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
|
||||
shared-configs: #共享配置
|
||||
- data-id: shared-spring-seata.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mysql.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mybatis-plus.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-redis.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-xxl-job.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
@@ -0,0 +1,33 @@
|
||||
server:
|
||||
port: 18096
|
||||
tomcat:
|
||||
uri-encoding: UTF-8
|
||||
threads:
|
||||
max: 1000
|
||||
min-spare: 30
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
username: nacos
|
||||
password: nacos
|
||||
server-addr: nacos-service.yjy-public-slwl-java.svc.cluster.local:8848
|
||||
discovery:
|
||||
namespace: 92312ba8-1119-440f-81af-c29618df303b
|
||||
config:
|
||||
namespace: 92312ba8-1119-440f-81af-c29618df303b
|
||||
shared-configs: #共享配置
|
||||
- data-id: shared-spring-seata.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mysql.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-mybatis-plus.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-redis.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
||||
- data-id: shared-spring-xxl-job.yml
|
||||
group: SHARED_GROUP
|
||||
refresh: false
|
36
sl-express-ms-trade-service/src/main/resources/bootstrap.yml
Normal file
36
sl-express-ms-trade-service/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
application:
|
||||
version: v1.0
|
||||
logging:
|
||||
config: classpath:logback-spring.xml
|
||||
spring:
|
||||
application:
|
||||
name: sl-express-ms-trade
|
||||
profiles:
|
||||
active: local
|
||||
mvc:
|
||||
pathmatch:
|
||||
#解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
|
||||
#因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher
|
||||
matching-strategy: ant_path_matcher
|
||||
sl:
|
||||
job:
|
||||
trading:
|
||||
count: ${job.trading.count} #每次查询交易单的数量
|
||||
refund:
|
||||
count: ${job.refund.count} #每次查询退款单的数量
|
||||
qrcode: #二维码生成参数
|
||||
margin: ${qrcode.margin} #边距,二维码和背景之间的边距
|
||||
fore-color: ${qrcode.fore-color} #二维码颜色,默认黑色
|
||||
back-color: ${qrcode.back-color} #背景色,默认白色
|
||||
#低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
|
||||
error-correction-level: ${qrcode.error-correction-level} #纠错级别,可选参数:L、M、Q、H,默认:M
|
||||
width: ${qrcode.width} #宽
|
||||
height: ${qrcode.height} #高
|
||||
swagger:
|
||||
package-path: com.sl.ms.trade.controller
|
||||
title: 神领物流 - 交易(支付)微服务接口文档
|
||||
description: 该微服务完记录交易、对接支付宝微信支付平台。
|
||||
contact-name: 传智教育·研究院
|
||||
contact-url: http://www.itcast.cn/
|
||||
contact-email: yjy@itcast.cn
|
||||
version: ${application.version}
|
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。-->
|
||||
<!--scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。-->
|
||||
<!--debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。-->
|
||||
<configuration debug="false" scan="false" scanPeriod="60 seconds">
|
||||
<springProperty scope="context" name="appName" source="spring.application.name"/>
|
||||
<!--文件名-->
|
||||
<property name="logback.appname" value="${appName}"/>
|
||||
<!--文件位置-->
|
||||
<property name="logback.logdir" value="/data/logs"/>
|
||||
|
||||
<!-- 定义控制台输出 -->
|
||||
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} - [%thread] - %-5level - %logger{50} - %msg%n</pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>DEBUG</level>
|
||||
</filter>
|
||||
<File>${logback.logdir}/${logback.appname}/${logback.appname}.log</File>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>${logback.logdir}/${logback.appname}/${logback.appname}.%d{yyyy-MM-dd}.log.zip</FileNamePattern>
|
||||
<maxHistory>90</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<!--evel:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,-->
|
||||
<!--不能设置为INHERITED或者同义词NULL。默认是DEBUG。-->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="stdout"/>
|
||||
</root>
|
||||
</configuration>
|
BIN
sl-express-ms-trade-service/src/main/resources/logos/alipay.png
Normal file
BIN
sl-express-ms-trade-service/src/main/resources/logos/alipay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
sl-express-ms-trade-service/src/main/resources/logos/wechat.png
Normal file
BIN
sl-express-ms-trade-service/src/main/resources/logos/wechat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,29 @@
|
||||
package com.sl.ms.trade.job;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* @author zzj
|
||||
* @version 1.0
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class TradeJobTest {
|
||||
|
||||
@Resource
|
||||
private TradeJob tradeJob;
|
||||
|
||||
@Test
|
||||
public void tradingJob() {
|
||||
this.tradeJob.tradingJob();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refundJob() {
|
||||
this.tradeJob.refundJob();
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.sl.ms.trade.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.easysdk.kernel.util.JsonUtil;
|
||||
import com.sl.ms.trade.domain.request.JsapiPayDTO;
|
||||
import com.sl.ms.trade.entity.TradingEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
|
||||
@SpringBootTest
|
||||
class JsapiPayServiceTest {
|
||||
|
||||
@Resource
|
||||
private JsapiPayService jsapiPayService;
|
||||
|
||||
@Test
|
||||
public void TestCreateJsapiTrading() {
|
||||
// JsapiPayDTO(openId=otdlR4yqCSxTWQkKfUCa-9n5FCGg, enterpriseId=1561414331, productOrderNo=1551888004743520257, tradingChannel=WECHAT_PAY, tradingAmount=0.1, memo=null)
|
||||
String json = "{\"openId\":\"otdlR4yqCSxTWQkKfUCa-9n5FCGg\",\"enterpriseId\":1561414331,\"productOrderNo\":1551888004743520257,\"tradingChannel\":\"WECHAT_PAY\",\"tradingAmount\":0.1}";
|
||||
JsapiPayDTO jsapiPayDTO = JSONUtil.toBean(json, JsapiPayDTO.class);
|
||||
TradingEntity tradingEntity = BeanUtil.toBean(jsapiPayDTO, TradingEntity.class);
|
||||
tradingEntity.setMemo("xxxx");
|
||||
tradingEntity.setProductOrderNo(1551881034745602050L);
|
||||
jsapiPayService.createJsapiTrading(tradingEntity);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user