🎉 精简仅保留 system、infra 功能,适合小项目
This commit is contained in:
parent
57b8411f03
commit
fc9f46947e
yudao-dependencies
yudao-example
pom.xml
yudao-sso-demo-by-code
pom.xml
src/main
java/cn/iocoder/yudao/ssodemo
resources
yudao-sso-demo-by-password
pom.xml
src/main
java/cn/iocoder/yudao/ssodemo
resources
yudao-framework
pom.xml
yudao-spring-boot-starter-biz-data-permission
pom.xml
src/test/java/cn/iocoder/yudao/framework/datapermission/core
yudao-spring-boot-starter-biz-dict
yudao-spring-boot-starter-biz-ip
yudao-spring-boot-starter-biz-pay
pom.xml
src
main
java/cn/iocoder/yudao/framework/pay
config
core
client
PayClient.javaPayClientConfig.javaPayClientFactory.java
dto
order
refund
transfer
exception
impl
enums
resources/META-INF/spring
test/java/cn/iocoder/yudao/framework/pay/core/client/impl
@ -360,14 +360,6 @@
|
||||
<version>${spring-boot-admin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<version>${revision}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?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>
|
||||
|
||||
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-example</artifactId>
|
||||
<version>1.0.0-snapshot</version>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>yudao-sso-demo-by-code</module>
|
||||
<module>yudao-sso-demo-by-password</module>
|
||||
</modules>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>提供各种示例,例如说:SSO 单点登录</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
</project>
|
@ -1,65 +0,0 @@
|
||||
<?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>
|
||||
|
||||
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-sso-demo-by-code</artifactId>
|
||||
<version>1.0.0-snapshot</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>基于授权码模式,如何实现 SSO 单点登录?</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<!-- Maven 相关 -->
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 统一依赖管理 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.22</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -1,13 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SSODemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SSODemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* OAuth 2.0 客户端
|
||||
*
|
||||
* 对应调用 OAuth2OpenController 接口
|
||||
*/
|
||||
@Component
|
||||
public class OAuth2Client {
|
||||
|
||||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2";
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*
|
||||
* 默认使用 1;如果使用别的租户,可以调整
|
||||
*/
|
||||
public static final Long TENANT_ID = 1L;
|
||||
|
||||
private static final String CLIENT_ID = "yudao-sso-demo-by-code";
|
||||
private static final String CLIENT_SECRET = "test";
|
||||
|
||||
|
||||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 使用 code 授权码,获得访问令牌
|
||||
*
|
||||
* @param code 授权码
|
||||
* @param redirectUri 重定向 URI
|
||||
* @return 访问令牌
|
||||
*/
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("grant_type", "authorization_code");
|
||||
body.add("code", code);
|
||||
body.add("redirect_uri", redirectUri);
|
||||
// body.add("state", ""); // 选填;填了会校验
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/token",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验访问令牌,并返回它的基本信息
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 访问令牌的基本信息
|
||||
*/
|
||||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("token", token);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/check-token",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用刷新令牌,获得(刷新)访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 访问令牌
|
||||
*/
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("grant_type", "refresh_token");
|
||||
body.add("refresh_token", refreshToken);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/token",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除访问令牌
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 成功
|
||||
*/
|
||||
public CommonResult<Boolean> revokeToken(String token) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("token", token);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/token",
|
||||
HttpMethod.DELETE,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
private static void addClientHeader(HttpHeaders headers) {
|
||||
// client 拼接,需要 BASE64 编码
|
||||
String client = CLIENT_ID + ":" + CLIENT_SECRET;
|
||||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8));
|
||||
headers.add("Authorization", "Basic " + client);
|
||||
}
|
||||
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* 用户 User 信息的客户端
|
||||
*
|
||||
* 对应调用 OAuth2UserController 接口
|
||||
*/
|
||||
@Component
|
||||
public class UserClient {
|
||||
|
||||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user";
|
||||
|
||||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public CommonResult<UserInfoRespDTO> getUser() {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||
addTokenHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/get",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||
addTokenHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
// 使用 updateReqDTO 即可
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/update",
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(updateReqDTO, headers),
|
||||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
|
||||
private static void addTokenHeader(HttpHeaders headers) {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
Assert.notNull(loginUser, "登录用户不能为空");
|
||||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken());
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用返回
|
||||
*
|
||||
* @param <T> 数据泛型
|
||||
*/
|
||||
@Data
|
||||
public class CommonResult<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
/**
|
||||
* 错误提示,用户可阅读
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 访问令牌 Response DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OAuth2AccessTokenRespDTO {
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@JsonProperty("refresh_token")
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
@JsonProperty("token_type")
|
||||
private String tokenType;
|
||||
|
||||
/**
|
||||
* 过期时间;单位:秒
|
||||
*/
|
||||
@JsonProperty("expires_in")
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 授权范围;如果多个授权范围,使用空格分隔
|
||||
*/
|
||||
private String scope;
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 校验令牌 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OAuth2CheckTokenRespDTO {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
@JsonProperty("user_type")
|
||||
private Integer userType;
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
@JsonProperty("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 客户端编号
|
||||
*/
|
||||
@JsonProperty("client_id")
|
||||
private String clientId;
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private List<String> scopes;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*
|
||||
* 时间戳 / 1000,即单位:秒
|
||||
*/
|
||||
private Long exp;
|
||||
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 获得用户基本信息 Response dto
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserInfoRespDTO {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private Integer sex;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 所在部门
|
||||
*/
|
||||
private Dept dept;
|
||||
|
||||
/**
|
||||
* 所属岗位数组
|
||||
*/
|
||||
private List<Post> posts;
|
||||
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
@Data
|
||||
public static class Dept {
|
||||
|
||||
/**
|
||||
* 部门编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位
|
||||
*/
|
||||
@Data
|
||||
public static class Post {
|
||||
|
||||
/**
|
||||
* 岗位编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 岗位名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 更新用户基本信息 Request DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserUpdateReqDTO {
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private Integer sex;
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Resource
|
||||
private OAuth2Client oauth2Client;
|
||||
|
||||
/**
|
||||
* 使用 code 访问令牌,获得访问令牌
|
||||
*
|
||||
* @param code 授权码
|
||||
* @param redirectUri 重定向 URI
|
||||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||
*/
|
||||
@PostMapping("/login-by-code")
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code,
|
||||
@RequestParam("redirectUri") String redirectUri) {
|
||||
return oauth2Client.postAccessToken(code, redirectUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用刷新令牌,获得(刷新)访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||
*/
|
||||
@PostMapping("/refresh-token")
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||
return oauth2Client.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 成功
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return oauth2Client.revokeToken(token);
|
||||
}
|
||||
// 返回成功
|
||||
return new CommonResult<>();
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.controller;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.UserClient;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
private UserClient userClient;
|
||||
|
||||
/**
|
||||
* 获得当前登录用户的基本信息
|
||||
*
|
||||
* @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||
*/
|
||||
@GetMapping("/get")
|
||||
public CommonResult<UserInfoRespDTO> getUser() {
|
||||
return userClient.getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前登录用户的昵称
|
||||
*
|
||||
* @param nickname 昵称
|
||||
* @return 成功
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) {
|
||||
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null);
|
||||
return userClient.updateUser(updateReqDTO);
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.config;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfiguration{
|
||||
|
||||
@Resource
|
||||
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||
|
||||
@Resource
|
||||
private AccessDeniedHandlerImpl accessDeniedHandler;
|
||||
@Resource
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Bean
|
||||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
// 设置 URL 安全权限
|
||||
httpSecurity.csrf().disable() // 禁用 CSRF 保护
|
||||
.authorizeRequests()
|
||||
// 1. 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||
// 2. 登录相关的接口,可匿名访问
|
||||
.antMatchers("/auth/login-by-code").permitAll()
|
||||
.antMatchers("/auth/refresh-token").permitAll()
|
||||
.antMatchers("/auth/logout").permitAll()
|
||||
// last. 兜底规则,必须认证
|
||||
.and().authorizeRequests()
|
||||
.anyRequest().authenticated();
|
||||
|
||||
// 设置处理器
|
||||
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
|
||||
.authenticationEntryPoint(authenticationEntryPoint);
|
||||
|
||||
// 添加 Token Filter
|
||||
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class LoginUser {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private List<String> scopes;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Token 过滤器,验证 token 的有效性
|
||||
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
private OAuth2Client oauth2Client;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
// 1. 获得访问令牌
|
||||
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 2. 基于 token 构建登录用户
|
||||
LoginUser loginUser = buildLoginUserByToken(token);
|
||||
// 3. 设置当前用户
|
||||
if (loginUser != null) {
|
||||
SecurityUtils.setLoginUser(loginUser, request);
|
||||
}
|
||||
}
|
||||
|
||||
// 继续过滤链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private LoginUser buildLoginUserByToken(String token) {
|
||||
try {
|
||||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token);
|
||||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData();
|
||||
if (accessToken == null) {
|
||||
return null;
|
||||
}
|
||||
// 构建登录用户
|
||||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
|
||||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
|
||||
.setAccessToken(accessToken.getAccessToken());
|
||||
} catch (Exception exception) {
|
||||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
|
||||
*
|
||||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@SuppressWarnings("JavadocReference")
|
||||
@Slf4j
|
||||
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||
throws IOException, ServletException {
|
||||
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
||||
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
|
||||
SecurityUtils.getLoginUserId(), e);
|
||||
// 返回 403
|
||||
CommonResult<Object> result = new CommonResult<>();
|
||||
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||
result.setMsg("没有该操作权限");
|
||||
ServletUtils.writeJSON(response, result);
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
|
||||
*
|
||||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
||||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
|
||||
// 返回 401
|
||||
CommonResult<Object> result = new CommonResult<>();
|
||||
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||
result.setMsg("账号未登录");
|
||||
ServletUtils.writeJSON(response, result);
|
||||
}
|
||||
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 安全服务工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
public static final String AUTHORIZATION_BEARER = "Bearer";
|
||||
|
||||
private SecurityUtils() {}
|
||||
|
||||
/**
|
||||
* 从请求中,获得认证 Token
|
||||
*
|
||||
* @param request 请求
|
||||
* @param header 认证 Token 对应的 Header 名字
|
||||
* @return 认证 Token
|
||||
*/
|
||||
public static String obtainAuthorization(HttpServletRequest request, String header) {
|
||||
String authorization = request.getHeader(header);
|
||||
if (!StringUtils.hasText(authorization)) {
|
||||
return null;
|
||||
}
|
||||
int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
|
||||
if (index == -1) { // 未找到
|
||||
return null;
|
||||
}
|
||||
return authorization.substring(index + 7).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前认证信息
|
||||
*
|
||||
* @return 认证信息
|
||||
*/
|
||||
public static Authentication getAuthentication() {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
return context.getAuthentication();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户
|
||||
*
|
||||
* @return 当前用户
|
||||
*/
|
||||
@Nullable
|
||||
public static LoginUser getLoginUser() {
|
||||
Authentication authentication = getAuthentication();
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号,从上下文中
|
||||
*
|
||||
* @return 用户编号
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getLoginUserId() {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
return loginUser != null ? loginUser.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前用户
|
||||
*
|
||||
* @param loginUser 登录用户
|
||||
* @param request 请求
|
||||
*/
|
||||
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
|
||||
// 创建 Authentication,并设置到上下文
|
||||
Authentication authentication = buildAuthentication(loginUser, request);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
|
||||
// 创建 UsernamePasswordAuthenticationToken 对象
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
loginUser, null, Collections.emptyList());
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 客户端工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ServletUtils {
|
||||
|
||||
/**
|
||||
* 返回 JSON 字符串
|
||||
*
|
||||
* @param response 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||
String content = JSONUtil.toJsonStr(object);
|
||||
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
}
|
||||
|
||||
public static void write(HttpServletResponse response, String text, String contentType) {
|
||||
ServletUtil.write(response, text, contentType);
|
||||
}
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
server:
|
||||
port: 18080
|
@ -1,61 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SSO 授权后的回调页</title>
|
||||
<!-- jQuery:操作 dom、发起请求等 -->
|
||||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||
<!-- 工具类 -->
|
||||
<script type="application/javascript">
|
||||
(function ($) {
|
||||
/**
|
||||
* 获得 URL 的指定参数的值
|
||||
*
|
||||
* @param name 参数名
|
||||
* @returns 参数值
|
||||
*/
|
||||
$.getUrlParam = function (name) {
|
||||
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
const r = window.location.search.substr(1).match(reg);
|
||||
if (r != null) return unescape(r[2]); return null;
|
||||
}
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
$(function () {
|
||||
// 获得 code 授权码
|
||||
const code = $.getUrlParam('code');
|
||||
if (!code) {
|
||||
alert('获取不到 code 参数,请排查!')
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交
|
||||
const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code
|
||||
+ '&redirectUri=' + redirectUri,
|
||||
method: 'POST',
|
||||
success: function( result ) {
|
||||
if (result.code !== 0) {
|
||||
alert('获得访问令牌失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('获得访问令牌成功!点击确认,跳转回首页')
|
||||
|
||||
// 设置到 localStorage 中
|
||||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||
|
||||
// 跳转回首页
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
正在使用 code 授权码,进行 accessToken 访问令牌的获取
|
||||
</body>
|
||||
</html>
|
@ -1,159 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>首页</title>
|
||||
<!-- jQuery:操作 dom、发起请求等 -->
|
||||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
/**
|
||||
* 跳转单点登录
|
||||
*/
|
||||
function ssoLogin() {
|
||||
const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
|
||||
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
|
||||
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
|
||||
window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId
|
||||
+ '&redirect_uri=' + redirectUri
|
||||
+ '&response_type=' + responseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改昵称
|
||||
*/
|
||||
function updateNickname() {
|
||||
const nickname = prompt("请输入新的昵称", "");
|
||||
if (!nickname) {
|
||||
return;
|
||||
}
|
||||
// 更新用户的昵称
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('更新昵称失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('更新昵称成功!');
|
||||
$('#nicknameSpan').html(nickname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
function refreshToken() {
|
||||
const refreshToken = localStorage.getItem('REFRESH-TOKEN');
|
||||
if (!refreshToken) {
|
||||
alert("获取不到刷新令牌");
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
|
||||
method: 'POST',
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('刷新访问令牌失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('更新访问令牌成功!');
|
||||
$('#accessTokenSpan').html(result.data.access_token);
|
||||
|
||||
// 设置到 localStorage 中
|
||||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出,删除访问令牌
|
||||
*/
|
||||
function logout() {
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
if (!accessToken) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/auth/logout",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('退出登录失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('退出登录成功!');
|
||||
// 删除 localStorage 中
|
||||
localStorage.removeItem('ACCESS-TOKEN');
|
||||
localStorage.removeItem('REFRESH-TOKEN');
|
||||
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
// 情况一:未登录
|
||||
if (!accessToken) {
|
||||
$('#noLoginDiv').css("display", "block");
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:已登录
|
||||
$('#yesLoginDiv').css("display", "block");
|
||||
$('#accessTokenSpan').html(accessToken);
|
||||
// 获得登录用户的信息
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/user/get",
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('获得个人信息失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
$('#nicknameSpan').html(result.data.nickname);
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
|
||||
<div id="noLoginDiv" style="display: none">
|
||||
您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
|
||||
</div>
|
||||
|
||||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
|
||||
<div id="yesLoginDiv" style="display: none">
|
||||
您已登录!<button onclick="logout()">退出登录</button> <br />
|
||||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
|
||||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
body { /** 页面居中 */
|
||||
border-radius: 20px;
|
||||
height: 350px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
</style>
|
||||
</html>
|
@ -1,65 +0,0 @@
|
||||
<?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>
|
||||
|
||||
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-sso-demo-by-password</artifactId>
|
||||
<version>1.0.0-snapshot</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>基于密码模式,如何实现 SSO 单点登录?</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<!-- Maven 相关 -->
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 统一依赖管理 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.22</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -1,13 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SSODemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SSODemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* OAuth 2.0 客户端
|
||||
*
|
||||
* 对应调用 OAuth2OpenController 接口
|
||||
*/
|
||||
@Component
|
||||
public class OAuth2Client {
|
||||
|
||||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2";
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*
|
||||
* 默认使用 1;如果使用别的租户,可以调整
|
||||
*/
|
||||
public static final Long TENANT_ID = 1L;
|
||||
|
||||
private static final String CLIENT_ID = "yudao-sso-demo-by-password";
|
||||
private static final String CLIENT_SECRET = "test";
|
||||
|
||||
|
||||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 校验访问令牌,并返回它的基本信息
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 访问令牌的基本信息
|
||||
*/
|
||||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("token", token);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/check-token",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用刷新令牌,获得(刷新)访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 访问令牌
|
||||
*/
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("grant_type", "refresh_token");
|
||||
body.add("refresh_token", refreshToken);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/token",
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除访问令牌
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 成功
|
||||
*/
|
||||
public CommonResult<Boolean> revokeToken(String token) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", TENANT_ID.toString());
|
||||
addClientHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("token", token);
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/token",
|
||||
HttpMethod.DELETE,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
private static void addClientHeader(HttpHeaders headers) {
|
||||
// client 拼接,需要 BASE64 编码
|
||||
String client = CLIENT_ID + ":" + CLIENT_SECRET;
|
||||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8));
|
||||
headers.add("Authorization", "Basic " + client);
|
||||
}
|
||||
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* 用户 User 信息的客户端
|
||||
*
|
||||
* 对应调用 OAuth2UserController 接口
|
||||
*/
|
||||
@Component
|
||||
public class UserClient {
|
||||
|
||||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user";
|
||||
|
||||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public CommonResult<UserInfoRespDTO> getUser() {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||
addTokenHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/get",
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(body, headers),
|
||||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) {
|
||||
// 1.1 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||
addTokenHeader(headers);
|
||||
// 1.2 构建请求参数
|
||||
// 使用 updateReqDTO 即可
|
||||
|
||||
// 2. 执行请求
|
||||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||
BASE_URL + "/update",
|
||||
HttpMethod.PUT,
|
||||
new HttpEntity<>(updateReqDTO, headers),
|
||||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||
return exchange.getBody();
|
||||
}
|
||||
|
||||
|
||||
private static void addTokenHeader(HttpHeaders headers) {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
Assert.notNull(loginUser, "登录用户不能为空");
|
||||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken());
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用返回
|
||||
*
|
||||
* @param <T> 数据泛型
|
||||
*/
|
||||
@Data
|
||||
public class CommonResult<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
/**
|
||||
* 错误提示,用户可阅读
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 访问令牌 Response DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OAuth2AccessTokenRespDTO {
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@JsonProperty("refresh_token")
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
@JsonProperty("token_type")
|
||||
private String tokenType;
|
||||
|
||||
/**
|
||||
* 过期时间;单位:秒
|
||||
*/
|
||||
@JsonProperty("expires_in")
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 授权范围;如果多个授权范围,使用空格分隔
|
||||
*/
|
||||
private String scope;
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 校验令牌 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OAuth2CheckTokenRespDTO {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
@JsonProperty("user_type")
|
||||
private Integer userType;
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
@JsonProperty("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 客户端编号
|
||||
*/
|
||||
@JsonProperty("client_id")
|
||||
private String clientId;
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private List<String> scopes;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*
|
||||
* 时间戳 / 1000,即单位:秒
|
||||
*/
|
||||
private Long exp;
|
||||
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 获得用户基本信息 Response dto
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserInfoRespDTO {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private Integer sex;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 所在部门
|
||||
*/
|
||||
private Dept dept;
|
||||
|
||||
/**
|
||||
* 所属岗位数组
|
||||
*/
|
||||
private List<Post> posts;
|
||||
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
@Data
|
||||
public static class Dept {
|
||||
|
||||
/**
|
||||
* 部门编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位
|
||||
*/
|
||||
@Data
|
||||
public static class Post {
|
||||
|
||||
/**
|
||||
* 岗位编号
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 岗位名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 更新用户基本信息 Request DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserUpdateReqDTO {
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private Integer sex;
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Resource
|
||||
private OAuth2Client oauth2Client;
|
||||
|
||||
/**
|
||||
* 使用刷新令牌,获得(刷新)访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||
*/
|
||||
@PostMapping("/refresh-token")
|
||||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||
return oauth2Client.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 成功
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return oauth2Client.revokeToken(token);
|
||||
}
|
||||
// 返回成功
|
||||
return new CommonResult<>();
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.controller;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.UserClient;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
private UserClient userClient;
|
||||
|
||||
/**
|
||||
* 获得当前登录用户的基本信息
|
||||
*
|
||||
* @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||
*/
|
||||
@GetMapping("/get")
|
||||
public CommonResult<UserInfoRespDTO> getUser() {
|
||||
return userClient.getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前登录用户的昵称
|
||||
*
|
||||
* @param nickname 昵称
|
||||
* @return 成功
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) {
|
||||
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null);
|
||||
return userClient.updateUser(updateReqDTO);
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.config;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Resource
|
||||
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||
|
||||
@Resource
|
||||
private AccessDeniedHandlerImpl accessDeniedHandler;
|
||||
@Resource
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Bean
|
||||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
// 设置 URL 安全权限
|
||||
httpSecurity.csrf().disable() // 禁用 CSRF 保护
|
||||
.authorizeRequests()
|
||||
// 1. 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||
// 2. 登录相关的接口,可匿名访问
|
||||
.antMatchers("/auth/login-by-code").permitAll()
|
||||
.antMatchers("/auth/refresh-token").permitAll()
|
||||
.antMatchers("/auth/logout").permitAll()
|
||||
// last. 兜底规则,必须认证
|
||||
.and().authorizeRequests()
|
||||
.anyRequest().authenticated();
|
||||
|
||||
// 设置处理器
|
||||
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
|
||||
.authenticationEntryPoint(authenticationEntryPoint);
|
||||
|
||||
// 添加 Token Filter
|
||||
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class LoginUser {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private List<String> scopes;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Token 过滤器,验证 token 的有效性
|
||||
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
private OAuth2Client oauth2Client;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
// 1. 获得访问令牌
|
||||
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 2. 基于 token 构建登录用户
|
||||
LoginUser loginUser = buildLoginUserByToken(token);
|
||||
// 3. 设置当前用户
|
||||
if (loginUser != null) {
|
||||
SecurityUtils.setLoginUser(loginUser, request);
|
||||
}
|
||||
}
|
||||
|
||||
// 继续过滤链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private LoginUser buildLoginUserByToken(String token) {
|
||||
try {
|
||||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token);
|
||||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData();
|
||||
if (accessToken == null) {
|
||||
return null;
|
||||
}
|
||||
// 构建登录用户
|
||||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
|
||||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
|
||||
.setAccessToken(accessToken.getAccessToken());
|
||||
} catch (Exception exception) {
|
||||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
|
||||
*
|
||||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@SuppressWarnings("JavadocReference")
|
||||
@Slf4j
|
||||
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||
throws IOException, ServletException {
|
||||
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
||||
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
|
||||
SecurityUtils.getLoginUserId(), e);
|
||||
// 返回 403
|
||||
CommonResult<Object> result = new CommonResult<>();
|
||||
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||
result.setMsg("没有该操作权限");
|
||||
ServletUtils.writeJSON(response, result);
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
|
||||
*
|
||||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
||||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
|
||||
// 返回 401
|
||||
CommonResult<Object> result = new CommonResult<>();
|
||||
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||
result.setMsg("账号未登录");
|
||||
ServletUtils.writeJSON(response, result);
|
||||
}
|
||||
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||
|
||||
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 安全服务工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
public static final String AUTHORIZATION_BEARER = "Bearer";
|
||||
|
||||
private SecurityUtils() {}
|
||||
|
||||
/**
|
||||
* 从请求中,获得认证 Token
|
||||
*
|
||||
* @param request 请求
|
||||
* @param header 认证 Token 对应的 Header 名字
|
||||
* @return 认证 Token
|
||||
*/
|
||||
public static String obtainAuthorization(HttpServletRequest request, String header) {
|
||||
String authorization = request.getHeader(header);
|
||||
if (!StringUtils.hasText(authorization)) {
|
||||
return null;
|
||||
}
|
||||
int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
|
||||
if (index == -1) { // 未找到
|
||||
return null;
|
||||
}
|
||||
return authorization.substring(index + 7).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前认证信息
|
||||
*
|
||||
* @return 认证信息
|
||||
*/
|
||||
public static Authentication getAuthentication() {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
return context.getAuthentication();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户
|
||||
*
|
||||
* @return 当前用户
|
||||
*/
|
||||
@Nullable
|
||||
public static LoginUser getLoginUser() {
|
||||
Authentication authentication = getAuthentication();
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号,从上下文中
|
||||
*
|
||||
* @return 用户编号
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getLoginUserId() {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
return loginUser != null ? loginUser.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前用户
|
||||
*
|
||||
* @param loginUser 登录用户
|
||||
* @param request 请求
|
||||
*/
|
||||
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
|
||||
// 创建 Authentication,并设置到上下文
|
||||
Authentication authentication = buildAuthentication(loginUser, request);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
|
||||
// 创建 UsernamePasswordAuthenticationToken 对象
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
loginUser, null, Collections.emptyList());
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 客户端工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ServletUtils {
|
||||
|
||||
/**
|
||||
* 返回 JSON 字符串
|
||||
*
|
||||
* @param response 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||
String content = JSONUtil.toJsonStr(object);
|
||||
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
}
|
||||
|
||||
public static void write(HttpServletResponse response, String text, String contentType) {
|
||||
ServletUtil.write(response, text, contentType);
|
||||
}
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
server:
|
||||
port: 18080
|
@ -1,154 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>首页</title>
|
||||
<!-- jQuery:操作 dom、发起请求等 -->
|
||||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
/**
|
||||
* 跳转单点登录
|
||||
*/
|
||||
function passwordLogin() {
|
||||
window.location.href = '/login.html'
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改昵称
|
||||
*/
|
||||
function updateNickname() {
|
||||
const nickname = prompt("请输入新的昵称", "");
|
||||
if (!nickname) {
|
||||
return;
|
||||
}
|
||||
// 更新用户的昵称
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('更新昵称失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('更新昵称成功!');
|
||||
$('#nicknameSpan').html(nickname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
function refreshToken() {
|
||||
const refreshToken = localStorage.getItem('REFRESH-TOKEN');
|
||||
if (!refreshToken) {
|
||||
alert("获取不到刷新令牌");
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
|
||||
method: 'POST',
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('刷新访问令牌失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('更新访问令牌成功!');
|
||||
$('#accessTokenSpan').html(result.data.access_token);
|
||||
|
||||
// 设置到 localStorage 中
|
||||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出,删除访问令牌
|
||||
*/
|
||||
function logout() {
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
if (!accessToken) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/auth/logout",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('退出登录失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
alert('退出登录成功!');
|
||||
// 删除 localStorage 中
|
||||
localStorage.removeItem('ACCESS-TOKEN');
|
||||
localStorage.removeItem('REFRESH-TOKEN');
|
||||
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||
// 情况一:未登录
|
||||
if (!accessToken) {
|
||||
$('#noLoginDiv').css("display", "block");
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:已登录
|
||||
$('#yesLoginDiv').css("display", "block");
|
||||
$('#accessTokenSpan').html(accessToken);
|
||||
// 获得登录用户的信息
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:18080/user/get",
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('获得个人信息失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
$('#nicknameSpan').html(result.data.nickname);
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
|
||||
<div id="noLoginDiv" style="display: none">
|
||||
您未登录,点击 <a href="#" onclick="passwordLogin()">跳转 </a> 账号密码登录
|
||||
</div>
|
||||
|
||||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
|
||||
<div id="yesLoginDiv" style="display: none">
|
||||
您已登录!<button onclick="logout()">退出登录</button> <br />
|
||||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
|
||||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
body { /** 页面居中 */
|
||||
border-radius: 20px;
|
||||
height: 350px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
</style>
|
||||
</html>
|
@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录</title>
|
||||
<!-- jQuery:操作 dom、发起请求等 -->
|
||||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
function login() {
|
||||
const clientId = 'yudao-sso-demo-by-password'; // 可以改写成,你的 clientId
|
||||
const clientSecret = 'test'; // 可以改写成,你的 clientSecret
|
||||
const grantType = 'password'; // 密码模式
|
||||
|
||||
// 账号 + 密码
|
||||
const username = $('#username').val();
|
||||
const password = $('#password').val();
|
||||
if (username.length === 0 || password.length === 0) {
|
||||
alert('账号或密码未输入');
|
||||
return;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
$.ajax({
|
||||
url: "http://127.0.0.1:48080/admin-api/system/oauth2/token?"
|
||||
// 客户端
|
||||
+ "client_id=" + clientId
|
||||
+ "&client_secret=" + clientSecret
|
||||
// 密码模式的参数
|
||||
+ "&grant_type=" + grantType
|
||||
+ "&username=" + username
|
||||
+ "&password=" + password
|
||||
+ '&scope=user.read user.write',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'tenant-id': '1', // 多租户编号,写死
|
||||
},
|
||||
success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
alert('登录失败,原因:' + result.msg)
|
||||
return;
|
||||
}
|
||||
// 设置到 localStorage 中
|
||||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||
|
||||
// 提示登录成功
|
||||
alert('登录成功!点击确认,跳转回首页');
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
账号:<input id="username" value="admin" /> <br />
|
||||
密码:<input id="password" value="admin123" > <br />
|
||||
<button style="float: right; margin-top: 5px;" onclick="login()">登录</button>
|
||||
</body>
|
||||
<style>
|
||||
body { /** 页面居中 */
|
||||
border-radius: 20px;
|
||||
height: 350px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
</style>
|
||||
</html>
|
@ -24,19 +24,16 @@
|
||||
<module>yudao-spring-boot-starter-mq</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-excel</module>
|
||||
<module>yudao-spring-boot-starter-test</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-biz-operatelog</module>
|
||||
<module>yudao-spring-boot-starter-biz-dict</module>
|
||||
<module>yudao-spring-boot-starter-biz-sms</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-biz-pay</module>
|
||||
<module>yudao-spring-boot-starter-biz-tenant</module>
|
||||
<module>yudao-spring-boot-starter-biz-data-permission</module>
|
||||
<module>yudao-spring-boot-starter-biz-error-code</module>
|
||||
<module>yudao-spring-boot-starter-biz-ip</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-flowable</module>
|
||||
<module>yudao-spring-boot-starter-captcha</module>
|
||||
<module>yudao-spring-boot-starter-websocket</module>
|
||||
<module>yudao-spring-boot-starter-desensitize</module>
|
||||
|
@ -41,12 +41,6 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -1,108 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionAnnotationInterceptor} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionAnnotationInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private MethodInvocation methodInvocation;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
interceptor.getDataPermissionCache().clear();
|
||||
}
|
||||
|
||||
@Test // 无 @DataPermission 注解
|
||||
public void testInvoke_none() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestNone.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("none", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
@Test // 在 Method 上有 @DataPermission 注解
|
||||
public void testInvoke_method() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestMethod.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("method", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
@Test // 在 Class 上有 @DataPermission 注解
|
||||
public void testInvoke_class() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestClass.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("class", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
private void mockMethodInvocation(Class<?> clazz) throws Throwable {
|
||||
Object targetObject = clazz.newInstance();
|
||||
Method method = targetObject.getClass().getMethod("echo");
|
||||
when(methodInvocation.getThis()).thenReturn(targetObject);
|
||||
when(methodInvocation.getMethod()).thenReturn(method);
|
||||
when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
|
||||
}
|
||||
|
||||
static class TestMethod {
|
||||
|
||||
@DataPermission(enable = false)
|
||||
public String echo() {
|
||||
return "method";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@DataPermission(enable = false)
|
||||
static class TestClass {
|
||||
|
||||
public String echo() {
|
||||
return "class";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestNone {
|
||||
|
||||
public String echo() {
|
||||
return "none";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionContextHolder} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DataPermissionContextHolderTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
DataPermissionContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGet() {
|
||||
// mock 方法
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
|
||||
// 调用
|
||||
DataPermission result = DataPermissionContextHolder.get();
|
||||
// 断言
|
||||
assertSame(result, dataPermission02);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPush() {
|
||||
// 调用
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
// 断言
|
||||
DataPermission first = DataPermissionContextHolder.getAll().get(0);
|
||||
DataPermission second = DataPermissionContextHolder.getAll().get(1);
|
||||
assertSame(dataPermission01, first);
|
||||
assertSame(dataPermission02, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
// mock 方法
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
|
||||
// 调用
|
||||
DataPermission result = DataPermissionContextHolder.remove();
|
||||
// 断言
|
||||
assertSame(result, dataPermission02);
|
||||
assertEquals(1, DataPermissionContextHolder.getAll().size());
|
||||
}
|
||||
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionDatabaseInterceptor} 的单元测试
|
||||
* 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)}
|
||||
* 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)}
|
||||
* 以及在这个过程中,ContextHolder 和 MappedStatementCache
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionDatabaseInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 清理上下文
|
||||
DataPermissionDatabaseInterceptor.ContextHolder.clear();
|
||||
// 清空缓存
|
||||
interceptor.getMappedStatementCache().clear();
|
||||
}
|
||||
|
||||
@Test // 不存在规则,且不匹配
|
||||
public void testBeforeQuery_withoutRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 存在规则,且不匹配
|
||||
public void testBeforeQuery_withMatchRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
// mock 方法(数据权限)
|
||||
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
|
||||
.thenReturn(singletonList(new DeptDataPermissionRule()));
|
||||
// mock 方法(MPBoundSql)
|
||||
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
|
||||
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
|
||||
// mock 方法(SQL)
|
||||
String sql = "select * from t_user where id = 1";
|
||||
when(mpBs.sql()).thenReturn(sql);
|
||||
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
verify(mpBs, times(1)).sql(
|
||||
eq("SELECT * FROM t_user WHERE id = 1 AND t_user.dept_id = 100"));
|
||||
// 断言缓存
|
||||
assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 存在规则,但不匹配
|
||||
public void testBeforeQuery_withoutMatchRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
// mock 方法(数据权限)
|
||||
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
|
||||
.thenReturn(singletonList(new DeptDataPermissionRule()));
|
||||
// mock 方法(MPBoundSql)
|
||||
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
|
||||
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
|
||||
// mock 方法(SQL)
|
||||
String sql = "select * from t_role where id = 1";
|
||||
when(mpBs.sql()).thenReturn(sql);
|
||||
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
verify(mpBs, times(1)).sql(
|
||||
eq("SELECT * FROM t_role WHERE id = 1"));
|
||||
// 断言缓存
|
||||
assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNoRewritable() {
|
||||
// 准备参数
|
||||
MappedStatement ms = mock(MappedStatement.class);
|
||||
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
|
||||
// mock 方法
|
||||
when(ms.getId()).thenReturn("selectById");
|
||||
|
||||
// 调用
|
||||
interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
|
||||
// 断言
|
||||
Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements =
|
||||
interceptor.getMappedStatementCache().getNoRewritableMappedStatements();
|
||||
assertEquals(1, noRewritableMappedStatements.size());
|
||||
assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoRewritable() {
|
||||
// 准备参数
|
||||
MappedStatement ms = mock(MappedStatement.class);
|
||||
// mock 方法
|
||||
when(ms.getId()).thenReturn("selectById");
|
||||
// mock 数据
|
||||
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
|
||||
interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
|
||||
|
||||
// 场景一,rules 为空
|
||||
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null));
|
||||
// 场景二,rules 非空,可重写
|
||||
assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule())));
|
||||
// 场景三,rule 非空,不可重写
|
||||
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules));
|
||||
}
|
||||
|
||||
private static class DeptDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
private static final String COLUMN = "dept_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return SetUtils.asSet("t_user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
LongValue value = new LongValue(100L);
|
||||
return new EqualsTo(column, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class EmptyDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,533 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionDatabaseInterceptor} 的单元测试
|
||||
* 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试
|
||||
* 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionDatabaseInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 租户的数据权限规则
|
||||
DataPermissionRule tenantRule = new DataPermissionRule() {
|
||||
|
||||
private static final String COLUMN = "tenant_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试
|
||||
"t_user", "t_role"); // 满足自己的单元测试
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
LongValue value = new LongValue(1L);
|
||||
return new EqualsTo(column, value);
|
||||
}
|
||||
|
||||
};
|
||||
// 部门的数据权限规则
|
||||
DataPermissionRule deptRule = new DataPermissionRule() {
|
||||
|
||||
private static final String COLUMN = "dept_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return asSet("t_user"); // 满足自己的单元测试
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
ExpressionList values = new ExpressionList(new LongValue(10L),
|
||||
new LongValue(20L));
|
||||
return new InExpression(column, values);
|
||||
}
|
||||
|
||||
};
|
||||
// 设置到上下文,保证
|
||||
DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete() {
|
||||
assertSql("delete from entity where id = ?",
|
||||
"DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void update() {
|
||||
assertSql("update entity set name = ? where id = ?",
|
||||
"UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSingle() {
|
||||
// 单表
|
||||
assertSql("select * from entity where id = ?",
|
||||
"SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1");
|
||||
|
||||
assertSql("select * from entity where id = ? or name = ?",
|
||||
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
|
||||
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
|
||||
|
||||
/* not */
|
||||
assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
|
||||
"SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectIn() {
|
||||
/* in */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
// 在最前
|
||||
assertSql("SELECT * FROM entity e WHERE e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
|
||||
"SELECT * FROM entity e WHERE e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
|
||||
// 在最后
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
// 在中间
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
|
||||
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectEq() {
|
||||
/* = */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectInnerNotEq() {
|
||||
/* inner not = */
|
||||
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))",
|
||||
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)",
|
||||
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectExists() {
|
||||
/* EXISTS */
|
||||
assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
|
||||
/* NOT EXISTS */
|
||||
assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelect() {
|
||||
/* >= */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
|
||||
/* <= */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
|
||||
/* <> */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectFromSelect() {
|
||||
assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))",
|
||||
"SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectBodySubSelect() {
|
||||
assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1",
|
||||
"SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectLeftJoin() {
|
||||
// left join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"left join entity2 e2 on e1.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
|
||||
"WHERE e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectRightJoin() {
|
||||
// right join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM with_as_1 e " +
|
||||
"right join entity1 e1 on e1.id = e.id",
|
||||
"SELECT * FROM with_as_1 e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id " +
|
||||
"right join entity2 e2 on e1.id = e2.id ",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
|
||||
"RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
|
||||
"WHERE e2.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectMixJoin() {
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id " +
|
||||
"left join entity2 e2 on e1.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
|
||||
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"right join entity2 e2 on e1.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
|
||||
"WHERE e2.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"inner join entity2 e2 on e1.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void selectJoinSubSelect() {
|
||||
assertSql("select * from (select * from entity) e1 " +
|
||||
"left join entity2 e2 on e1.id = e2.id",
|
||||
"SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " +
|
||||
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1");
|
||||
|
||||
assertSql("select * from entity1 e1 " +
|
||||
"left join (select * from entity2) e2 " +
|
||||
"on e1.id = e2.id",
|
||||
"SELECT * FROM entity1 e1 " +
|
||||
"LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " +
|
||||
"ON e1.id = e2.id " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubJoin() {
|
||||
|
||||
assertSql("select * FROM " +
|
||||
"(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)",
|
||||
"SELECT * FROM " +
|
||||
"(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
|
||||
"WHERE e2.tenant_id = 1");
|
||||
|
||||
assertSql("select * FROM " +
|
||||
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)",
|
||||
"SELECT * FROM " +
|
||||
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
|
||||
|
||||
assertSql("select * FROM " +
|
||||
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " +
|
||||
"right join entity3 e3 on e1.id = e3.id",
|
||||
"SELECT * FROM " +
|
||||
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
|
||||
"RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " +
|
||||
"WHERE e3.tenant_id = 1");
|
||||
|
||||
|
||||
assertSql("select * FROM entity e " +
|
||||
"LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " +
|
||||
"on e.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
|
||||
"ON e.id = e2.id AND e2.tenant_id = 1 " +
|
||||
"WHERE e.tenant_id = 1");
|
||||
|
||||
assertSql("select * FROM entity e " +
|
||||
"LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
|
||||
"on e.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
|
||||
"ON e.id = e2.id AND e1.tenant_id = 1 " +
|
||||
"WHERE e.tenant_id = 1");
|
||||
|
||||
assertSql("select * FROM entity e " +
|
||||
"RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
|
||||
"on e.id = e2.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
|
||||
"ON e.id = e2.id AND e.tenant_id = 1 " +
|
||||
"WHERE e1.tenant_id = 1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void selectLeftJoinMultipleTrailingOn() {
|
||||
// 多个 on 尾缀的
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN entity2 e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " +
|
||||
"ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectInnerJoin() {
|
||||
// inner join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
|
||||
"WHERE e.id = ? OR e.name = ?");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?)");
|
||||
|
||||
// 隐式内连接
|
||||
assertSql("SELECT * FROM entity,entity1 " +
|
||||
"WHERE entity.id = entity1.id",
|
||||
"SELECT * FROM entity, entity1 " +
|
||||
"WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
|
||||
|
||||
// 隐式内连接
|
||||
assertSql("SELECT * FROM entity a, with_as_entity1 b " +
|
||||
"WHERE a.id = b.id",
|
||||
"SELECT * FROM entity a, with_as_entity1 b " +
|
||||
"WHERE a.id = b.id AND a.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " +
|
||||
"WHERE a.id = b.id",
|
||||
"SELECT * FROM with_as_entity a, with_as_entity1 b " +
|
||||
"WHERE a.id = b.id");
|
||||
|
||||
// SubJoin with 隐式内连接
|
||||
assertSql("SELECT * FROM (entity,entity1) " +
|
||||
"WHERE entity.id = entity1.id",
|
||||
"SELECT * FROM (entity, entity1) " +
|
||||
"WHERE entity.id = entity1.id " +
|
||||
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM ((entity,entity1),entity2) " +
|
||||
"WHERE entity.id = entity1.id and entity.id = entity2.id",
|
||||
"SELECT * FROM ((entity, entity1), entity2) " +
|
||||
"WHERE entity.id = entity1.id AND entity.id = entity2.id " +
|
||||
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM (entity,(entity1,entity2)) " +
|
||||
"WHERE entity.id = entity1.id and entity.id = entity2.id",
|
||||
"SELECT * FROM (entity, (entity1, entity2)) " +
|
||||
"WHERE entity.id = entity1.id AND entity.id = entity2.id " +
|
||||
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
|
||||
|
||||
// 沙雕的括号写法
|
||||
assertSql("SELECT * FROM (((entity,entity1))) " +
|
||||
"WHERE entity.id = entity1.id",
|
||||
"SELECT * FROM (((entity, entity1))) " +
|
||||
"WHERE entity.id = entity1.id " +
|
||||
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void selectWithAs() {
|
||||
assertSql("with with_as_A as (select * from entity) select * from with_as_A",
|
||||
"WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void selectIgnoreTable() {
|
||||
assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)",
|
||||
"SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)");
|
||||
}
|
||||
|
||||
private void assertSql(String sql, String targetSql) {
|
||||
assertEquals(targetSql, interceptor.parserSingle(sql, null));
|
||||
}
|
||||
|
||||
|
||||
// ========== 额外的测试 ==========
|
||||
|
||||
@Test
|
||||
public void testSelectSingle() {
|
||||
// 单表
|
||||
assertSql("select * from t_user where id = ?",
|
||||
"SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
|
||||
|
||||
assertSql("select * from t_user where id = ? or name = ?",
|
||||
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
|
||||
|
||||
assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
|
||||
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
|
||||
|
||||
/* not */
|
||||
assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
|
||||
"SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectLeftJoin() {
|
||||
// left join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"left join t_role e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"left join t_role e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRightJoin() {
|
||||
// right join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"right join t_role e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"right join t_role e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectInnerJoin() {
|
||||
// inner join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
|
||||
"WHERE e.id = ? OR e.name = ?");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?)");
|
||||
|
||||
// 没有 On 的 inner join
|
||||
assertSql("SELECT * FROM entity,entity1 " +
|
||||
"WHERE entity.id = entity1.id",
|
||||
"SELECT * FROM entity, entity1 " +
|
||||
"WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
|
||||
}
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionRuleFactoryImpl} 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionRuleFactoryImpl dataPermissionRuleFactory;
|
||||
|
||||
@Spy
|
||||
private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(),
|
||||
new DataPermissionRule02());
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
DataPermissionContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_02() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertSame(rules, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_03() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_04() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(DataPermissionRule01.class, result.get(0).getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_05() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(DataPermissionRule02.class, result.get(0).getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_06() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertSame(rules, result);
|
||||
}
|
||||
|
||||
@DataPermission(enable = false)
|
||||
static class TestClass03 {}
|
||||
|
||||
@DataPermission(includeRules = DataPermissionRule01.class)
|
||||
static class TestClass04 {}
|
||||
|
||||
@DataPermission(excludeRules = DataPermissionRule01.class)
|
||||
static class TestClass05 {}
|
||||
|
||||
@DataPermission
|
||||
static class TestClass06 {}
|
||||
|
||||
static class DataPermissionRule01 implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DataPermissionRule02 implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,238 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
||||
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DeptDataPermissionRule} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DeptDataPermissionRule rule;
|
||||
|
||||
@Mock
|
||||
private PermissionApi permissionApi;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
// 清空 rule
|
||||
rule.getTableNames().clear();
|
||||
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
|
||||
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
|
||||
}
|
||||
|
||||
@Test // 无 LoginUser
|
||||
public void testGetExpression_noLoginUser() {
|
||||
// 准备参数
|
||||
String tableName = randomString();
|
||||
Alias tableAlias = new Alias(randomString());
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertNull(expression);
|
||||
}
|
||||
|
||||
@Test // 无数据权限时
|
||||
public void testGetExpression_noDeptDataPermission() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(permissionApi 返回 null)
|
||||
when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(null);
|
||||
|
||||
// 调用
|
||||
NullPointerException exception = assertThrows(NullPointerException.class,
|
||||
() -> rule.getExpression(tableName, tableAlias));
|
||||
// 断言
|
||||
assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 全部数据权限
|
||||
public void testGetExpression_allDeptDataPermission() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertNull(expression);
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
public void testGetExpression_noDept_noSelf() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("null = null", expression.toString());
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(字段都不符合)
|
||||
public void testGetExpression_noDeptColumn_noSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertSame(EXPRESSION_NULL, expression);
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(self 符合)
|
||||
public void testGetExpression_noDeptColumn_yesSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setSelf(true);
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
// 添加 user 字段配置
|
||||
rule.addUserColumn("t_user", "id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("u.id = 1", expression.toString());
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(dept 符合)
|
||||
public void testGetExpression_yesDeptColumn_noSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
// 添加 dept 字段配置
|
||||
rule.addDeptColumn("t_user", "dept_id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("u.dept_id IN (10, 20)", expression.toString());
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(dept + self 符合)
|
||||
public void testGetExpression_yesDeptColumn_yesSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
|
||||
.setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
|
||||
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
|
||||
// 添加 user 字段配置
|
||||
rule.addUserColumn("t_user", "id");
|
||||
// 添加 dept 字段配置
|
||||
rule.addDeptColumn("t_user", "dept_id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString());
|
||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DataPermissionUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testExecuteIgnore() {
|
||||
DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable()));
|
||||
}
|
||||
|
||||
}
|
@ -40,11 +40,5 @@
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -1,48 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.dict.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
|
||||
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DictFrameworkUtils} 的单元测试
|
||||
*/
|
||||
public class DictFrameworkUtilsTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Mock
|
||||
private DictDataApi dictDataApi;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
DictFrameworkUtils.init(dictDataApi);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDictDataLabel() {
|
||||
// mock 数据
|
||||
DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
|
||||
// mock 方法
|
||||
when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(dataRespDTO);
|
||||
// 断言返回值
|
||||
assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseDictDataValue() {
|
||||
// mock 数据
|
||||
DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
|
||||
// mock 方法
|
||||
when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(resp);
|
||||
// 断言返回值
|
||||
assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel()));
|
||||
}
|
||||
|
||||
}
|
@ -43,12 +43,6 @@
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -1,36 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.ip.core.utils;
|
||||
|
||||
|
||||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link AreaUtils} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AreaUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testGetArea() {
|
||||
// 调用:北京
|
||||
Area area = AreaUtils.getArea(110100);
|
||||
// 断言
|
||||
assertEquals(area.getId(), 110100);
|
||||
assertEquals(area.getName(), "北京市");
|
||||
assertEquals(area.getType(), AreaTypeEnum.CITY.getType());
|
||||
assertEquals(area.getParent().getId(), 110000);
|
||||
assertEquals(area.getChildren().size(), 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormat() {
|
||||
assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区");
|
||||
assertEquals(AreaUtils.format(1), "中国");
|
||||
assertEquals(AreaUtils.format(2), "蒙古");
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.ip.core.utils;
|
||||
|
||||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link IPUtils} 的单元测试
|
||||
*
|
||||
* @author wanglhup
|
||||
*/
|
||||
public class IPUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testGetAreaId_string() {
|
||||
// 120.202.4.0|120.202.4.255|420600
|
||||
Integer areaId = IPUtils.getAreaId("120.202.4.50");
|
||||
assertEquals(420600, areaId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAreaId_long() throws Exception {
|
||||
// 120.203.123.0|120.203.133.255|360900
|
||||
long ip = Searcher.checkIP("120.203.123.250");
|
||||
Integer areaId = IPUtils.getAreaId(ip);
|
||||
assertEquals(360900, areaId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetArea_string() {
|
||||
// 120.202.4.0|120.202.4.255|420600
|
||||
Area area = IPUtils.getArea("120.202.4.50");
|
||||
assertEquals("襄阳市", area.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetArea_long() throws Exception {
|
||||
// 120.203.123.0|120.203.133.255|360900
|
||||
long ip = Searcher.checkIP("120.203.123.252");
|
||||
Area area = IPUtils.getArea(ip);
|
||||
assertEquals("宜春市", area.getName());
|
||||
}
|
||||
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<?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">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>支付拓展,接入国内多个支付渠道
|
||||
1. 支付宝,基于官方 SDK 接入
|
||||
2. 微信支付,基于 weixin-java-pay 接入
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.79.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -1,22 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 支付配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class YudaoPayAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public PayClientFactory payClientFactory() {
|
||||
return new PayClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface PayClient {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
*
|
||||
* @return 渠道编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析 order 回调数据
|
||||
*
|
||||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
* @param body HTTP 回调接口的 request body
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
|
||||
|
||||
/**
|
||||
* 获得支付订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderRespDTO getOrder(String outTradeNo);
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
/**
|
||||
* 调用支付渠道,进行退款
|
||||
*
|
||||
* @param reqDTO 统一退款请求信息
|
||||
* @return 退款信息
|
||||
*/
|
||||
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析 refund 回调数据
|
||||
*
|
||||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
* @param body HTTP 回调接口的 request body
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
|
||||
|
||||
/**
|
||||
* 获得退款订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @param outRefundNo 外部退款号
|
||||
* @return 退款订单信息
|
||||
*/
|
||||
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
|
||||
|
||||
/**
|
||||
* 调用渠道,进行转账
|
||||
*
|
||||
* @param reqDTO 统一转账请求信息
|
||||
* @return 转账信息
|
||||
*/
|
||||
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 获得转账订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @param type 转账类型
|
||||
* @return 转账信息
|
||||
*/
|
||||
PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.Validator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 支付客户端的配置,本质是支付渠道的配置
|
||||
* 每个不同的渠道,需要不同的配置,通过子类来定义
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
// @JsonTypeInfo 注解的作用,Jackson 多态
|
||||
// 1. 序列化到时数据库时,增加 @class 属性。
|
||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
||||
public interface PayClientConfig {
|
||||
|
||||
/**
|
||||
* 参数校验
|
||||
*
|
||||
* @param validator 校验对象
|
||||
*/
|
||||
void validate(Validator validator);
|
||||
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface PayClientFactory {
|
||||
|
||||
/**
|
||||
* 获得支付客户端
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @return 支付客户端
|
||||
*/
|
||||
PayClient getPayClient(Long channelId);
|
||||
|
||||
/**
|
||||
* 创建支付客户端
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @param channelCode 渠道编码
|
||||
* @param config 支付配置
|
||||
*/
|
||||
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
|
||||
Config config);
|
||||
|
||||
/**
|
||||
* 注册支付客户端 Class,用于模块中实现的 PayClient
|
||||
*
|
||||
* @param channel 支付渠道的编码的枚举
|
||||
* @param payClientClass 支付客户端 class
|
||||
*/
|
||||
void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass);
|
||||
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 渠道支付订单 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class PayOrderRespDTO {
|
||||
|
||||
/**
|
||||
* 支付状态
|
||||
*
|
||||
* 枚举:{@link PayOrderStatusRespEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
*
|
||||
* 对应 PayOrderExtensionDO 的 no 字段
|
||||
*/
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
/**
|
||||
* 支付渠道用户编号
|
||||
*/
|
||||
private String channelUserId;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的同步/异步通知结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
// ========== 主动发起支付时,会返回的字段 ==========
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
*
|
||||
* 枚举 {@link PayOrderDisplayModeEnum} 类
|
||||
*/
|
||||
private String displayMode;
|
||||
/**
|
||||
* 展示内容
|
||||
*/
|
||||
private String displayContent;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*
|
||||
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||
* 如果是系统异常,则会抛出 {@link PayException}
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
public PayOrderRespDTO() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的订单返回
|
||||
*/
|
||||
public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.displayMode = displayMode;
|
||||
respDTO.displayContent = displayContent;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的订单返回
|
||||
*/
|
||||
public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelOrderNo = channelOrderNo;
|
||||
respDTO.channelUserId = channelUserId;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定状态的订单返回,适合支付渠道回调时
|
||||
*/
|
||||
public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = status;
|
||||
respDTO.channelOrderNo = channelOrderNo;
|
||||
respDTO.channelUserId = channelUserId;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
|
||||
*/
|
||||
public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outTradeNo, Object rawData) {
|
||||
PayOrderRespDTO respDTO = new PayOrderRespDTO();
|
||||
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outTradeNo = outTradeNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 统一下单 Request DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class PayOrderUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
// ========== 商户相关字段 ==========
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
*
|
||||
* 对应 PayOrderExtensionDO 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
@NotEmpty(message = "商品标题不能为空")
|
||||
@Length(max = 32, message = "商品标题不能超过 32")
|
||||
private String subject;
|
||||
/**
|
||||
* 商品描述信息
|
||||
*/
|
||||
@Length(max = 128, message = "商品描述信息长度不能超过128")
|
||||
private String body;
|
||||
/**
|
||||
* 支付结果的 notify 回调地址
|
||||
*/
|
||||
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
/**
|
||||
* 支付结果的 return 回调地址
|
||||
*/
|
||||
@URL(message = "支付结果的 return 回调地址必须是 URL 格式")
|
||||
private String returnUrl;
|
||||
|
||||
// ========== 订单相关字段 ==========
|
||||
|
||||
/**
|
||||
* 支付金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 支付过期时间
|
||||
*/
|
||||
@NotNull(message = "支付过期时间不能为空")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
// ========== 拓展参数 ==========
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
*
|
||||
* 例如说,微信公众号需要传递 openid 参数
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
*
|
||||
* 如果不传递,则每个支付渠道使用默认的方式
|
||||
*
|
||||
* 枚举 {@link PayOrderDisplayModeEnum}
|
||||
*/
|
||||
private String displayMode;
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 渠道退款订单 Response DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayRefundRespDTO {
|
||||
|
||||
/**
|
||||
* 退款状态
|
||||
*
|
||||
* 枚举 {@link PayRefundStatusRespEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部退款号
|
||||
*
|
||||
* 对应 PayRefundDO 的 no 字段
|
||||
*/
|
||||
private String outRefundNo;
|
||||
|
||||
/**
|
||||
* 渠道退款单号
|
||||
*
|
||||
* 对应 PayRefundDO.channelRefundNo 字段
|
||||
*/
|
||||
private String channelRefundNo;
|
||||
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的异步通知结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*
|
||||
* 注意:这里返回的是业务异常,而是不系统异常。
|
||||
* 如果是系统异常,则会抛出 {@link PayException}
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
private PayRefundRespDTO() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO waitingOf(String channelRefundNo,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelRefundNo = channelRefundNo;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelRefundNo = channelRefundNo;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【FAILURE】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
|
||||
return failureOf(null, null,
|
||||
outRefundNo, rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【FAILURE】状态的退款返回
|
||||
*/
|
||||
public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outRefundNo, Object rawData) {
|
||||
PayRefundRespDTO respDTO = new PayRefundRespDTO();
|
||||
respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outRefundNo = outRefundNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 统一 退款 Request DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class PayRefundUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 外部订单号
|
||||
*
|
||||
* 对应 PayOrderExtensionDO 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 外部退款号
|
||||
*
|
||||
* 对应 PayRefundDO 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "退款请求单号不能为空")
|
||||
private String outRefundNo;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
@NotEmpty(message = "退款原因不能为空")
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 支付金额,单位:分
|
||||
*
|
||||
* 目前微信支付在退款的时候,必须传递该字段
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer payPrice;
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款结果的 notify 回调地址
|
||||
*/
|
||||
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统一转账 Response DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayTransferRespDTO {
|
||||
|
||||
/**
|
||||
* 转账状态
|
||||
*
|
||||
* 关联 {@link PayTransferStatusRespEnum#getStatus()}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 外部转账单号
|
||||
*
|
||||
*/
|
||||
private String outTransferNo;
|
||||
|
||||
/**
|
||||
* 支付渠道编号
|
||||
*/
|
||||
private String channelTransferNo;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 原始的返回结果
|
||||
*/
|
||||
private Object rawData;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO waitingOf(String channelTransferNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【IN_PROGRESS】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO dealingOf(String channelTransferNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【CLOSED】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus();
|
||||
respDTO.channelErrorCode = channelErrorCode;
|
||||
respDTO.channelErrorMsg = channelErrorMsg;
|
||||
// 相对通用的字段
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.successTime = successTime;
|
||||
// 相对通用的字段
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
return respDTO;
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
|
||||
|
||||
/**
|
||||
* 统一转账 Request DTO
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class PayTransferUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 转账类型
|
||||
*
|
||||
* 关联 {@link PayTransferTypeEnum#getType()}
|
||||
*/
|
||||
@NotNull(message = "转账类型不能为空")
|
||||
@InEnum(PayTransferTypeEnum.class)
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
@NotEmpty(message = "外部转账单编号不能为空")
|
||||
private String outTransferNo;
|
||||
|
||||
/**
|
||||
* 转账金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "转账金额不能为空")
|
||||
@Min(value = 1, message = "转账金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 转账标题
|
||||
*/
|
||||
@NotEmpty(message = "转账标题不能为空")
|
||||
@Length(max = 128, message = "转账标题不能超过 128")
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 收款人姓名
|
||||
*/
|
||||
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 支付宝登录号
|
||||
*/
|
||||
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
|
||||
private String alipayLogonId;
|
||||
|
||||
/**
|
||||
* 微信 openId
|
||||
*/
|
||||
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.exception;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 支付系统异常 Exception
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayException extends RuntimeException {
|
||||
|
||||
public PayException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
|
||||
|
||||
/**
|
||||
* 渠道编号
|
||||
*/
|
||||
private final Long channelId;
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final String channelCode;
|
||||
/**
|
||||
* 支付配置
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
public AbstractPayClient(Long channelId, String channelCode, Config config) {
|
||||
this.channelId = channelId;
|
||||
this.channelCode = channelCode;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.debug("[init][客户端({}) 初始化完成]", getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
public final void refresh(Config config) {
|
||||
// 判断是否更新
|
||||
if (config.equals(this.config)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
// 执行统一下单
|
||||
PayOrderRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedOrder(reqDTO);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
|
||||
try {
|
||||
return doParseOrderNotify(params, body);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||
getId(), params, body, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayOrderRespDTO getOrder(String outTradeNo) {
|
||||
try {
|
||||
return doGetOrder(outTradeNo);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
|
||||
getId(), outTradeNo, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
|
||||
throws Throwable;
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
// 执行统一退款
|
||||
PayRefundRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedRefund(reqDTO);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
|
||||
try {
|
||||
return doParseRefundNotify(params, body);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
|
||||
getId(), params, body, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
|
||||
try {
|
||||
return doGetRefund(outTradeNo, outRefundNo);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
|
||||
getId(), outTradeNo, outRefundNo, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
validatePayTransferReqDTO(reqDTO);
|
||||
PayTransferRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedTransfer(reqDTO);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) {
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
case ALIPAY_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class);
|
||||
break;
|
||||
}
|
||||
case WX_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
|
||||
try {
|
||||
return doGetTransfer(outTradeNo, type);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
|
||||
getId(), outTradeNo, type, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
|
||||
throws Throwable;
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
private PayException buildPayException(Throwable ex) {
|
||||
if (ex instanceof PayException) {
|
||||
return (PayException) ex;
|
||||
}
|
||||
throw new PayException(ex);
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
/**
|
||||
* 无需任何配置 PayClientConfig 实现类
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Data
|
||||
public class NonePayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* 配置名称
|
||||
* <p>
|
||||
* 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
public NonePayClientConfig(){
|
||||
this.name = "none-config";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Validator validator) {
|
||||
// 无任何配置不需要校验
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.mock.MockPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum.*;
|
||||
|
||||
/**
|
||||
* 支付客户端的工厂实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class PayClientFactoryImpl implements PayClientFactory {
|
||||
|
||||
/**
|
||||
* 支付客户端 Map
|
||||
*
|
||||
* key:渠道编号
|
||||
*/
|
||||
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 支付客户端 Class Map
|
||||
*/
|
||||
private final Map<PayChannelEnum, Class<?>> clientClass = new ConcurrentHashMap<>();
|
||||
|
||||
public PayClientFactoryImpl() {
|
||||
// 微信支付客户端
|
||||
clientClass.put(WX_PUB, WxPubPayClient.class);
|
||||
clientClass.put(WX_LITE, WxLitePayClient.class);
|
||||
clientClass.put(WX_APP, WxAppPayClient.class);
|
||||
clientClass.put(WX_BAR, WxBarPayClient.class);
|
||||
clientClass.put(WX_NATIVE, WxNativePayClient.class);
|
||||
// 支付包支付客户端
|
||||
clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class);
|
||||
clientClass.put(ALIPAY_QR, AlipayQrPayClient.class);
|
||||
clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
|
||||
clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
|
||||
clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
|
||||
// Mock 支付客户端
|
||||
clientClass.put(MOCK, MockPayClient.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerPayClientClass(PayChannelEnum channel, Class<?> payClientClass) {
|
||||
clientClass.put(channel, payClientClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayClient getPayClient(Long channelId) {
|
||||
AbstractPayClient<?> client = clients.get(channelId);
|
||||
if (client == null) {
|
||||
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
|
||||
Config config) {
|
||||
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
|
||||
if (client == null) {
|
||||
client = this.createPayClient(channelId, channelCode, config);
|
||||
client.init();
|
||||
clients.put(client.getId(), client);
|
||||
} else {
|
||||
client.refresh(config);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(Long channelId, String channelCode,
|
||||
Config config) {
|
||||
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
|
||||
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode));
|
||||
Class<?> payClientClass = clientClass.get(channelEnum);
|
||||
Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode));
|
||||
return (AbstractPayClient<Config>) ReflectUtil.newInstance(payClientClass, channelId, config);
|
||||
}
|
||||
|
||||
}
|
@ -1,342 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.AlipayResponse;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.*;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.*;
|
||||
import com.alipay.api.response.*;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> {
|
||||
|
||||
@Getter // 仅用于单测场景
|
||||
protected DefaultAlipayClient client;
|
||||
|
||||
public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void doInit() {
|
||||
AlipayConfig alipayConfig = new AlipayConfig();
|
||||
BeanUtil.copyProperties(config, alipayConfig, false);
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
|
||||
*
|
||||
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
|
||||
*/
|
||||
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
|
||||
Assert.isFalse(response.isSuccess());
|
||||
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
|
||||
// 1. 校验回调数据
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
|
||||
StandardCharsets.UTF_8.name(), config.getSignType());
|
||||
|
||||
// 2. 解析订单的状态
|
||||
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
|
||||
Integer status = parseStatus(bodyObj.get("trade_status"));
|
||||
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
|
||||
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
|
||||
status = PayOrderStatusRespEnum.REFUND.getStatus();
|
||||
}
|
||||
Assert.notNull(status, (Supplier<Throwable>) () -> {
|
||||
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
|
||||
});
|
||||
return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
|
||||
bodyObj.get("out_trade_no"), body);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
|
||||
model.setOutTradeNo(outTradeNo);
|
||||
// 1.2 构建 AlipayTradeQueryRequest 请求
|
||||
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
||||
request.setBizModel(model);
|
||||
AlipayTradeQueryResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) { // 不成功,例如说订单不存在
|
||||
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
outTradeNo, response);
|
||||
}
|
||||
// 2.2 解析订单的状态
|
||||
Integer status = parseStatus(response.getTradeStatus());
|
||||
Assert.notNull(status, () -> {
|
||||
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
|
||||
});
|
||||
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private static Integer parseStatus(String tradeStatus) {
|
||||
return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
|
||||
: ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
|
||||
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
|
||||
}
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
/**
|
||||
* 支付宝统一的退款接口 alipay.trade.refund
|
||||
*
|
||||
* @param reqDTO 退款请求 request DTO
|
||||
* @return 退款请求 Response
|
||||
*/
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getOutRefundNo());
|
||||
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
// 1.2 构建 AlipayTradePayRequest 请求
|
||||
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeRefundResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) {
|
||||
// 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) {
|
||||
return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
// 2.2 创建返回结果
|
||||
// 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
|
||||
// 另外,支付宝没有退款单号,所以不用设置
|
||||
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
|
||||
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
|
||||
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
|
||||
// ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
|
||||
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
|
||||
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
|
||||
throw new UnsupportedOperationException("支付宝无退款回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
|
||||
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
|
||||
model.setOutTradeNo(outTradeNo);
|
||||
model.setOutRequestNo(outRefundNo);
|
||||
model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
|
||||
// 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
|
||||
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeFastpayRefundQueryResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
if (!response.isSuccess()) {
|
||||
// 明确不存在的情况,应该就是失败,可进行关闭
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
}
|
||||
// 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
|
||||
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||
}
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
|
||||
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
|
||||
outRefundNo, response);
|
||||
}
|
||||
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 校验公钥类型 必须使用公钥证书模式
|
||||
if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
|
||||
}
|
||||
// 1.2 构建 AlipayFundTransUniTransferModel
|
||||
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
|
||||
// ① 通用的参数
|
||||
model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
|
||||
model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。
|
||||
model.setOutBizNo(reqDTO.getOutTransferNo());
|
||||
model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
|
||||
model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
|
||||
if (reqDTO.getChannelExtras() != null) {
|
||||
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
|
||||
}
|
||||
// ② 个性化的参数
|
||||
Participant payeeInfo = new Participant();
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
// TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
|
||||
// @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
|
||||
case ALIPAY_BALANCE: {
|
||||
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
|
||||
payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
|
||||
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
|
||||
model.setPayeeInfo(payeeInfo);
|
||||
break;
|
||||
}
|
||||
case BANK_CARD: {
|
||||
payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
|
||||
// TODO 待实现
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
default: {
|
||||
throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
|
||||
}
|
||||
}
|
||||
// 1.3 构建 AlipayFundTransUniTransferRequest
|
||||
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
|
||||
request.setBizModel(model);
|
||||
// 执行请求
|
||||
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
|
||||
// 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
|
||||
// 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
|
||||
return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTransferNo(), response);
|
||||
} else {
|
||||
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
|
||||
return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
|
||||
response.getOutBizNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
|
||||
// 1.1 构建 AlipayFundTransCommonQueryModel
|
||||
AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
|
||||
model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
|
||||
model.setBizScene("DIRECT_TRANSFER"); //业务场景
|
||||
model.setOutBizNo(outTradeNo);
|
||||
// 1.2 构建 AlipayFundTransCommonQueryRequest
|
||||
AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
|
||||
request.setBizModel(model);
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayFundTransCommonQueryResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理返回结果
|
||||
if (response.isSuccess()) {
|
||||
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
outTradeNo, response);
|
||||
}
|
||||
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
|
||||
return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
|
||||
}
|
||||
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
|
||||
response.getOutBizNo(), response);
|
||||
} else {
|
||||
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
|
||||
// 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
|
||||
if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
|
||||
return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
outTradeNo, response);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
protected String formatAmount(Integer amount) {
|
||||
return String.valueOf(amount / 100.0);
|
||||
}
|
||||
|
||||
protected String formatTime(LocalDateTime time) {
|
||||
return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
protected LocalDateTime parseTime(String str) {
|
||||
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradeAppPayModel;
|
||||
import com.alipay.api.request.AlipayTradeAppPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeAppPayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 支付宝【App 支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
|
||||
*
|
||||
* // TODO 芋艿:未详细测试,因为手头没 App
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeAppPayModel 请求
|
||||
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody() + "test");
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||
model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示
|
||||
String displayMode = PayOrderDisplayModeEnum.APP.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeAppPayResponse response = client.sdkExecute(request);
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePayModel;
|
||||
import com.alipay.api.request.AlipayTradePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝【条码支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
|
||||
if (StrUtil.isEmpty(authCode)) {
|
||||
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
|
||||
}
|
||||
|
||||
// 1.1 构建 AlipayTradePayModel 请求
|
||||
AlipayTradePayModel model = new AlipayTradePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setScene("bar_code"); // 当面付条码支付场景
|
||||
// ② 个性化的参数
|
||||
model.setAuthCode(authCode);
|
||||
// ③ 支付宝条码支付只有一种展示
|
||||
String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePayRequest 请求
|
||||
AlipayTradePayRequest request = new AlipayTradePayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePayResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
if ("10000".equals(response.getCode())) { // 免密支付
|
||||
LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment());
|
||||
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime,
|
||||
response.getOutTradeNo(), response)
|
||||
.setDisplayMode(displayMode).setDisplayContent("");
|
||||
}
|
||||
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
|
||||
return PayOrderRespDTO.waitingOf(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 支付宝的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class AlipayPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* 公钥类型 - 公钥模式
|
||||
*/
|
||||
public static final Integer MODE_PUBLIC_KEY = 1;
|
||||
/**
|
||||
* 公钥类型 - 证书模式
|
||||
*/
|
||||
public static final Integer MODE_CERTIFICATE = 2;
|
||||
|
||||
/**
|
||||
* 签名算法类型 - RSA
|
||||
*/
|
||||
public static final String SIGN_TYPE_DEFAULT = "RSA2";
|
||||
|
||||
/**
|
||||
* 网关地址
|
||||
*
|
||||
* 1. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
|
||||
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
|
||||
*/
|
||||
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String serverUrl;
|
||||
|
||||
/**
|
||||
* 开放平台上创建的应用的 ID
|
||||
*/
|
||||
@NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 签名算法类型,推荐:RSA2
|
||||
* <p>
|
||||
* {@link #SIGN_TYPE_DEFAULT}
|
||||
*/
|
||||
@NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private String signType;
|
||||
|
||||
/**
|
||||
* 公钥类型
|
||||
* 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
|
||||
* 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
|
||||
*/
|
||||
@NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
|
||||
private Integer mode;
|
||||
|
||||
// ========== 公钥模式 ==========
|
||||
/**
|
||||
* 商户私钥
|
||||
*/
|
||||
@NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class})
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 支付宝公钥字符串
|
||||
*/
|
||||
@NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class})
|
||||
private String alipayPublicKey;
|
||||
|
||||
// ========== 证书模式 ==========
|
||||
/**
|
||||
* 指定商户公钥应用证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class})
|
||||
private String appCertContent;
|
||||
/**
|
||||
* 指定支付宝公钥证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class})
|
||||
private String alipayPublicCertContent;
|
||||
/**
|
||||
* 指定根证书内容字符串
|
||||
*/
|
||||
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
|
||||
private String rootCertContent;
|
||||
|
||||
public interface ModePublicKey {
|
||||
}
|
||||
|
||||
public interface ModeCertificate {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Validator validator) {
|
||||
ValidationUtils.validate(validator, this,
|
||||
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
|
||||
}
|
||||
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.Method;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付宝【PC 网站】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
|
||||
*
|
||||
* @author XGD
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePagePayModel 请求
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
|
||||
// ② 个性化的参数
|
||||
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
|
||||
model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
|
||||
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
|
||||
String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
|
||||
PayOrderDisplayModeEnum.URL.getMode());
|
||||
|
||||
// 1.2 构建 AlipayTradePagePayRequest 请求
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePagePayResponse response;
|
||||
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
|
||||
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
|
||||
} else {
|
||||
response = client.pageExecute(request, Method.GET.name());
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
|
||||
/**
|
||||
* 支付宝【扫码支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePrecreateModel 请求
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
|
||||
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradePrecreateRequest 请求
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradePrecreateResponse response;
|
||||
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
// 证书模式
|
||||
response = client.certificateExecute(request);
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.http.Method;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradeWapPayModel;
|
||||
import com.alipay.api.request.AlipayTradeWapPayRequest;
|
||||
import com.alipay.api.response.AlipayTradeWapPayResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 支付宝【Wap 网站】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeWapPayModel 请求
|
||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝 Wap 支付只有一种展示:URL
|
||||
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
|
||||
|
||||
// 1.2 构建 AlipayTradeWapPayRequest 请求
|
||||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
model.setQuitUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
|
||||
// 2.2 处理结果
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.mock;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 模拟支付的 PayClient 实现类
|
||||
*
|
||||
* 模拟支付返回结果都是成功,方便大家日常流畅
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
|
||||
|
||||
private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS";
|
||||
|
||||
public MockPayClient(Long channelId, NonePayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.MOCK.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(),
|
||||
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) {
|
||||
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(),
|
||||
outTradeNo, MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
|
||||
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) {
|
||||
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(),
|
||||
outRefundNo, MOCK_RESP_SUCCESS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
|
||||
throw new UnsupportedOperationException("模拟支付无退款回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
|
||||
throw new UnsupportedOperationException("模拟支付无支付回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
}
|
@ -1,483 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.request.*;
|
||||
import com.github.binarywang.wxpay.bean.result.*;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.*;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
|
||||
|
||||
/**
|
||||
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
|
||||
*
|
||||
* @author 遇到源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {
|
||||
|
||||
protected WxPayService client;
|
||||
|
||||
public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 client 客户端
|
||||
*
|
||||
* @param tradeType 交易类型
|
||||
*/
|
||||
protected void doInit(String tradeType) {
|
||||
// 创建 config 配置
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "privateCertContent");
|
||||
payConfig.setTradeType(tradeType);
|
||||
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
|
||||
if (Base64.isBase64(config.getKeyContent())) {
|
||||
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
|
||||
}
|
||||
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||
}
|
||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
||||
}
|
||||
|
||||
// 创建 client 客户端
|
||||
client = new WxPayServiceImpl();
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doUnifiedOrderV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【V2】调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* 【V3】调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws WxPayException;
|
||||
|
||||
/**
|
||||
* 【V2】创建微信下单请求
|
||||
*
|
||||
* @param reqDTO 下信息
|
||||
* @return 下单请求
|
||||
*/
|
||||
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
|
||||
return WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 【V3】创建微信下单请求
|
||||
*
|
||||
* @param reqDTO 下信息
|
||||
* @return 下单请求
|
||||
*/
|
||||
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
request.setDescription(reqDTO.getSubject());
|
||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
|
||||
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doParseOrderNotifyV2(body);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doParseOrderNotifyV3(body);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
|
||||
// 2. 构建结果
|
||||
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
|
||||
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
|
||||
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
response.getOutTradeNo(), body);
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
|
||||
WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||
// 2. 构建结果
|
||||
Integer status = parseStatus(result.getTradeState());
|
||||
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
|
||||
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
|
||||
result.getOutTradeNo(), body);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doGetOrderV2(outTradeNo);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doGetOrderV3(outTradeNo);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||
outTradeNo, e.getXmlString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
|
||||
.outTradeNo(outTradeNo).build();
|
||||
// 执行请求
|
||||
WxPayOrderQueryResult response = client.queryOrder(request);
|
||||
|
||||
// 转换结果
|
||||
Integer status = parseStatus(response.getTradeState());
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
|
||||
.setOutTradeNo(outTradeNo);
|
||||
// 执行请求
|
||||
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
|
||||
|
||||
// 转换结果
|
||||
Integer status = parseStatus(response.getTradeState());
|
||||
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
|
||||
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
|
||||
outTradeNo, response);
|
||||
}
|
||||
|
||||
private static Integer parseStatus(String tradeState) {
|
||||
switch (tradeState) {
|
||||
case "NOTPAY":
|
||||
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
|
||||
return PayOrderStatusRespEnum.WAITING.getStatus();
|
||||
case "SUCCESS":
|
||||
return PayOrderStatusRespEnum.SUCCESS.getStatus();
|
||||
case "REFUND":
|
||||
return PayOrderStatusRespEnum.REFUND.getStatus();
|
||||
case "CLOSED":
|
||||
case "REVOKED": // 已撤销(刷卡支付独有)
|
||||
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
|
||||
return PayOrderStatusRespEnum.CLOSED.getStatus();
|
||||
default:
|
||||
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 退款相关 ==========
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedRefundV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doUnifiedRefundV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||
reqDTO.getOutTradeNo(), e.getXmlString());
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundRequest request = new WxPayRefundRequest()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setRefundFee(reqDTO.getRefundPrice())
|
||||
.setRefundDesc(reqDTO.getReason())
|
||||
.setTotalFee(reqDTO.getPayPrice())
|
||||
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 2.1 执行请求
|
||||
WxPayRefundResult response = client.refundV2(request);
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundV3Request request = new WxPayRefundV3Request()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
|
||||
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
|
||||
.setReason(reqDTO.getReason())
|
||||
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
// 2.1 执行请求
|
||||
WxPayRefundV3Result response = client.refundV3(request);
|
||||
// 2.2 创建返回结果
|
||||
if (Objects.equals("SUCCESS", response.getStatus())) {
|
||||
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
if (Objects.equals("PROCESSING", response.getStatus())) {
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doParseRefundNotifyV2(body);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return parseRefundNotifyV3(body);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
|
||||
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
|
||||
// 2. 构建结果
|
||||
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
|
||||
result.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
|
||||
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||
// 2. 构建结果
|
||||
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
|
||||
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
|
||||
result.getOutRefundNo(), response);
|
||||
}
|
||||
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V2:
|
||||
return doGetRefundV2(outTradeNo, outRefundNo);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doGetRefundV3(outTradeNo, outRefundNo);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
|
||||
outRefundNo, e.getXmlString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
|
||||
.outTradeNo(outTradeNo)
|
||||
.outRefundNo(outRefundNo)
|
||||
.build();
|
||||
// 2.1 执行请求
|
||||
WxPayRefundQueryResult response = client.refundQuery(request);
|
||||
// 2.2 创建返回结果
|
||||
if (!Objects.equals("SUCCESS", response.getResultCode())) {
|
||||
return PayRefundRespDTO.waitingOf(null,
|
||||
outRefundNo, response);
|
||||
}
|
||||
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
|
||||
record -> record.getOutRefundNo().equals(outRefundNo));
|
||||
if (refund == null) {
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
}
|
||||
switch (refund.getRefundStatus()) {
|
||||
case "SUCCESS":
|
||||
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
|
||||
outRefundNo, response);
|
||||
case "PROCESSING":
|
||||
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
|
||||
outRefundNo, response);
|
||||
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
|
||||
case "FAIL":
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
|
||||
request.setOutRefundNo(outRefundNo);
|
||||
// 2.1 执行请求
|
||||
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
|
||||
// 2.2 创建返回结果
|
||||
switch (response.getStatus()) {
|
||||
case "SUCCESS":
|
||||
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
|
||||
outRefundNo, response);
|
||||
case "PROCESSING":
|
||||
return PayRefundRespDTO.waitingOf(response.getRefundId(),
|
||||
outRefundNo, response);
|
||||
case "ABNORMAL": // 退款异常
|
||||
case "CLOSED":
|
||||
return PayRefundRespDTO.failureOf(outRefundNo, response);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String formatDateV2(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV2(String time) {
|
||||
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV2B(String time) {
|
||||
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static String formatDateV3(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV3(String time) {
|
||||
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
static String getErrorCode(WxPayException e) {
|
||||
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||
return e.getErrCode();
|
||||
}
|
||||
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||
return "CUSTOM_ERROR";
|
||||
}
|
||||
return e.getReturnCode();
|
||||
}
|
||||
|
||||
static String getErrorMessage(WxPayException e) {
|
||||
if (StrUtil.isNotEmpty(e.getErrCode())) {
|
||||
return e.getErrCodeDes();
|
||||
}
|
||||
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
|
||||
return e.getCustomErrorMsg();
|
||||
}
|
||||
return e.getReturnMsg();
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信支付【App 支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
|
||||
*
|
||||
* // TODO 芋艿:未详细测试,因为手头没 App
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxAppPayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxAppPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_APP.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.APP);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||
// 执行请求
|
||||
WxPayMpOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||
// 执行请求
|
||||
WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信支付【付款码支付】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxBarPayClient extends AbstractWxPayClient {
|
||||
|
||||
/**
|
||||
* 微信付款码的过期时间
|
||||
*/
|
||||
private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
|
||||
|
||||
public WxBarPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.MICROPAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
|
||||
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
|
||||
if (expireTime.isAfter(reqDTO.getExpireTime())) {
|
||||
expireTime = reqDTO.getExpireTime();
|
||||
}
|
||||
// 构建 WxPayMicropayRequest 对象
|
||||
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(expireTime))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.authCode(getAuthCode(reqDTO))
|
||||
.build();
|
||||
// 执行请求,重试直到失败(过期),或者成功
|
||||
WxPayException lastWxPayException = null;
|
||||
for (int i = 1; i < Byte.MAX_VALUE; i++) {
|
||||
try {
|
||||
WxPayMicropayResult response = client.micropay(request);
|
||||
// 支付成功,例如说:1)用户输入了密码;2)用户免密支付
|
||||
return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
response.getOutTradeNo(), response)
|
||||
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
|
||||
} catch (WxPayException ex) {
|
||||
lastWxPayException = ex;
|
||||
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
|
||||
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
|
||||
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||
// 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||
if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
|
||||
throw ex;
|
||||
}
|
||||
// 等待 5 秒,继续下一轮重新发起支付
|
||||
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
|
||||
toJsonString(request), ex.getMessage());
|
||||
ThreadUtil.sleep(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
throw lastWxPayException;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
|
||||
if (StrUtil.isEmpty(authCode)) {
|
||||
throw invalidParamException("支付请求的 authCode 不能为空!");
|
||||
}
|
||||
return authCode;
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 微信支付【小程序】的 PayClient 实现类
|
||||
*
|
||||
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
|
||||
*
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxLitePayClient extends WxPubPayClient {
|
||||
|
||||
public WxLitePayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
|
||||
}
|
||||
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 微信支付【Native 二维码】的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
|
||||
*
|
||||
* @author zwy
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxNativePayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxNativePayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.NATIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
|
||||
// 执行请求
|
||||
WxPayNativeOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderV3Request 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
|
||||
// 执行请求
|
||||
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 微信支付的 PayClientConfig 实现类
|
||||
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class WxPayClientConfig implements PayClientConfig {
|
||||
|
||||
/**
|
||||
* API 版本 - V2
|
||||
*
|
||||
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
|
||||
*/
|
||||
public static final String API_VERSION_V2 = "v2";
|
||||
/**
|
||||
* API 版本 - V3
|
||||
*
|
||||
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
|
||||
*/
|
||||
public static final String API_VERSION_V3 = "v3";
|
||||
|
||||
/**
|
||||
* 公众号或者小程序的 appid
|
||||
*
|
||||
* 只有公众号或小程序需要该字段
|
||||
*/
|
||||
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
|
||||
private String appId;
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
|
||||
private String mchId;
|
||||
/**
|
||||
* API 版本
|
||||
*/
|
||||
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
|
||||
private String apiVersion;
|
||||
|
||||
// ========== V2 版本的参数 ==========
|
||||
|
||||
/**
|
||||
* 商户密钥
|
||||
*/
|
||||
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
|
||||
private String mchKey;
|
||||
/**
|
||||
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
|
||||
*
|
||||
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
|
||||
*/
|
||||
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
|
||||
private String keyContent;
|
||||
|
||||
// ========== V3 版本的参数 ==========
|
||||
/**
|
||||
* apiclient_key.pem 证书文件的对应字符串
|
||||
*/
|
||||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
||||
private String privateKeyContent;
|
||||
/**
|
||||
* apiclient_cert.pem 证书文件的对应的字符串
|
||||
*/
|
||||
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
|
||||
private String privateCertContent;
|
||||
/**
|
||||
* apiV3 密钥值
|
||||
*/
|
||||
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
|
||||
private String apiV3Key;
|
||||
|
||||
/**
|
||||
* 分组校验 v2版本
|
||||
*/
|
||||
public interface V2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组校验 v3版本
|
||||
*/
|
||||
public interface V3 {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Validator validator) {
|
||||
ValidationUtils.validate(validator, this,
|
||||
API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws FileNotFoundException {
|
||||
String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
|
||||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
|
||||
/// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
|
||||
System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
|
||||
}
|
||||
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信支付(公众号)的 PayClient 实现类
|
||||
*
|
||||
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class WxPubPayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
}
|
||||
|
||||
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
super.doInit(WxPayConstants.TradeType.JSAPI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
|
||||
.setOpenid(getOpenid(reqDTO));
|
||||
// 执行请求
|
||||
WxPayMpOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
|
||||
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||
// 执行请求
|
||||
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||
|
||||
// 转换结果
|
||||
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
|
||||
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
|
||||
if (StrUtil.isEmpty(openid)) {
|
||||
throw invalidParamException("支付请求的 openid 不能为空!");
|
||||
}
|
||||
return openid;
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.channel;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付渠道的编码的枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayChannelEnum {
|
||||
|
||||
WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页
|
||||
WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class),
|
||||
WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class),
|
||||
WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class),
|
||||
WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class),
|
||||
|
||||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class),
|
||||
MOCK("mock", "模拟支付", NonePayClientConfig.class),
|
||||
|
||||
WALLET("wallet", "钱包支付", NonePayClientConfig.class);
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*
|
||||
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
|
||||
*/
|
||||
private final String code;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 配置类
|
||||
*/
|
||||
private final Class<? extends PayClientConfig> configClass;
|
||||
|
||||
/**
|
||||
* 微信支付
|
||||
*/
|
||||
public static final String WECHAT = "WECHAT";
|
||||
|
||||
/**
|
||||
* 支付宝支付
|
||||
*/
|
||||
public static final String ALIPAY = "ALIPAY";
|
||||
|
||||
public static PayChannelEnum getByCode(String code) {
|
||||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
|
||||
}
|
||||
|
||||
public static boolean isAlipay(String channelCode) {
|
||||
return channelCode != null && channelCode.startsWith("alipay");
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.order;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付 UI 展示模式
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayOrderDisplayModeEnum {
|
||||
|
||||
URL("url"), // Redirect 跳转链接的方式
|
||||
IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
|
||||
FORM("form"), // HTML 表单提交
|
||||
QR_CODE("qr_code"), // 二维码的文字内容
|
||||
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
|
||||
BAR_CODE("bar_code"), // 条形码
|
||||
APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
|
||||
;
|
||||
|
||||
/**
|
||||
* 展示模式
|
||||
*/
|
||||
private final String mode;
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.order;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的支付状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayOrderStatusRespEnum {
|
||||
|
||||
WAITING(0, "未支付"),
|
||||
SUCCESS(10, "支付成功"),
|
||||
REFUND(20, "已退款"),
|
||||
CLOSED(30, "支付关闭"),
|
||||
;
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 判断是否支付成功
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否已退款
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付成功
|
||||
*/
|
||||
public static boolean isRefund(Integer status) {
|
||||
return Objects.equals(status, REFUND.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否支付关闭
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付关闭
|
||||
*/
|
||||
public static boolean isClosed(Integer status) {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.refund;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的退款状态枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayRefundStatusRespEnum {
|
||||
|
||||
WAITING(0, "等待退款"),
|
||||
SUCCESS(10, "退款成功"),
|
||||
FAILURE(20, "退款失败");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isFailure(Integer status) {
|
||||
return Objects.equals(status, FAILURE.getStatus());
|
||||
}
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的转账状态枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayTransferStatusRespEnum {
|
||||
|
||||
WAITING(0, "等待转账"),
|
||||
|
||||
/**
|
||||
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
|
||||
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
|
||||
*/
|
||||
IN_PROGRESS(10, "转账进行中"),
|
||||
|
||||
SUCCESS(20, "转账成功"),
|
||||
/**
|
||||
* 转账关闭 (失败,或者其它情况)
|
||||
*/
|
||||
CLOSED(30, "转账关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
public static boolean isSuccess(Integer status) {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isClosed(Integer status) {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isInProgress(Integer status) {
|
||||
return Objects.equals(status, IN_PROGRESS.getStatus());
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 转账类型枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum PayTransferTypeEnum implements IntArrayValuable {
|
||||
|
||||
ALIPAY_BALANCE(1, "支付宝余额"),
|
||||
WX_BALANCE(2, "微信余额"),
|
||||
BANK_CARD(3, "银行卡"),
|
||||
WALLET_BALANCE(4, "钱包余额");
|
||||
|
||||
public interface WxPay {
|
||||
}
|
||||
|
||||
public interface Alipay {
|
||||
}
|
||||
|
||||
private final Integer type;
|
||||
private final String name;
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayTransferTypeEnum::getType).toArray();
|
||||
|
||||
@Override
|
||||
public int[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static PayTransferTypeEnum typeOf(Integer type) {
|
||||
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
|
||||
}
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration
|
@ -1,133 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPubPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* {@link PayClientFactoryImpl} 的集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Disabled
|
||||
public class PayClientFactoryImplIntegrationTest {
|
||||
|
||||
private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
|
||||
|
||||
private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl();
|
||||
|
||||
/**
|
||||
* {@link WxPubPayClient} 的 V2 版本
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_WX_PUB_V2() {
|
||||
// 创建配置
|
||||
WxPayClientConfig config = new WxPayClientConfig();
|
||||
config.setAppId("wx041349c6f39b268b");
|
||||
config.setMchId("1545083881");
|
||||
config.setApiVersion(WxPayClientConfig.API_VERSION_V2);
|
||||
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
// System.out.println(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link WxPubPayClient} 的 V3 版本
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException {
|
||||
// 创建配置
|
||||
WxPayClientConfig config = new WxPayClientConfig();
|
||||
config.setAppId("wx041349c6f39b268b");
|
||||
config.setMchId("1545083881");
|
||||
config.setApiVersion(WxPayClientConfig.API_VERSION_V3);
|
||||
config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
|
||||
config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
|
||||
config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
// System.out.println(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AlipayQrPayClient}
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testCreatePayClient_ALIPAY_QR() {
|
||||
// 创建配置
|
||||
AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
config.setAppId("2021000118634035");
|
||||
config.setServerUrl(SERVER_URL_SANDBOX);
|
||||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
|
||||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址
|
||||
// CommonResult<AlipayTradePrecreateResponse> result = (CommonResult<AlipayTradePrecreateResponse>) client.unifiedOrder(reqDTO);
|
||||
// System.out.println(JsonUtils.toJsonString(result));
|
||||
// System.out.println(result.getData().getQrCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AlipayWapPayClient}
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePayClient_ALIPAY_WAP() {
|
||||
// 创建配置
|
||||
AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
config.setAppId("2021000118634035");
|
||||
config.setServerUrl(SERVER_URL_SANDBOX);
|
||||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
|
||||
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
|
||||
// 创建客户端
|
||||
Long channelId = RandomUtil.randomLong();
|
||||
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
|
||||
PayClient client = payClientFactory.getPayClient(channelId);
|
||||
// 发起支付
|
||||
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
|
||||
// CommonResult<?> result = client.unifiedOrder(reqDTO);
|
||||
// System.out.println(JsonUtils.toJsonString(result));
|
||||
}
|
||||
|
||||
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
|
||||
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
|
||||
reqDTO.setPrice(123);
|
||||
reqDTO.setSubject("IPhone 13");
|
||||
reqDTO.setBody("biubiubiu");
|
||||
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setUserIp("127.0.0.1");
|
||||
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
|
||||
return reqDTO;
|
||||
}
|
||||
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.DefaultSigner;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import lombok.Setter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import java.util.Date;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 支付宝 Client 的测试基类
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest {
|
||||
|
||||
protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> {
|
||||
o.setServerUrl(randomURL());
|
||||
o.setPrivateKey(randomString());
|
||||
o.setMode(MODE_PUBLIC_KEY);
|
||||
o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
o.setAppCertContent("");
|
||||
o.setAlipayPublicCertContent("");
|
||||
o.setRootCertContent("");
|
||||
});
|
||||
|
||||
@Mock
|
||||
protected DefaultAlipayClient defaultAlipayClient;
|
||||
|
||||
@Setter
|
||||
private AbstractAlipayPayClient client;
|
||||
|
||||
/**
|
||||
* 子类需要实现该方法. 设置 client 的具体实现
|
||||
*/
|
||||
@BeforeEach
|
||||
public abstract void setUp();
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 初始化")
|
||||
public void testDoInit() {
|
||||
// 调用
|
||||
client.doInit();
|
||||
// 断言
|
||||
DefaultAlipayClient realClient = client.getClient();
|
||||
assertNotSame(defaultAlipayClient, realClient);
|
||||
assertInstanceOf(DefaultSigner.class, realClient.getSigner());
|
||||
assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一退款:成功")
|
||||
public void testUnifiedRefund_success() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
Date refundTime = randomDate();
|
||||
String outRefundNo = randomString();
|
||||
String outTradeNo = randomString();
|
||||
Integer refundAmount = randomInteger();
|
||||
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
|
||||
o.setSubCode("");
|
||||
o.setGmtRefundPay(refundTime);
|
||||
});
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
|
||||
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
|
||||
AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel();
|
||||
assertEquals(outRefundNo, bizModel.getOutRequestNo());
|
||||
assertEquals(outTradeNo, bizModel.getOutTradeNo());
|
||||
assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
|
||||
o.setOutRefundNo(outRefundNo);
|
||||
o.setOutTradeNo(outTradeNo);
|
||||
o.setNotifyUrl(notifyUrl);
|
||||
o.setRefundPrice(refundAmount);
|
||||
});
|
||||
|
||||
// 调用
|
||||
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
|
||||
// 断言
|
||||
assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus());
|
||||
assertEquals(outRefundNo, resp.getOutRefundNo());
|
||||
assertNull(resp.getChannelRefundNo());
|
||||
assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一退款:渠道返回失败")
|
||||
public void test_unified_refund_channel_failed() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
String subCode = randomString();
|
||||
String subMsg = randomString();
|
||||
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
|
||||
o.setSubCode(subCode);
|
||||
o.setSubMsg(subMsg);
|
||||
});
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
|
||||
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outRefundNo = randomString();
|
||||
String outTradeNo = randomString();
|
||||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
|
||||
o.setOutRefundNo(outRefundNo);
|
||||
o.setOutTradeNo(outTradeNo);
|
||||
o.setNotifyUrl(notifyUrl);
|
||||
});
|
||||
|
||||
// 调用
|
||||
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
|
||||
// 断言
|
||||
assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus());
|
||||
assertEquals(outRefundNo, resp.getOutRefundNo());
|
||||
assertNull(resp.getChannelRefundNo());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertEquals(subCode, resp.getChannelErrorCode());
|
||||
assertEquals(subMsg, resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一退款:参数校验不通过")
|
||||
public void testUnifiedRefund_paramInvalidate() {
|
||||
// 准备请求参数
|
||||
String notifyUrl = randomURL();
|
||||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
|
||||
o.setOutTradeNo("");
|
||||
o.setNotifyUrl(notifyUrl);
|
||||
});
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一退款:抛出业务异常")
|
||||
public void testUnifiedRefund_throwServiceException() throws AlipayApiException {
|
||||
// mock 方法
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
|
||||
.thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
|
||||
// 准备请求参数
|
||||
String notifyUrl = randomURL();
|
||||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一退款:抛出系统异常")
|
||||
public void testUnifiedRefund_throwPayException() throws AlipayApiException {
|
||||
// mock 方法
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
|
||||
.thenThrow(new RuntimeException("系统异常"));
|
||||
// 准备请求参数
|
||||
String notifyUrl = randomURL();
|
||||
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一下单:参数校验不通过")
|
||||
public void testUnifiedOrder_paramInvalidate() {
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
String notifyUrl = randomURL();
|
||||
PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> {
|
||||
o.setOutTradeNo(outTradeNo);
|
||||
o.setNotifyUrl(notifyUrl);
|
||||
});
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO));
|
||||
}
|
||||
|
||||
protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) {
|
||||
return randomPojo(PayOrderUnifiedReqDTO.class, o -> {
|
||||
o.setOutTradeNo(outTradeNo);
|
||||
o.setNotifyUrl(notifyUrl);
|
||||
o.setPrice(price);
|
||||
o.setSubject(RandomUtil.randomString(32));
|
||||
o.setBody(RandomUtil.randomString(32));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePayModel;
|
||||
import com.alipay.api.request.AlipayTradePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePayResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED;
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link AlipayBarPayClient} 单元测试
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class AlipayBarPayClientTest extends AbstractAlipayClientTest {
|
||||
|
||||
@InjectMocks
|
||||
private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config);
|
||||
|
||||
@Override
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
setClient(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝条码支付:非免密码支付下单成功")
|
||||
public void testUnifiedOrder_success() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String outTradeNo = randomString();
|
||||
String notifyUrl = randomURL();
|
||||
Integer price = randomInteger();
|
||||
String authCode = randomString();
|
||||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode(""));
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> {
|
||||
assertInstanceOf(AlipayTradePayModel.class, request.getBizModel());
|
||||
assertEquals(notifyUrl, request.getNotifyUrl());
|
||||
AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel();
|
||||
assertEquals(outTradeNo, model.getOutTradeNo());
|
||||
assertEquals(String.valueOf(price / 100.0), model.getTotalAmount());
|
||||
assertEquals(authCode, model.getAuthCode());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
Map<String, String> extraParam = new HashMap<>();
|
||||
extraParam.put("auth_code", authCode);
|
||||
reqDTO.setChannelExtras(extraParam);
|
||||
|
||||
// 调用方法
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(WAITING.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode());
|
||||
assertEquals("", resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝条码支付:免密码支付下单成功")
|
||||
public void testUnifiedOrder_code10000Success() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String outTradeNo = randomString();
|
||||
String channelNo = randomString();
|
||||
String channelUserId = randomString();
|
||||
Date payTime = randomDate();
|
||||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> {
|
||||
o.setSubCode("");
|
||||
o.setCode("10000");
|
||||
o.setOutTradeNo(outTradeNo);
|
||||
o.setTradeNo(channelNo);
|
||||
o.setBuyerUserId(channelUserId);
|
||||
o.setGmtPayment(payTime);
|
||||
});
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true)))
|
||||
.thenReturn(response);
|
||||
// 准备请求参数
|
||||
String authCode = randomString();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
|
||||
Map<String, String> extraParam = new HashMap<>();
|
||||
extraParam.put("auth_code", authCode);
|
||||
reqDTO.setChannelExtras(extraParam);
|
||||
|
||||
// 下单请求
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(PayOrderStatusRespEnum.SUCCESS.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertEquals(channelNo, resp.getChannelOrderNo());
|
||||
assertEquals(channelUserId, resp.getChannelUserId());
|
||||
assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime());
|
||||
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode());
|
||||
assertEquals("", resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝条码支付:没有传条码")
|
||||
public void testUnifiedOrder_emptyAuthCode() {
|
||||
// 准备参数
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝条码支付:渠道返回失败")
|
||||
public void test_unified_order_channel_failed() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String subCode = randomString();
|
||||
String subMsg = randomString();
|
||||
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> {
|
||||
o.setSubCode(subCode);
|
||||
o.setSubMsg(subMsg);
|
||||
});
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true)))
|
||||
.thenReturn(response);
|
||||
// 准备请求参数
|
||||
String authCode = randomString();
|
||||
String outTradeNo = randomString();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
|
||||
Map<String, String> extraParam = new HashMap<>();
|
||||
extraParam.put("auth_code", authCode);
|
||||
reqDTO.setChannelExtras(extraParam);
|
||||
|
||||
// 调用方法
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(CLOSED.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertNull(resp.getDisplayMode());
|
||||
assertNull(resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertEquals(subCode, resp.getChannelErrorCode());
|
||||
assertEquals(subMsg, resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.http.Method;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED;
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link AlipayPcPayClient} 单元测试
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class AlipayPcPayClientTest extends AbstractAlipayClientTest {
|
||||
|
||||
@InjectMocks
|
||||
private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config);
|
||||
|
||||
@Override
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
setClient(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 PC 网站支付:URL Display Mode 下单成功")
|
||||
public void testUnifiedOrder_urlSuccess() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode(""));
|
||||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
|
||||
eq(Method.GET.name()))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
Integer price = randomInteger();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
reqDTO.setDisplayMode(null);
|
||||
|
||||
// 调用
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(WAITING.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode());
|
||||
assertEquals(response.getBody(), resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 PC 网站支付:Form Display Mode 下单成功")
|
||||
public void testUnifiedOrder_formSuccess() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode(""));
|
||||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
|
||||
eq(Method.POST.name()))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
Integer price = randomInteger();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode());
|
||||
|
||||
// 调用
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(WAITING.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode());
|
||||
assertEquals(response.getBody(), resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 PC 网站支付:渠道返回失败")
|
||||
public void testUnifiedOrder_channelFailed() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String subCode = randomString();
|
||||
String subMsg = randomString();
|
||||
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> {
|
||||
o.setSubCode(subCode);
|
||||
o.setSubMsg(subMsg);
|
||||
});
|
||||
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
|
||||
eq(Method.GET.name()))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
|
||||
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode());
|
||||
|
||||
// 调用
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(CLOSED.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertNull(resp.getDisplayMode());
|
||||
assertNull(resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertEquals(subCode, resp.getChannelErrorCode());
|
||||
assertEquals(subMsg, resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED;
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link AlipayQrPayClient} 单元测试
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class AlipayQrPayClientTest extends AbstractAlipayClientTest {
|
||||
|
||||
@InjectMocks
|
||||
private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
setClient(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝扫描支付:下单成功")
|
||||
public void testUnifiedOrder_success() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
String qrCode = randomString();
|
||||
Integer price = randomInteger();
|
||||
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> {
|
||||
o.setQrCode(qrCode);
|
||||
o.setSubCode("");
|
||||
});
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
|
||||
assertEquals(notifyUrl, request.getNotifyUrl());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
|
||||
// 调用
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(WAITING.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode());
|
||||
assertEquals(response.getQrCode(), resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertNull(resp.getChannelErrorCode());
|
||||
assertNull(resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝扫描支付:渠道返回失败")
|
||||
public void testUnifiedOrder_channelFailed() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String notifyUrl = randomURL();
|
||||
String subCode = randomString();
|
||||
String subMsg = randomString();
|
||||
Integer price = randomInteger();
|
||||
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> {
|
||||
o.setSubCode(subCode);
|
||||
o.setSubMsg(subMsg);
|
||||
});
|
||||
// mock
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
|
||||
assertEquals(notifyUrl, request.getNotifyUrl());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
// 准备请求参数
|
||||
String outTradeNo = randomString();
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
|
||||
// 调用
|
||||
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
|
||||
// 断言
|
||||
assertEquals(CLOSED.getStatus(), resp.getStatus());
|
||||
assertEquals(outTradeNo, resp.getOutTradeNo());
|
||||
assertNull(resp.getChannelOrderNo());
|
||||
assertNull(resp.getChannelUserId());
|
||||
assertNull(resp.getSuccessTime());
|
||||
assertNull(resp.getDisplayMode());
|
||||
assertNull(resp.getDisplayContent());
|
||||
assertSame(response, resp.getRawData());
|
||||
assertEquals(subCode, resp.getChannelErrorCode());
|
||||
assertEquals(subMsg, resp.getChannelErrorMsg());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝扫描支付, 抛出系统异常")
|
||||
public void testUnifiedOrder_throwPayException() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String outTradeNo = randomString();
|
||||
String notifyUrl = randomURL();
|
||||
Integer price = randomInteger();
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
|
||||
assertEquals(notifyUrl, request.getNotifyUrl());
|
||||
return true;
|
||||
}))).thenThrow(new RuntimeException("系统异常"));
|
||||
// 准备请求参数
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price);
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(PayException.class, () -> client.unifiedOrder(reqDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("支付宝 Client 统一下单:抛出业务异常")
|
||||
public void testUnifiedOrder_throwServiceException() throws AlipayApiException {
|
||||
// mock 方法
|
||||
String outTradeNo = randomString();
|
||||
String notifyUrl = randomURL();
|
||||
Integer price = randomInteger();
|
||||
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
|
||||
assertEquals(notifyUrl, request.getNotifyUrl());
|
||||
return true;
|
||||
}))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
|
||||
// 准备请求参数
|
||||
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
|
||||
|
||||
// 调用,并断言
|
||||
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO));
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user