6 Commits

Author SHA1 Message Date
future 840e24dc3e 合并dev分支 2026-06-24 23:11:01 +08:00
future b5f8523554 完成登录功能 2026-06-24 23:11:01 +08:00
future b291dacf3d 修复丢失文件 2026-06-24 23:11:01 +08:00
future 8cb7bc6ae8 feat: 添加签到模块和数据库表结构
- 新增gym-checkIn签到模块
- 创建sign_in_record签到记录表(V13迁移脚本)
- 添加签到相关路由配置
- 更新应用配置以支持签到模块
- 添加Redis密钥常量
2026-06-24 23:11:00 +08:00
future 91b005975a 签到模块 2026-06-24 23:08:54 +08:00
future 2ffd1aa7d6 修复项目构建 2026-06-24 23:08:53 +08:00
7 changed files with 669 additions and 79 deletions
@@ -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,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 ? "验证码发送成功" : "验证码发送失败")));
@@ -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,12 +45,9 @@ 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 -> {
@@ -70,47 +58,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
log.info("手机号未注册,创建新会员");
return createNewMemberAndLogin(request, encryptedPhone);
}));
})
.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);
}
@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;
}
+2 -1
View File
@@ -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>
+62
View File
@@ -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>
@@ -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()));
});
}
}
@@ -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 "支付宝接口调用失败,请稍后重试";
}
}