317 lines
12 KiB
Java
317 lines
12 KiB
Java
|
|
package com.ruoyi.database.service;
|
|||
|
|
|
|||
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
|
|
import com.ruoyi.database.exception.BusinessException;
|
|||
|
|
import lombok.extern.slf4j.Slf4j;
|
|||
|
|
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;
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过授权码获取用户手机号
|
|||
|
|
*/
|
|||
|
|
public String getPhoneNumber(String authCode) {
|
|||
|
|
try {
|
|||
|
|
// 1. 构建请求参数
|
|||
|
|
Map<String, String> params = buildBaseParams();
|
|||
|
|
params.put("method", "alipay.system.oauth.token");
|
|||
|
|
params.put("grant_type", "authorization_code");
|
|||
|
|
params.put("code", authCode);
|
|||
|
|
|
|||
|
|
log.info("请求参数构建完成: {}", params);
|
|||
|
|
|
|||
|
|
// 2. 生成签名
|
|||
|
|
String sign = generateSign(params);
|
|||
|
|
params.put("sign", sign);
|
|||
|
|
|
|||
|
|
log.info("签名生成完成");
|
|||
|
|
|
|||
|
|
// 3. 调用支付宝接口 - 使用修复后的方法
|
|||
|
|
String responseBody = callAlipayApiWithCharset(params);
|
|||
|
|
|
|||
|
|
log.info("支付宝手机号接口响应: {}", responseBody);
|
|||
|
|
|
|||
|
|
// 4. 解析响应
|
|||
|
|
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");
|
|||
|
|
throw new BusinessException("ALIPAY_PHONE_ERROR",
|
|||
|
|
String.format("支付宝接口错误[%s]: %s", subCode, subMsg));
|
|||
|
|
}
|
|||
|
|
throw new BusinessException("ALIPAY_PHONE_ERROR", "支付宝接口响应格式错误");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 提取手机号
|
|||
|
|
String mobile = (String) tokenResponse.get("mobile");
|
|||
|
|
if (mobile == null || mobile.trim().isEmpty()) {
|
|||
|
|
throw new BusinessException("ALIPAY_PHONE_ERROR", "获取手机号失败: 手机号为空");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 6. 验证手机号格式
|
|||
|
|
if (!validatePhoneNumber(mobile)) {
|
|||
|
|
log.warn("获取到的手机号格式可能不正确: {}", mobile);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.info("成功获取用户手机号: {}", mobile);
|
|||
|
|
return mobile;
|
|||
|
|
|
|||
|
|
} catch (BusinessException e) {
|
|||
|
|
throw e;
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("获取支付宝手机号异常", e);
|
|||
|
|
throw new BusinessException("ALIPAY_PHONE_ERROR", "获取支付宝手机号异常: " + e.getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 修复后的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 {
|
|||
|
|
String phoneNumber = getPhoneNumber(authCode);
|
|||
|
|
|
|||
|
|
// 在实际业务中,你可能需要根据手机号关联用户ID
|
|||
|
|
// 这里返回手机号作为示例,实际应该返回支付宝用户ID
|
|||
|
|
return phoneNumber;
|
|||
|
|
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("获取支付宝用户ID异常", e);
|
|||
|
|
throw new BusinessException("ALIPAY_USER_ID_ERROR", "获取支付宝用户ID异常: " + e.getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|