2025-11-26 11:16:51 +08:00
|
|
|
|
package com.ruoyi.database.service;
|
|
|
|
|
|
|
2025-11-28 14:35:30 +08:00
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
|
import com.alibaba.fastjson.TypeReference;
|
|
|
|
|
|
import com.alibaba.fastjson.parser.Feature;
|
|
|
|
|
|
import com.alipay.api.AlipayApiException;
|
|
|
|
|
|
import com.alipay.api.internal.util.AlipayEncrypt;
|
|
|
|
|
|
import com.alipay.api.internal.util.AlipaySignature;
|
2025-11-26 11:16:51 +08:00
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
|
import com.ruoyi.database.exception.BusinessException;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
2025-11-28 14:35:30 +08:00
|
|
|
|
import org.checkerframework.checker.units.qual.A;
|
2025-11-26 11:16:51 +08:00
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
|
import org.springframework.http.*;
|
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import org.springframework.web.client.RestTemplate;
|
2025-11-28 14:35:30 +08:00
|
|
|
|
|
|
|
|
|
|
import javax.crypto.Cipher;
|
|
|
|
|
|
import javax.crypto.spec.SecretKeySpec;
|
2025-11-26 11:16:51 +08:00
|
|
|
|
import java.net.URLEncoder;
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
|
|
import java.security.KeyFactory;
|
|
|
|
|
|
import java.security.PrivateKey;
|
|
|
|
|
|
import java.security.Signature;
|
|
|
|
|
|
import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
|
|
@Slf4j
|
|
|
|
|
|
@Service
|
|
|
|
|
|
public class AlipayPhoneService {
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private RestTemplate restTemplate;
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
|
|
|
|
@Value("${alipay.appId}")
|
|
|
|
|
|
private String appId;
|
|
|
|
|
|
|
|
|
|
|
|
@Value("${alipay.privateKey}")
|
|
|
|
|
|
private String privateKey;
|
|
|
|
|
|
|
|
|
|
|
|
@Value("${alipay.gateway}")
|
|
|
|
|
|
private String gateway;
|
|
|
|
|
|
|
2025-11-28 14:35:30 +08:00
|
|
|
|
|
|
|
|
|
|
private final String AES_STR = "LoKBapEj35+bRnC/Rrhe8g==";
|
|
|
|
|
|
|
|
|
|
|
|
public String getPhoneNumber(String authCode) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 使用auth_code获取access_token
|
|
|
|
|
|
// String accessToken = getAccessToken(authCode);
|
|
|
|
|
|
System.out.println("authCode:" + authCode);
|
|
|
|
|
|
String charset = "UTF-8";
|
|
|
|
|
|
String encryptType = "AES";
|
|
|
|
|
|
//判断是否为加密内容
|
|
|
|
|
|
boolean isDataEncrypted = !authCode.startsWith("{");
|
|
|
|
|
|
//3. 解密
|
|
|
|
|
|
String plainData = null;
|
|
|
|
|
|
if (isDataEncrypted) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
plainData = AlipayEncrypt.decryptContent(authCode, encryptType, AES_STR, charset);
|
|
|
|
|
|
System.out.println("解密:" + plainData);
|
|
|
|
|
|
} catch (AlipayApiException e) {
|
|
|
|
|
|
//解密异常, 记录日志
|
|
|
|
|
|
throw new Exception("解密异常");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
plainData = authCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. 使用access_token获取用户信息(包含手机号)
|
|
|
|
|
|
Map<String, Object> responseMap = objectMapper.readValue(plainData, Map.class);
|
|
|
|
|
|
return responseMap.get("mobile").toString();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("获取支付宝手机号异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_PHONE_ERROR", "获取支付宝手机号异常: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 11:16:51 +08:00
|
|
|
|
/**
|
2025-12-03 08:49:50 +08:00
|
|
|
|
* 获取用户id
|
2025-11-26 11:16:51 +08:00
|
|
|
|
*/
|
2025-12-03 08:49:50 +08:00
|
|
|
|
private String getOpenId(String authCode) {
|
2025-11-26 11:16:51 +08:00
|
|
|
|
try {
|
|
|
|
|
|
Map<String, String> params = buildBaseParams();
|
|
|
|
|
|
params.put("method", "alipay.system.oauth.token");
|
|
|
|
|
|
params.put("grant_type", "authorization_code");
|
|
|
|
|
|
params.put("code", authCode);
|
|
|
|
|
|
|
2025-11-28 14:35:30 +08:00
|
|
|
|
log.info("获取access_token请求参数: {}", params);
|
2025-11-26 11:16:51 +08:00
|
|
|
|
|
|
|
|
|
|
String sign = generateSign(params);
|
|
|
|
|
|
params.put("sign", sign);
|
|
|
|
|
|
|
|
|
|
|
|
String responseBody = callAlipayApiWithCharset(params);
|
2025-11-28 14:35:30 +08:00
|
|
|
|
log.info("获取access_token响应: {}", responseBody);
|
2025-11-26 11:16:51 +08:00
|
|
|
|
|
|
|
|
|
|
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
|
|
|
|
|
Map<String, Object> tokenResponse = (Map<String, Object>) responseMap.get("alipay_system_oauth_token_response");
|
|
|
|
|
|
|
|
|
|
|
|
if (tokenResponse == null) {
|
|
|
|
|
|
Map<String, Object> errorResponse = (Map<String, Object>) responseMap.get("error_response");
|
|
|
|
|
|
if (errorResponse != null) {
|
|
|
|
|
|
String subCode = (String) errorResponse.get("sub_code");
|
|
|
|
|
|
String subMsg = (String) errorResponse.get("sub_msg");
|
2025-11-28 14:35:30 +08:00
|
|
|
|
throw new BusinessException("ALIPAY_ACCESS_TOKEN_ERROR",
|
|
|
|
|
|
String.format("获取access_token失败[%s]: %s", subCode, subMsg));
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new BusinessException("ALIPAY_ACCESS_TOKEN_ERROR", "获取access_token失败: 响应格式错误");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 08:49:50 +08:00
|
|
|
|
String accessToken = (String) tokenResponse.get("open_id");
|
2025-11-28 14:35:30 +08:00
|
|
|
|
if (accessToken == null || accessToken.trim().isEmpty()) {
|
|
|
|
|
|
throw new BusinessException("ALIPAY_ACCESS_TOKEN_ERROR", "获取access_token失败: access_token为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.info("成功获取access_token: {}", accessToken);
|
|
|
|
|
|
return accessToken;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("获取access_token异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_ACCESS_TOKEN_ERROR", "获取access_token异常: " + e.getMessage());
|
2025-11-26 11:16:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修复后的API调用方法 - 正确处理charset参数
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String callAlipayApiWithCharset(Map<String, String> params) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 将charset参数提取到URL查询字符串中
|
|
|
|
|
|
String charset = params.get("charset");
|
|
|
|
|
|
params.remove("charset"); // 从业务参数中移除
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 构建表单数据
|
|
|
|
|
|
String formData = params.entrySet().stream()
|
|
|
|
|
|
.map(entry -> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用指定的charset进行编码
|
|
|
|
|
|
String encodedValue = URLEncoder.encode(entry.getValue(), charset);
|
|
|
|
|
|
return entry.getKey() + "=" + encodedValue;
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("参数编码失败: key={}, value={}", entry.getKey(), entry.getValue(), e);
|
|
|
|
|
|
return entry.getKey() + "=" + entry.getValue();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect(Collectors.joining("&"));
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 构建完整的URL,包含charset参数
|
|
|
|
|
|
String url = gateway + "?charset=" + charset;
|
|
|
|
|
|
|
|
|
|
|
|
log.debug("支付宝API请求URL: {}", url);
|
|
|
|
|
|
log.debug("支付宝API请求表单数据: {}", formData);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 设置请求头
|
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
|
|
|
|
|
headers.add("User-Agent", "Alipay Client");
|
|
|
|
|
|
|
|
|
|
|
|
HttpEntity<String> request = new HttpEntity<>(formData, headers);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 调用接口
|
|
|
|
|
|
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
|
|
|
|
|
|
return response.getBody();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("调用支付宝API异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_API_ERROR", "调用支付宝API异常: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修复签名生成方法
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String generateSign(Map<String, String> params) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 参数排序
|
|
|
|
|
|
List<String> keys = new ArrayList<>(params.keySet());
|
|
|
|
|
|
Collections.sort(keys);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 拼接待签名字符串(排除sign参数)
|
|
|
|
|
|
String signContent = keys.stream()
|
|
|
|
|
|
.filter(key -> !"sign".equals(key) && params.get(key) != null && !params.get(key).isEmpty())
|
|
|
|
|
|
.map(key -> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用UTF-8编码进行签名(即使charset是其他值)
|
|
|
|
|
|
String value = params.get(key);
|
|
|
|
|
|
return key + "=" + value;
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
return key + "=" + params.get(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect(Collectors.joining("&"));
|
|
|
|
|
|
|
|
|
|
|
|
log.debug("待签名字符串: {}", signContent);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 使用RSA2签名
|
|
|
|
|
|
Signature signature = Signature.getInstance("SHA256WithRSA");
|
|
|
|
|
|
signature.initSign(getPrivateKey());
|
|
|
|
|
|
signature.update(signContent.getBytes(StandardCharsets.UTF_8)); // 使用UTF-8编码签名
|
|
|
|
|
|
byte[] signed = signature.sign();
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Base64编码
|
|
|
|
|
|
String sign = Base64.getEncoder().encodeToString(signed);
|
|
|
|
|
|
log.debug("生成签名: {}", sign);
|
|
|
|
|
|
|
|
|
|
|
|
return sign;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("生成支付宝签名异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_SIGN_ERROR", "生成签名失败: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建基础参数 - 确保charset正确
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Map<String, String> buildBaseParams() {
|
|
|
|
|
|
Map<String, String> params = new HashMap<>();
|
|
|
|
|
|
params.put("app_id", appId);
|
|
|
|
|
|
params.put("charset", "UTF-8"); // 明确指定UTF-8
|
|
|
|
|
|
params.put("sign_type", "RSA2");
|
|
|
|
|
|
params.put("timestamp", getCurrentTimestamp());
|
|
|
|
|
|
params.put("version", "1.0");
|
|
|
|
|
|
params.put("format", "json");
|
|
|
|
|
|
return params;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String getCurrentTimestamp() {
|
|
|
|
|
|
return java.time.LocalDateTime.now().format(
|
|
|
|
|
|
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取私钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
private PrivateKey getPrivateKey() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
String privateKeyContent = privateKey
|
|
|
|
|
|
.replace("-----BEGIN PRIVATE KEY-----", "")
|
|
|
|
|
|
.replace("-----END PRIVATE KEY-----", "")
|
|
|
|
|
|
.replaceAll("\\s", "");
|
|
|
|
|
|
|
|
|
|
|
|
byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
|
|
|
|
|
|
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
|
|
|
|
|
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
|
|
|
|
return keyFactory.generatePrivate(keySpec);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
throw new BusinessException("ALIPAY_KEY_ERROR", "加载私钥失败: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 调用支付宝API
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String callAlipayApi(Map<String, String> params) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 构建表单数据
|
|
|
|
|
|
String formData = params.entrySet().stream()
|
|
|
|
|
|
.map(entry -> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name());
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
return entry.getKey() + "=" + entry.getValue();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect(Collectors.joining("&"));
|
|
|
|
|
|
|
|
|
|
|
|
log.debug("支付宝API请求表单数据: {}", formData);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置请求头
|
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
|
|
|
|
|
headers.add("User-Agent", "Alipay Client");
|
|
|
|
|
|
|
|
|
|
|
|
HttpEntity<String> request = new HttpEntity<>(formData, headers);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用接口
|
|
|
|
|
|
ResponseEntity<String> response = restTemplate.postForEntity(gateway, request, String.class);
|
|
|
|
|
|
return response.getBody();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("调用支付宝API异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_API_ERROR", "调用支付宝API异常: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 验证支付宝响应签名(简化版,实际需要完整实现)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private boolean verifySign(Map<String, Object> responseMap) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 这里需要实现完整的支付宝响应签名验证
|
|
|
|
|
|
// 包括提取sign、排序参数、使用支付宝公钥验证等
|
|
|
|
|
|
// 生产环境必须实现此方法
|
|
|
|
|
|
|
|
|
|
|
|
log.warn("支付宝响应签名验证需要完整实现");
|
|
|
|
|
|
return true; // 测试环境暂时返回true
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("验证支付宝签名异常", e);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 验证手机号格式
|
|
|
|
|
|
*/
|
|
|
|
|
|
public boolean validatePhoneNumber(String phoneNumber) {
|
|
|
|
|
|
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 简单的手机号格式验证
|
|
|
|
|
|
return phoneNumber.matches("^1[3-9]\\d{9}$");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取支付宝用户ID (如果需要)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public String getAlipayUserId(String authCode) {
|
|
|
|
|
|
try {
|
2025-12-12 17:20:23 +08:00
|
|
|
|
String openId = getOpenId(authCode);
|
2025-11-26 11:16:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 在实际业务中,你可能需要根据手机号关联用户ID
|
|
|
|
|
|
// 这里返回手机号作为示例,实际应该返回支付宝用户ID
|
2025-12-12 17:20:23 +08:00
|
|
|
|
return openId;
|
2025-11-26 11:16:51 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
log.error("获取支付宝用户ID异常", e);
|
|
|
|
|
|
throw new BusinessException("ALIPAY_USER_ID_ERROR", "获取支付宝用户ID异常: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|