新增Mifa支付对接
This commit is contained in:
parent
844e5942fe
commit
6e80241a59
@ -18,6 +18,7 @@ import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodi
|
||||
import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService;
|
||||
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
|
||||
import com.red.circle.order.infra.database.rds.service.pay.PayCountryService;
|
||||
import com.red.circle.order.infra.util.IpUtils;
|
||||
import com.red.circle.order.inner.asserts.PayErrorCode;
|
||||
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType;
|
||||
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
|
||||
@ -30,10 +31,14 @@ import com.red.circle.order.app.common.PayerMaxProperties;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static com.red.circle.tool.core.text.StringPool.SYMBOL_COMMA;
|
||||
|
||||
/**
|
||||
* web 下单处理.
|
||||
*
|
||||
@ -119,7 +124,7 @@ public class PayWebPlaceAnOrderCmdExe {
|
||||
+ "&appType=" + commodity.getType());
|
||||
details.setCancelRedirectUrl(details.getRechargeUrl());
|
||||
details.setNewVersion(cmd.getNewVersion());
|
||||
details.setRequestIp(cmd.getRequestIp());
|
||||
details.setRequestIp(IpUtils.getIpAddr());
|
||||
|
||||
// 记录充值金额
|
||||
rechargeRecord(details);
|
||||
|
||||
@ -0,0 +1,160 @@
|
||||
package com.red.circle.order.app.command.pay.web.strategy;
|
||||
|
||||
import com.red.circle.component.pay.paymax.service.PlaceAnOrderParam;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.ResponseErrorCode;
|
||||
import com.red.circle.order.app.common.MiFaPayPlaceAnOrderParam;
|
||||
import com.red.circle.order.app.dto.clientobject.MiFaPayResponseCO;
|
||||
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
|
||||
import com.red.circle.order.app.service.MiFaPayService;
|
||||
import com.red.circle.order.domain.payer.PlaceAnOrderPaymentDetail;
|
||||
import com.red.circle.order.infra.util.MemberInfoGenerator;
|
||||
import com.red.circle.order.inner.asserts.OrderErrorCode;
|
||||
import com.red.circle.tool.core.json.JacksonUtils;
|
||||
import com.red.circle.tool.core.text.StringUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.red.circle.tool.core.text.StringPool.SYMBOL_COMMA;
|
||||
|
||||
/**
|
||||
* MiFaPay web 支付策略
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("MIFA_PAY_PLACE_AN_ORDER")
|
||||
@RequiredArgsConstructor
|
||||
public class MiFaPayWebPayPlaceAnOrderStrategy implements PayWebPlaceAnOrderStrategyV2 {
|
||||
|
||||
private final MiFaPayService miFaPayService;
|
||||
|
||||
@Override
|
||||
public PlaceAnOrderResponseCO doOperation(PayPlaceAnOrderDetailsV2 details) {
|
||||
MiFaPayPlaceAnOrderParam param = getPlaceAnOrderParam(details);
|
||||
|
||||
MiFaPayResponseCO response = requestApi(details, param);
|
||||
ResponseAssert.notNull(OrderErrorCode.CREATE_ORDER_ERROR, response);
|
||||
|
||||
// 500 支付失败,调用第三方错误
|
||||
if (!response.isSuccess()) {
|
||||
ResponseAssert.failure(ResponseErrorCode.INTERNAL_SERVER_ERROR, response.getMsg());
|
||||
}
|
||||
|
||||
// 解析内层data数据
|
||||
MiFaPayResponseCO.MiFaPayData data = parseResponseData(response.getData());
|
||||
|
||||
return new PlaceAnOrderResponseCO()
|
||||
.setTradeNo(data != null ? data.getMbOrderId() : null)
|
||||
.setOrderId(data != null ? data.getOrderId() : details.getOrderId())
|
||||
.setCurrency(details.getCurrency())
|
||||
.setCountryCode(details.getCountryCode())
|
||||
.setFactoryCode(details.getFactoryCode())
|
||||
.setFactoryChannelCode(details.getFactoryChannel())
|
||||
.setRequestUrl(data != null ? data.getPayUrl() : null)
|
||||
.putRequestParam(param)
|
||||
.putResponseParam(response);
|
||||
}
|
||||
|
||||
private MiFaPayResponseCO requestApi(PayPlaceAnOrderDetailsV2 details, MiFaPayPlaceAnOrderParam param) {
|
||||
try {
|
||||
return miFaPayService.placeAnOrder(param);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
log.error("MiFaPay订单创建请求失败:details={}、requestParam={}",
|
||||
JacksonUtils.toJson(details), JacksonUtils.toJson(param));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MiFaPayPlaceAnOrderParam getPlaceAnOrderParam(PayPlaceAnOrderDetailsV2 details) {
|
||||
// 构建商品信息
|
||||
List<MiFaPayPlaceAnOrderParam.ProductInfo> products = details.getProducts().stream()
|
||||
.map(p -> MiFaPayPlaceAnOrderParam.ProductInfo.builder()
|
||||
.sku(p.getCode())
|
||||
.productName(p.getName())
|
||||
.price(p.getAmountUsd().multiply(new BigDecimal("100")).toBigInteger().toString())
|
||||
.quantity(String.valueOf(p.getQuantity()))
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 金额转分(乘以100,去掉小数点)
|
||||
String amountInCents = details.getAmount()
|
||||
.multiply(new BigDecimal("100"))
|
||||
.toBigInteger()
|
||||
.toString();
|
||||
|
||||
MemberInfoGenerator.MemberInfo memberInfo = MemberInfoGenerator.generate(String.valueOf(details.getAcceptUserId()));
|
||||
|
||||
return MiFaPayPlaceAnOrderParam.builder()
|
||||
.merNo(null) // Service中会设置
|
||||
.orderId(details.getOrderId())
|
||||
.memberId(Objects.toString(details.getAcceptUserId()))
|
||||
.time(System.currentTimeMillis() / 1000)
|
||||
.amount(amountInCents)
|
||||
.currency(details.getCurrency())
|
||||
.payWay(details.getChannelCode())
|
||||
.payType(details.getFactoryChannel())
|
||||
.userIp(details.getRequestIp())
|
||||
.returnUrl(details.getSuccessRedirectUrl())
|
||||
.notifyUrl("https://your-domain.com/api/mifapay/notify")
|
||||
.language("en")
|
||||
.email(memberInfo.getEmail())
|
||||
.countryCode(replaceCountryCode(details.getCountryCode()))
|
||||
.firstName(memberInfo.getFirstName())
|
||||
.lastName(memberInfo.getLastName())
|
||||
.phone(memberInfo.getPhone())
|
||||
.productInfo(MiFaPayPlaceAnOrderParam.buildProductInfoJson(products))
|
||||
.terminalType("WEB")
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String replaceCountryCode(String countryCode) {
|
||||
if ("KSA".equalsIgnoreCase(countryCode)) {
|
||||
return countryCode.replaceAll("K", "");
|
||||
}
|
||||
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
|
||||
private String getPlaceAnOrderPaymentDetailParam(PayPlaceAnOrderDetailsV2 details) {
|
||||
if (StringUtils.isBlank(details.getChannelCode())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(details.getFactoryChannel())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PlaceAnOrderPaymentDetail paymentDetail = new PlaceAnOrderPaymentDetail();
|
||||
paymentDetail.setPaymentType(details.getFactoryChannel());
|
||||
if (paymentDetail.getPaymentType().equals("PROMPTPAY")) {
|
||||
paymentDetail.setPaymentType("REALTIME_PAYMENT");
|
||||
paymentDetail.setTargetOrg("PROMPTPAY");
|
||||
}
|
||||
return paymentDetail.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应中的data字段(JSON字符串)
|
||||
*/
|
||||
private MiFaPayResponseCO.MiFaPayData parseResponseData(String dataJson) {
|
||||
if (StringUtils.isBlank(dataJson)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JacksonUtils.readValue(dataJson, MiFaPayResponseCO.MiFaPayData.class);
|
||||
} catch (Exception e) {
|
||||
log.error("MiFaPay解析响应data失败: {}", dataJson, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,349 @@
|
||||
package com.red.circle.order.app.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.red.circle.tool.core.json.JacksonUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MiFaPay下单请求参数
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MiFaPayPlaceAnOrderParam implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// ==================== 必填字段 ====================
|
||||
|
||||
/**
|
||||
* 商户编号(平台分配)
|
||||
*/
|
||||
private String merNo;
|
||||
|
||||
/**
|
||||
* 商户订单号
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 用户唯一标识(商户系统内用户ID)
|
||||
*/
|
||||
private String memberId;
|
||||
|
||||
/**
|
||||
* 请求时间戳
|
||||
*/
|
||||
private Long time;
|
||||
|
||||
/**
|
||||
* 订单金额(单位:分,如 1000000=10000 USD,不能携带小数点)
|
||||
*/
|
||||
private String amount;
|
||||
|
||||
/**
|
||||
* 币种代码(ISO 4217)
|
||||
*/
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* 支付方式(如 BankTransfer/Ewallet)
|
||||
*/
|
||||
private String payWay;
|
||||
|
||||
/**
|
||||
* 支付类型(如 LinePay/Dana)
|
||||
*/
|
||||
private String payType;
|
||||
|
||||
/**
|
||||
* 用户IP地址
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 支付完成后前端跳转地址
|
||||
*/
|
||||
private String returnUrl;
|
||||
|
||||
/**
|
||||
* 支付结果异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 页面语言
|
||||
*/
|
||||
private String language;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 国家/地区编码
|
||||
*/
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
* 用户名字(名)
|
||||
*/
|
||||
private String firstName;
|
||||
|
||||
/**
|
||||
* 用户姓氏(姓)
|
||||
*/
|
||||
private String lastName;
|
||||
|
||||
/**
|
||||
* 用户手机号
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 商品信息(JSON数组字符串)
|
||||
*/
|
||||
private String productInfo;
|
||||
|
||||
/**
|
||||
* 终端类型(WEB/WAP)
|
||||
*/
|
||||
private String terminalType;
|
||||
|
||||
// ==================== 非必填字段 ====================
|
||||
|
||||
/**
|
||||
* 系统类型(IOS/ANDROID,默认为WAP)
|
||||
*/
|
||||
private String osType;
|
||||
|
||||
/**
|
||||
* 结算币种(ISO 4217)
|
||||
*/
|
||||
private String settlementCurrency;
|
||||
|
||||
/**
|
||||
* 信用卡3DS校验,默认开启('1'开启 '0'关闭)
|
||||
*/
|
||||
private String is3DSAuthentication;
|
||||
|
||||
/**
|
||||
* 付款人参数(JSON格式)
|
||||
*/
|
||||
private String paymentInfo;
|
||||
|
||||
/**
|
||||
* 国际银行账户号码
|
||||
*/
|
||||
private String iban;
|
||||
|
||||
/**
|
||||
* 附加数据(查询API和支付通知中原样返回)
|
||||
*/
|
||||
private String attach;
|
||||
|
||||
/**
|
||||
* 账单地址信息(JSON格式)
|
||||
*/
|
||||
private BillingInfo billingInfo;
|
||||
|
||||
/**
|
||||
* 配送地址信息(JSON格式)
|
||||
*/
|
||||
private ShippingInfo shippingInfo;
|
||||
|
||||
/**
|
||||
* 支付卡信息(加密后,JSON格式)
|
||||
*/
|
||||
private PayerInfo payerInfo;
|
||||
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
/**
|
||||
* 商品信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class ProductInfo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 商品SKU
|
||||
*/
|
||||
private String sku;
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
private String productName;
|
||||
|
||||
/**
|
||||
* 商品价格(单位:分)
|
||||
*/
|
||||
private String price;
|
||||
|
||||
/**
|
||||
* 商品数量
|
||||
*/
|
||||
private String quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款人信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class PaymentInfo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 用户标识
|
||||
*/
|
||||
private String userIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单地址信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class BillingInfo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 城市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 邮政编码
|
||||
*/
|
||||
private String postalCode;
|
||||
|
||||
/**
|
||||
* 国家代码
|
||||
*/
|
||||
private String country;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送地址信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class ShippingInfo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 城市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 邮政编码
|
||||
*/
|
||||
private String postalCode;
|
||||
|
||||
/**
|
||||
* 国家代码
|
||||
*/
|
||||
private String country;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付卡信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class PayerInfo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 卡号(脱敏)
|
||||
*/
|
||||
private String cardNo;
|
||||
|
||||
/**
|
||||
* 卡组织(如VISA)
|
||||
*/
|
||||
private String cardOrgn;
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 构建商品信息JSON字符串
|
||||
*/
|
||||
public static String buildProductInfoJson(List<ProductInfo> products) {
|
||||
if (products == null || products.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JacksonUtils.toJson(products);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建付款人信息JSON字符串
|
||||
*/
|
||||
public static String buildPaymentInfoJson(PaymentInfo paymentInfo) {
|
||||
if (paymentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JacksonUtils.toJson(paymentInfo);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
package com.red.circle.order.app.service;
|
||||
|
||||
import com.red.circle.component.pay.paymax.PayMaxUtils;
|
||||
import com.red.circle.order.app.common.MiFaPayPlaceAnOrderParam;
|
||||
import com.red.circle.order.app.dto.clientobject.MiFaPayResponseCO;
|
||||
import com.red.circle.order.infra.config.MiFaPayProperties;
|
||||
import com.red.circle.tool.core.http.RcHttpClient;
|
||||
import com.red.circle.tool.core.json.JacksonUtils;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* MiFaPay支付服务
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MiFaPayService {
|
||||
|
||||
/**
|
||||
* 下单接口路径
|
||||
*/
|
||||
private static final String PLACE_ORDER_PATH = "/paygateway/mbpay/order/en_v2";
|
||||
|
||||
private final String active;
|
||||
private final MiFaPayProperties miFaPayProperties;
|
||||
private final RcHttpClient rcHttpClient = RcHttpClient.builder().build();
|
||||
|
||||
public MiFaPayService(@Value("${spring.profiles.active:dev}") String active,
|
||||
MiFaPayProperties miFaPayProperties) {
|
||||
this.active = active;
|
||||
this.miFaPayProperties = miFaPayProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起支付下单
|
||||
*
|
||||
* @param param 下单参数
|
||||
* @return 支付响应
|
||||
*/
|
||||
public MiFaPayResponseCO placeAnOrder(MiFaPayPlaceAnOrderParam param) {
|
||||
// 设置商户编号
|
||||
param.setMerNo(miFaPayProperties.getMerNo());
|
||||
|
||||
String url = miFaPayProperties.getGatewayBaseUrl() + PLACE_ORDER_PATH;
|
||||
|
||||
// 将param转为JSON字符串作为data
|
||||
String dataJson = JacksonUtils.toJson(param);
|
||||
|
||||
// 对data进行签名
|
||||
String sign = PayMaxUtils.signRsa(dataJson, miFaPayProperties.getRsaPrivateKey());
|
||||
|
||||
// 构建请求体:merAccount + sign + data
|
||||
MiFaPayRequest request = new MiFaPayRequest();
|
||||
request.setMerAccount(miFaPayProperties.getMerAccount());
|
||||
request.setSign(sign);
|
||||
request.setData(dataJson);
|
||||
|
||||
String body = JacksonUtils.toJson(request);
|
||||
|
||||
// 非生产环境打印日志
|
||||
if (!Objects.equals(active, "prod")) {
|
||||
log.warn("MiFaPay Request URL: {}\nBody:{}", url, body);
|
||||
}
|
||||
|
||||
MiFaPayResponseCO response = rcHttpClient.post()
|
||||
.uri(url)
|
||||
.contentType("application/json")
|
||||
.bodyValue(body)
|
||||
.retrieve()
|
||||
.bodyToEntity(MiFaPayResponseCO.class)
|
||||
.block();
|
||||
|
||||
// 非生产环境打印响应日志
|
||||
if (!Objects.equals(active, "prod")) {
|
||||
log.warn("MiFaPay Response: {}", JacksonUtils.toJson(response));
|
||||
}
|
||||
|
||||
// 验证响应签名
|
||||
if (response != null && response.isSuccess() && response.getData() != null) {
|
||||
// boolean validSign = PayMaxUtils.validSignRsa(
|
||||
// response.getData(),
|
||||
// response.getSign(),
|
||||
// miFaPayProperties.getPlatformRsaPublicKey()
|
||||
// );
|
||||
// if (!validSign) {
|
||||
// log.error("MiFaPay response sign verification failed!");
|
||||
// }
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* MiFaPay请求体结构
|
||||
*/
|
||||
@Data
|
||||
public static class MiFaPayRequest {
|
||||
/**
|
||||
* 商户标识
|
||||
*/
|
||||
private String merAccount;
|
||||
|
||||
/**
|
||||
* 签名
|
||||
*/
|
||||
private String sign;
|
||||
|
||||
/**
|
||||
* 主请求参数(JSON字符串)
|
||||
*/
|
||||
private String data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.red.circle.order.app.dto.clientobject;
|
||||
|
||||
import com.red.circle.framework.dto.DTO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* MiFaPay响应结果
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class MiFaPayResponseCO extends DTO {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 响应描述(成功/错误信息)
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 响应码(000000 = 成功)其他均为失败
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 内层响应数据(JSON字符串格式)
|
||||
*/
|
||||
private String data;
|
||||
|
||||
/**
|
||||
* 商户标识(平台分配)
|
||||
*/
|
||||
private String merAccount;
|
||||
|
||||
/**
|
||||
* 响应签名(平台私钥对 data 签名)
|
||||
*/
|
||||
private String sign;
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return "000000".equals(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内层响应数据结构
|
||||
*/
|
||||
@Data
|
||||
public static class MiFaPayData {
|
||||
/**
|
||||
* 商户订单号(与请求一致)
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 平台订单号(平台生成,唯一标识)
|
||||
*/
|
||||
private String mbOrderId;
|
||||
|
||||
/**
|
||||
* 支付链接
|
||||
*/
|
||||
private String payUrl;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.red.circle.order.infra.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* MiFaPay配置属性
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "red-circle.pay.mifa-pay")
|
||||
public class MiFaPayProperties {
|
||||
|
||||
/**
|
||||
* 商户标识(平台分配)
|
||||
*/
|
||||
private String merAccount;
|
||||
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
private String merNo;
|
||||
|
||||
/**
|
||||
* 网关基础URL
|
||||
*/
|
||||
private String gatewayBaseUrl;
|
||||
|
||||
/**
|
||||
* 商户私钥(用于签名)
|
||||
*/
|
||||
private String rsaPrivateKey;
|
||||
|
||||
/**
|
||||
* 平台公钥(用于验签)
|
||||
*/
|
||||
private String platformRsaPublicKey;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.red.circle.order.infra.util;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
public class IpUtils {
|
||||
|
||||
/**
|
||||
* 获取当前请求的客户端IP地址(无需传入HttpServletRequest)
|
||||
*/
|
||||
public static String getIpAddr() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
return getIpAddr(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
String[] headers = {
|
||||
"X-Real-IP",
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
|
||||
String ip = null;
|
||||
for (String header : headers) {
|
||||
ip = request.getHeader(header);
|
||||
if (isValidIp(ip)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidIp(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 多个代理时,取第一个IP
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
// 本地IPv6转IPv4
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip)) {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
private static boolean isValidIp(String ip) {
|
||||
return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.red.circle.order.infra.util;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 根据memberId生成固定的用户信息
|
||||
*
|
||||
* @author system
|
||||
*/
|
||||
public class MemberInfoGenerator {
|
||||
|
||||
private static final String[] FIRST_NAMES = {
|
||||
// 男性名
|
||||
"James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Charles",
|
||||
"Christopher", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua",
|
||||
"Kenneth", "Kevin", "Brian", "George", "Timothy", "Ronald", "Edward", "Jason", "Jeffrey", "Ryan",
|
||||
"Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon",
|
||||
"Benjamin", "Samuel", "Raymond", "Gregory", "Frank", "Alexander", "Patrick", "Jack", "Dennis", "Jerry",
|
||||
// 女性名
|
||||
"Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan", "Jessica", "Sarah", "Karen",
|
||||
"Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle",
|
||||
"Dorothy", "Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", "Cynthia",
|
||||
"Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", "Emma", "Nicole", "Helen",
|
||||
"Samantha", "Katherine", "Christine", "Debra", "Rachel", "Carolyn", "Janet", "Catherine", "Maria", "Heather"
|
||||
};
|
||||
|
||||
private static final String[] LAST_NAMES = {
|
||||
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
|
||||
"Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
|
||||
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson",
|
||||
"Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores",
|
||||
"Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts",
|
||||
"Turner", "Phillips", "Evans", "Parker", "Edwards", "Collins", "Stewart", "Morris", "Murphy", "Cook",
|
||||
"Rogers", "Morgan", "Peterson", "Cooper", "Reed", "Bailey", "Bell", "Gomez", "Kelly", "Howard",
|
||||
"Ward", "Cox", "Diaz", "Richardson", "Wood", "Watson", "Brooks", "Bennett", "Gray", "James",
|
||||
"Reyes", "Cruz", "Hughes", "Price", "Myers", "Long", "Foster", "Sanders", "Ross", "Morales",
|
||||
"Powell", "Sullivan", "Russell", "Ortiz", "Jenkins", "Gutierrez", "Perry", "Butler", "Barnes", "Fisher"
|
||||
};
|
||||
|
||||
private static final String[] EMAIL_DOMAINS = {
|
||||
"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "icloud.com",
|
||||
"mail.com", "protonmail.com", "zoho.com", "aol.com", "yandex.com"
|
||||
};
|
||||
|
||||
private static final String[] PHONE_PREFIXES = {
|
||||
"130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
|
||||
"150", "151", "152", "153", "155", "156", "157", "158", "159",
|
||||
"180", "181", "182", "183", "184", "185", "186", "187", "188", "189",
|
||||
"170", "171", "172", "173", "175", "176", "177", "178"
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据memberId生成固定的名(First Name)
|
||||
*/
|
||||
public static String generateFirstName(String memberId) {
|
||||
int hash = Math.abs(memberId.hashCode());
|
||||
return FIRST_NAMES[hash % FIRST_NAMES.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据memberId生成固定的姓(Last Name)
|
||||
*/
|
||||
public static String generateLastName(String memberId) {
|
||||
int hash = Math.abs((memberId + "_last").hashCode());
|
||||
return LAST_NAMES[hash % LAST_NAMES.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据memberId生成固定的中国手机号
|
||||
*/
|
||||
public static String generatePhone(String memberId) {
|
||||
int hash = Math.abs(memberId.hashCode());
|
||||
String prefix = PHONE_PREFIXES[hash % PHONE_PREFIXES.length];
|
||||
|
||||
// 用memberId的hashCode生成后8位,保证固定
|
||||
int suffix = Math.abs((memberId + "_phone").hashCode()) % 100000000;
|
||||
return prefix + String.format("%08d", suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据memberId生成固定的邮箱
|
||||
*/
|
||||
public static String generateEmail(String memberId) {
|
||||
String firstName = generateFirstName(memberId).toLowerCase();
|
||||
String lastName = generateLastName(memberId).toLowerCase();
|
||||
|
||||
int hash = Math.abs((memberId + "_email").hashCode());
|
||||
String domain = EMAIL_DOMAINS[hash % EMAIL_DOMAINS.length];
|
||||
|
||||
// 生成数字后缀,保证唯一性
|
||||
int numSuffix = Math.abs((memberId + "_num").hashCode()) % 10000;
|
||||
|
||||
return firstName + "." + lastName + numSuffix + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性获取所有信息
|
||||
*/
|
||||
public static MemberInfo generate(String memberId) {
|
||||
return new MemberInfo(
|
||||
generateFirstName(memberId),
|
||||
generateLastName(memberId),
|
||||
generatePhone(memberId),
|
||||
generateEmail(memberId)
|
||||
);
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class MemberInfo {
|
||||
/**
|
||||
* 名
|
||||
*/
|
||||
private String firstName;
|
||||
|
||||
/**
|
||||
* 姓
|
||||
*/
|
||||
private String lastName;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
}
|
||||
}
|
||||
@ -1,26 +1,77 @@
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils;
|
||||
import com.red.circle.other.infra.utils.SeasonUtils;
|
||||
import com.red.circle.other.inner.asserts.user.UserSpecialErrorCode;
|
||||
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
|
||||
import com.red.circle.tool.core.date.ZonedId;
|
||||
import com.red.circle.tool.crypto.SecurityUtils;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import util.KeyUtils;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
|
||||
import static com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils.getCalcBillBelong;
|
||||
|
||||
public class ApiTest {
|
||||
|
||||
@Test
|
||||
public void testSeason() {
|
||||
System.out.println(SeasonUtils.getCurrentSeasonCycleKey());
|
||||
String encryptedKeyPem = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
|
||||
"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\n" +
|
||||
"-----END ENCRYPTED PRIVATE KEY-----";
|
||||
String password = "ev9TZd@SUMYUj9D";
|
||||
|
||||
try {
|
||||
System.out.println(getPrivateKeyPem(KeyUtils.loadEncryptedPrivateKey(encryptedKeyPem, password)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPrivateKeyBase64(PrivateKey privateKey) {
|
||||
return Base64.getEncoder().encodeToString(privateKey.getEncoded());
|
||||
}
|
||||
|
||||
// 或者获取 PEM 格式
|
||||
public static String getPrivateKeyPem(PrivateKey privateKey) {
|
||||
String base64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("-----BEGIN PRIVATE KEY-----\n");
|
||||
// 每64字符换行
|
||||
for (int i = 0; i < base64.length(); i += 64) {
|
||||
sb.append(base64, i, Math.min(i + 64, base64.length()));
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("-----END PRIVATE KEY-----");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package util;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMDecryptorProvider;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.PKCS8Generator;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
|
||||
import org.bouncycastle.operator.InputDecryptorProvider;
|
||||
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
|
||||
public class KeyUtils {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public static PrivateKey loadEncryptedPrivateKey(String encryptedKeyPem, String password) throws Exception {
|
||||
PEMParser pemParser = new PEMParser(new StringReader(encryptedKeyPem));
|
||||
Object object = pemParser.readObject();
|
||||
pemParser.close();
|
||||
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||
|
||||
if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
|
||||
// PKCS#8 加密格式
|
||||
PKCS8EncryptedPrivateKeyInfo encryptedInfo = (PKCS8EncryptedPrivateKeyInfo) object;
|
||||
InputDecryptorProvider decryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
|
||||
.setProvider("BC")
|
||||
.build(password.toCharArray());
|
||||
return converter.getPrivateKey(encryptedInfo.decryptPrivateKeyInfo(decryptorProvider));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported key format");
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,12 @@
|
||||
<artifactId>game-fruit-inner-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user