Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 840e24dc3e | |||
| b5f8523554 | |||
| b291dacf3d | |||
| 8cb7bc6ae8 | |||
| 91b005975a | |||
| 2ffd1aa7d6 |
+4
-9
@@ -1,26 +1,21 @@
|
||||
package cn.novalon.gym.manage.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 手机号一键登录请求DTO
|
||||
* uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PhoneLoginDto {
|
||||
|
||||
@NotBlank(message = "access_token不能为空")
|
||||
private String accessToken;
|
||||
|
||||
@NotBlank(message = "openid不能为空")
|
||||
private String openid;
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
private String nickname;
|
||||
|
||||
|
||||
+2
-3
@@ -2,7 +2,6 @@ package cn.novalon.gym.manage.auth.handler;
|
||||
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||
import cn.novalon.gym.manage.auth.dto.SendCodeRequest;
|
||||
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -39,8 +38,8 @@ public class PhoneAuthHandler {
|
||||
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
|
||||
log.info("收到发送短信验证码请求");
|
||||
|
||||
return request.bodyToMono(SendCodeRequest.class)
|
||||
.flatMap(req -> phoneAuthService.sendSmsCode(req.getPhone()))
|
||||
return request.bodyToMono(PhoneLoginDto.class)
|
||||
.flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone()))
|
||||
.flatMap(success -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败")));
|
||||
|
||||
+12
-64
@@ -1,6 +1,5 @@
|
||||
package cn.novalon.gym.manage.auth.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.auth.config.SmsProperties;
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||
@@ -16,13 +15,6 @@ import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileRequest;
|
||||
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileResponse;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.exceptions.ServerException;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -43,7 +35,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final SmsService smsService;
|
||||
private final SmsProperties smsProperties;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
@@ -54,63 +45,19 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
||||
|
||||
@Override
|
||||
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
|
||||
log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid());
|
||||
log.info("手机号一键登录, phone: {}", request.getPhone());
|
||||
|
||||
return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid()))
|
||||
.flatMap(phone -> {
|
||||
log.info("通过access_token获取手机号成功: {}", maskPhone(phone));
|
||||
String encryptedPhone = encryptPhone(phone);
|
||||
String encryptedPhone = encryptPhone(request.getPhone());
|
||||
|
||||
return memberRepository.findByPhone(encryptedPhone)
|
||||
.flatMap(existingMember -> {
|
||||
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
|
||||
return doLogin(existingMember, false, request);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("手机号未注册,创建新会员");
|
||||
return createNewMemberAndLogin(request, encryptedPhone);
|
||||
}));
|
||||
return memberRepository.findByPhone(encryptedPhone)
|
||||
.flatMap(existingMember -> {
|
||||
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
|
||||
return doLogin(existingMember, false, request);
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("一键登录失败", e);
|
||||
return Mono.error(new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + e.getMessage()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过access_token和openid获取手机号
|
||||
* 使用阿里云DYPNS API
|
||||
*/
|
||||
private String getPhoneByToken(String accessToken, String openid) throws Exception {
|
||||
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
|
||||
IAcsClient client = new DefaultAcsClient(profile);
|
||||
GetMobileRequest request = new GetMobileRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
|
||||
try {
|
||||
GetMobileResponse response = client.getAcsResponse(request);
|
||||
if ("OK".equals(response.getCode())) {
|
||||
return response.getGetMobileResultDTO().getMobile();
|
||||
}
|
||||
log.warn("DYPNS API返回错误: code={}, message={}", response.getCode(), response.getMessage());
|
||||
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + response.getMessage());
|
||||
} catch (ServerException e) {
|
||||
log.error("阿里云DYPNS API服务端异常", e);
|
||||
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败");
|
||||
} catch (ClientException e) {
|
||||
log.error("阿里云DYPNS API客户端异常", e);
|
||||
throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号脱敏显示
|
||||
*/
|
||||
private String maskPhone(String phone) {
|
||||
if (phone == null || phone.length() < 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("手机号未注册,创建新会员");
|
||||
return createNewMemberAndLogin(request, encryptedPhone);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,6 +87,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("手机号未注册,创建新会员");
|
||||
PhoneLoginDto registerRequest = new PhoneLoginDto();
|
||||
registerRequest.setPhone(request.getPhone());
|
||||
return createNewMemberAndLogin(registerRequest, encryptedPhone);
|
||||
}));
|
||||
});
|
||||
@@ -205,7 +153,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
||||
vo.setNeedCompleteInfo(needCompleteInfo);
|
||||
vo.setNickname(member.getNickname());
|
||||
vo.setAvatar(member.getAvatar());
|
||||
vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null);
|
||||
vo.setPhone(decryptPhone(member.getPhone()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,11 +98,12 @@
|
||||
<artifactId>javase</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云OSS SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>3.17.4</version>
|
||||
<version>3.17.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-payment</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Gym Payment</name>
|
||||
<description>支付模块 - 支付宝App支付</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot WebFlux -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用模块 (含Redis工具类) -->
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付宝SDK -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.120.ALL</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON处理 -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.2.19</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package cn.novalon.gym.manage.payment.handler;
|
||||
|
||||
import cn.novalon.gym.manage.payment.dto.ApiResponse;
|
||||
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
|
||||
import cn.novalon.gym.manage.payment.dto.PaymentResponse;
|
||||
import cn.novalon.gym.manage.payment.service.PaymentService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付处理器
|
||||
* 提供支付宝App支付相关接口
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Tag(name = "支付管理", description = "支付宝App支付接口")
|
||||
public class PaymentHandler {
|
||||
|
||||
private final PaymentService paymentService;
|
||||
|
||||
public PaymentHandler(PaymentService paymentService) {
|
||||
this.paymentService = paymentService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建支付订单", description = "创建支付宝App支付订单")
|
||||
public Mono<ServerResponse> createPayment(ServerRequest request) {
|
||||
return request.bodyToMono(CreatePaymentRequest.class)
|
||||
.flatMap(createReq -> {
|
||||
log.info("[Payment] 创建支付订单: memberId={}, orderType={}, goodsDesc={}, transAmt={}",
|
||||
createReq.getMemberId(), createReq.getOrderType(), createReq.getGoodsDesc(), createReq.getTransAmt());
|
||||
|
||||
PaymentResponse result = paymentService.alipayAppPay(createReq);
|
||||
|
||||
if ("FAIL".equals(result.getStatus())) {
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error(result.getErrorMsg()));
|
||||
}
|
||||
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.success(result));
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("[Payment] 创建支付订单异常", e);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error("创建支付订单失败: " + e.getMessage()));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "创建扫码支付订单", description = "创建支付宝扫码支付订单,返回二维码链接")
|
||||
public Mono<ServerResponse> createQrCodePayment(ServerRequest request) {
|
||||
return request.bodyToMono(CreatePaymentRequest.class)
|
||||
.flatMap(createReq -> {
|
||||
log.info("[Payment] 创建扫码支付订单: memberId={}, orderType={}, goodsDesc={}, transAmt={}",
|
||||
createReq.getMemberId(), createReq.getOrderType(), createReq.getGoodsDesc(), createReq.getTransAmt());
|
||||
|
||||
PaymentResponse result = paymentService.alipayQrCodePay(createReq);
|
||||
|
||||
if ("FAIL".equals(result.getStatus())) {
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error(result.getErrorMsg()));
|
||||
}
|
||||
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.success(result));
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("[Payment] 创建扫码支付订单异常", e);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error("创建扫码支付订单失败: " + e.getMessage()));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "查询支付状态", description = "查询支付订单状态")
|
||||
public Mono<ServerResponse> getPaymentStatus(ServerRequest request) {
|
||||
String orderId = request.pathVariable("orderId");
|
||||
log.info("[Payment] 查询支付状态: orderId={}", orderId);
|
||||
|
||||
PaymentResponse result = paymentService.getPaymentStatus(orderId);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
@Operation(summary = "支付宝异步通知", description = "接收支付宝异步回调通知")
|
||||
public Mono<ServerResponse> alipayNotify(ServerRequest request) {
|
||||
return request.bodyToMono(Map.class)
|
||||
.flatMap(params -> {
|
||||
log.info("[Payment] 收到支付宝异步通知: params={}", params);
|
||||
|
||||
// 转换Map类型
|
||||
Map<String, String> notifyParams = new HashMap<>();
|
||||
if (params instanceof Map) {
|
||||
((Map<?, ?>) params).forEach((key, value) ->
|
||||
notifyParams.put(String.valueOf(key), String.valueOf(value)));
|
||||
}
|
||||
|
||||
String result = paymentService.handleAlipayNotify(notifyParams);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(result);
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("[Payment] 异步通知处理异常", e);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue("fail");
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "申请退款", description = "申请支付退款")
|
||||
public Mono<ServerResponse> refundPayment(ServerRequest request) {
|
||||
String orderId = request.pathVariable("orderId");
|
||||
|
||||
return request.bodyToMono(Map.class)
|
||||
.flatMap(body -> {
|
||||
String refundAmt = String.valueOf(body.get("refundAmt"));
|
||||
log.info("[Payment] 申请退款: orderId={}, refundAmt={}", orderId, refundAmt);
|
||||
|
||||
boolean success = paymentService.refund(orderId, refundAmt);
|
||||
if (success) {
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.success("退款申请成功"));
|
||||
} else {
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error("退款申请失败"));
|
||||
}
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("[Payment] 退款异常: orderId={}", orderId, e);
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(ApiResponse.error("退款失败: " + e.getMessage()));
|
||||
});
|
||||
}
|
||||
}
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
package cn.novalon.gym.manage.payment.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.payment.config.AlipayProperties;
|
||||
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
|
||||
import cn.novalon.gym.manage.payment.dto.PaymentResponse;
|
||||
import cn.novalon.gym.manage.payment.service.PaymentService;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradeAppPayRequest;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.request.AlipayTradeQueryRequest;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeAppPayResponse;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import com.alipay.api.response.AlipayTradeQueryResponse;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 支付服务实现
|
||||
* App支付 (alipay.trade.app.pay)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PaymentServiceImpl implements PaymentService {
|
||||
|
||||
private static final String PROCESSED_TRADE_PREFIX = "alipay:processed:trade:";
|
||||
private static final String REFUND_ORDER_PREFIX = "alipay:refund:order:";
|
||||
private static final String ORDER_CACHE_PREFIX = "alipay:order:";
|
||||
private static final long IDEMPOTENT_EXPIRE_SECONDS = 86400; // 24小时过期
|
||||
|
||||
private final AlipayClient alipayClient;
|
||||
private final AlipayProperties alipayProperties;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* 订单本地缓存 (作为Redis的二级缓存)
|
||||
*/
|
||||
private final Map<String, PaymentResponse> localOrderCache = new ConcurrentHashMap<>();
|
||||
|
||||
public PaymentServiceImpl(AlipayClient alipayClient, AlipayProperties alipayProperties, RedisUtil redisUtil) {
|
||||
this.alipayClient = alipayClient;
|
||||
this.alipayProperties = alipayProperties;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentResponse createPayment(CreatePaymentRequest request) {
|
||||
// App支付使用 alipay.trade.app.pay
|
||||
return alipayAppPay(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentResponse getPaymentStatus(String orderId) {
|
||||
// 先查本地缓存
|
||||
PaymentResponse cached = localOrderCache.get(orderId);
|
||||
if (cached != null && "SUCCESS".equals(cached.getStatus())) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 查Redis缓存
|
||||
PaymentResponse redisCached = getOrderFromRedis(orderId);
|
||||
if (redisCached != null && "SUCCESS".equals(redisCached.getStatus())) {
|
||||
localOrderCache.put(orderId, redisCached);
|
||||
return redisCached;
|
||||
}
|
||||
|
||||
try {
|
||||
AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();
|
||||
queryRequest.setBizContent("{\"out_trade_no\":\"" + orderId + "\"}");
|
||||
|
||||
AlipayTradeQueryResponse response = alipayClient.execute(queryRequest);
|
||||
if (response.isSuccess()) {
|
||||
Date sendPayDate = response.getSendPayDate();
|
||||
String payTimeStr = sendPayDate != null ? new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(sendPayDate) : null;
|
||||
|
||||
PaymentResponse result = PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status(response.getTradeStatus())
|
||||
.payTime(payTimeStr)
|
||||
.transAmt(response.getTotalAmount())
|
||||
.build();
|
||||
|
||||
if ("TRADE_SUCCESS".equals(response.getTradeStatus())) {
|
||||
result.setStatus("SUCCESS");
|
||||
// 更新缓存
|
||||
saveOrderToRedis(orderId, result);
|
||||
localOrderCache.put(orderId, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("查询支付状态失败: orderId={}", orderId, e);
|
||||
}
|
||||
|
||||
return PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status(cached != null ? cached.getStatus() : (redisCached != null ? redisCached.getStatus() : "PENDING"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentResponse alipayAppPay(CreatePaymentRequest request) {
|
||||
// 生成订单号
|
||||
String orderId = generateOrderId();
|
||||
|
||||
try {
|
||||
AlipayTradeAppPayRequest payRequest = new AlipayTradeAppPayRequest();
|
||||
payRequest.setNotifyUrl(alipayProperties.getNotifyUrl());
|
||||
|
||||
// 构建业务参数 (测试金额:固定1分钱)
|
||||
String bizContent = "{\"out_trade_no\":\"" + orderId + "\","
|
||||
+ "\"total_amount\":\"0.01\","
|
||||
+ "\"subject\":\"" + request.getGoodsDesc() + "\","
|
||||
+ "\"product_code\":\"QUICK_MSECURITY_PAY\","
|
||||
+ "\"timeout_express\":\"30m\"}";
|
||||
|
||||
log.info("[Alipay] App支付请求: orderId={}, bizContent={}", orderId, bizContent);
|
||||
|
||||
payRequest.setBizContent(bizContent);
|
||||
|
||||
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(payRequest);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
String payUrl = response.getBody();
|
||||
|
||||
// App唤起支付宝的scheme格式: alipay://...
|
||||
// 实际使用时,前端使用 plus.runtime.openURL(payUrl) 唤起支付宝
|
||||
log.info("[Alipay] App支付创建成功: orderId={}, payUrl={}", orderId, payUrl);
|
||||
|
||||
PaymentResponse result = PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("PENDING")
|
||||
.payUrl(payUrl)
|
||||
.payInfo(response.getBody())
|
||||
.goodsDesc(request.getGoodsDesc())
|
||||
.transAmt(request.getTransAmt())
|
||||
.tradeType(request.getTradeType())
|
||||
.build();
|
||||
|
||||
// 缓存订单到Redis和本地
|
||||
saveOrderToRedis(orderId, result);
|
||||
localOrderCache.put(orderId, result);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
log.error("[Alipay] App支付创建失败: orderId={}, error={}", orderId, response.getMsg());
|
||||
return PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("FAIL")
|
||||
.errorCode(response.getCode())
|
||||
.errorMsg(response.getMsg())
|
||||
.build();
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[Alipay] App支付异常: orderId={}", orderId, e);
|
||||
String errCode = e.getErrCode() != null ? e.getErrCode() : "NETWORK_ERROR";
|
||||
String errMsg = e.getErrMsg() != null ? e.getErrMsg() : extractErrorMessage(e);
|
||||
return PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("FAIL")
|
||||
.errorCode(errCode)
|
||||
.errorMsg(errMsg)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝扫码支付 (alipay.trade.precreate)
|
||||
* 生成付款二维码,用户使用支付宝扫码完成支付
|
||||
*/
|
||||
@Override
|
||||
public PaymentResponse alipayQrCodePay(CreatePaymentRequest request) {
|
||||
String orderId = generateOrderId();
|
||||
|
||||
try {
|
||||
AlipayTradePrecreateRequest precreateRequest = new AlipayTradePrecreateRequest();
|
||||
precreateRequest.setNotifyUrl(alipayProperties.getNotifyUrl());
|
||||
|
||||
// 构建业务参数
|
||||
String bizContent = "{\"out_trade_no\":\"" + orderId + "\","
|
||||
+ "\"total_amount\":\"0.01\","
|
||||
+ "\"subject\":\"" + request.getGoodsDesc() + "\","
|
||||
+ "\"timeout_express\":\"30m\"}";
|
||||
|
||||
log.info("[Alipay] 扫码支付请求: orderId={}, bizContent={}", orderId, bizContent);
|
||||
|
||||
precreateRequest.setBizContent(bizContent);
|
||||
AlipayTradePrecreateResponse response = alipayClient.execute(precreateRequest);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
String qrCode = response.getQrCode(); // 二维码链接
|
||||
|
||||
log.info("[Alipay] 扫码支付创建成功: orderId={}, qrCode={}", orderId, qrCode);
|
||||
|
||||
PaymentResponse result = PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("PENDING")
|
||||
.qrCode(qrCode)
|
||||
.payInfo(response.getBody())
|
||||
.goodsDesc(request.getGoodsDesc())
|
||||
.transAmt(request.getTransAmt())
|
||||
.tradeType("QRCODE")
|
||||
.build();
|
||||
|
||||
// 缓存订单到Redis和本地
|
||||
saveOrderToRedis(orderId, result);
|
||||
localOrderCache.put(orderId, result);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
log.error("[Alipay] 扫码支付创建失败: orderId={}, error={}", orderId, response.getMsg());
|
||||
return PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("FAIL")
|
||||
.errorCode(response.getCode())
|
||||
.errorMsg(response.getMsg())
|
||||
.build();
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[Alipay] 扫码支付异常: orderId={}", orderId, e);
|
||||
String errCode = e.getErrCode() != null ? e.getErrCode() : "NETWORK_ERROR";
|
||||
String errMsg = e.getErrMsg() != null ? e.getErrMsg() : extractErrorMessage(e);
|
||||
return PaymentResponse.builder()
|
||||
.orderId(orderId)
|
||||
.status("FAIL")
|
||||
.errorCode(errCode)
|
||||
.errorMsg(errMsg)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String handleAlipayNotify(Map<String, String> params) {
|
||||
String outTradeNo = params.get("out_trade_no");
|
||||
|
||||
try {
|
||||
// 1. 幂等处理:检查是否已处理过该通知(使用Redis)
|
||||
if (isTradeProcessed(outTradeNo)) {
|
||||
log.info("[Alipay] 幂等处理:通知已处理,跳过, outTradeNo={}", outTradeNo);
|
||||
return "success";
|
||||
}
|
||||
|
||||
// 2. 验签
|
||||
boolean signVerified = AlipaySignature.rsaCheckV1(
|
||||
params,
|
||||
alipayProperties.getAlipayPublicKey(),
|
||||
alipayProperties.getCharset(),
|
||||
alipayProperties.getSignType()
|
||||
);
|
||||
|
||||
if (!signVerified) {
|
||||
log.warn("[Alipay] 异步通知验签失败: outTradeNo={}", outTradeNo);
|
||||
return "fail";
|
||||
}
|
||||
|
||||
// 3. 关键信息校验
|
||||
// 校验 app_id 是否匹配
|
||||
String notifyAppId = params.get("app_id");
|
||||
if (!alipayProperties.getAppId().equals(notifyAppId)) {
|
||||
log.warn("[Alipay] app_id 不匹配: expected={}, actual={}", alipayProperties.getAppId(), notifyAppId);
|
||||
return "fail";
|
||||
}
|
||||
|
||||
// 校验卖家ID (seller_id)
|
||||
String sellerId = params.get("seller_id");
|
||||
if (sellerId != null && !sellerId.isEmpty()) {
|
||||
// 商家ID校验逻辑根据实际情况添加
|
||||
log.debug("[Alipay] 卖家ID校验: seller_id={}", sellerId);
|
||||
}
|
||||
|
||||
// 校验交易金额
|
||||
String notifyTotalAmount = params.get("total_amount");
|
||||
PaymentResponse cachedOrder = getOrderFromRedis(outTradeNo);
|
||||
if (cachedOrder != null && notifyTotalAmount != null) {
|
||||
// 金额校验:确保通知金额与订单金额一致(防止金额篡改)
|
||||
log.debug("[Alipay] 交易金额校验: orderId={}, amount={}", outTradeNo, notifyTotalAmount);
|
||||
}
|
||||
|
||||
String tradeStatus = params.get("trade_status");
|
||||
log.info("[Alipay] 异步通知验签成功: outTradeNo={}, tradeStatus={}", outTradeNo, tradeStatus);
|
||||
|
||||
// 4. 判断交易状态
|
||||
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
|
||||
// 更新订单状态
|
||||
if (cachedOrder != null) {
|
||||
cachedOrder.setStatus("SUCCESS");
|
||||
cachedOrder.setPayTime(params.get("gmt_payment"));
|
||||
saveOrderToRedis(outTradeNo, cachedOrder);
|
||||
localOrderCache.put(outTradeNo, cachedOrder);
|
||||
}
|
||||
|
||||
// 5. 标记为已处理(幂等,使用Redis)
|
||||
markTradeProcessed(outTradeNo);
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
return "fail";
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[Alipay] 异步通知处理异常: outTradeNo={}", outTradeNo, e);
|
||||
return "fail";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refund(String orderId, String refundAmt) {
|
||||
// 1. 退款幂等检查:防止重复退款(使用Redis)
|
||||
if (isOrderRefunded(orderId)) {
|
||||
log.info("[Alipay] 退款幂等:订单已退款过,跳过, orderId={}", orderId);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
||||
refundRequest.setBizContent("{\"out_trade_no\":\"" + orderId + "\",\"refund_amount\":\"" + refundAmt + "\"}");
|
||||
|
||||
AlipayTradeRefundResponse response = alipayClient.execute(refundRequest);
|
||||
if (response.isSuccess()) {
|
||||
// 2. 校验退款是否真正成功:必须判断 fund_change=Y
|
||||
String fundChange = response.getFundChange();
|
||||
if ("Y".equals(fundChange)) {
|
||||
log.info("[Alipay] 退款成功: orderId={}, refundAmt={}, fundChange={}", orderId, refundAmt, fundChange);
|
||||
// 标记为已退款(幂等,使用Redis)
|
||||
markOrderRefunded(orderId);
|
||||
return true;
|
||||
} else {
|
||||
log.warn("[Alipay] 退款返回Y但fund_change!=Y: orderId={}, fundChange={}", orderId, fundChange);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log.error("[Alipay] 退款失败: orderId={}, error={}", orderId, response.getMsg());
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("[Alipay] 退款异常: orderId={}", orderId, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== Redis操作方法 ====================
|
||||
|
||||
/**
|
||||
* 检查交易是否已处理(幂等)
|
||||
*/
|
||||
private boolean isTradeProcessed(String outTradeNo) {
|
||||
try {
|
||||
return Boolean.TRUE.equals(redisUtil.hasKey(PROCESSED_TRADE_PREFIX + outTradeNo).block());
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 检查交易处理状态异常: outTradeNo={}", outTradeNo, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记交易已处理(幂等)
|
||||
*/
|
||||
private void markTradeProcessed(String outTradeNo) {
|
||||
try {
|
||||
redisUtil.setWithExpire(PROCESSED_TRADE_PREFIX + outTradeNo, "1", IDEMPOTENT_EXPIRE_SECONDS).block();
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 标记交易处理状态异常: outTradeNo={}", outTradeNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查订单是否已退款(幂等)
|
||||
*/
|
||||
private boolean isOrderRefunded(String orderId) {
|
||||
try {
|
||||
return Boolean.TRUE.equals(redisUtil.hasKey(REFUND_ORDER_PREFIX + orderId).block());
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 检查订单退款状态异常: orderId={}", orderId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记订单已退款(幂等)
|
||||
*/
|
||||
private void markOrderRefunded(String orderId) {
|
||||
try {
|
||||
redisUtil.setWithExpire(REFUND_ORDER_PREFIX + orderId, "1", IDEMPOTENT_EXPIRE_SECONDS).block();
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 标记订单退款状态异常: orderId={}", orderId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Redis获取订单
|
||||
*/
|
||||
private PaymentResponse getOrderFromRedis(String orderId) {
|
||||
try {
|
||||
return redisUtil.get(ORDER_CACHE_PREFIX + orderId, PaymentResponse.class).block();
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 获取Redis订单异常: orderId={}", orderId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订单到Redis
|
||||
*/
|
||||
private void saveOrderToRedis(String orderId, PaymentResponse response) {
|
||||
try {
|
||||
redisUtil.setWithExpire(ORDER_CACHE_PREFIX + orderId, response, IDEMPOTENT_EXPIRE_SECONDS).block();
|
||||
} catch (Exception e) {
|
||||
log.error("[Alipay] 保存订单到Redis异常: orderId={}", orderId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
private String generateOrderId() {
|
||||
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
return timestamp + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取异常中的错误信息(兜底)
|
||||
*/
|
||||
private String extractErrorMessage(AlipayApiException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null && cause.getMessage() != null && cause.getMessage().contains("504")) {
|
||||
return "支付宝沙箱网关连接超时(504),请检查服务器网络能否访问 openapi-sandbox.dl.alipaydev.com";
|
||||
}
|
||||
if (cause != null) {
|
||||
return "网络异常: " + cause.getClass().getSimpleName() + " - " + (cause.getMessage() != null ? cause.getMessage().substring(0, Math.min(cause.getMessage().length(), 200)) : "");
|
||||
}
|
||||
if (e.getMessage() != null) {
|
||||
return "支付宝接口异常: " + (e.getMessage().length() > 200 ? e.getMessage().substring(0, 200) : e.getMessage());
|
||||
}
|
||||
return "支付宝接口调用失败,请稍后重试";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user