新增Mifa支付对接

This commit is contained in:
tianfeng 2026-01-21 17:42:07 +08:00
parent 844e5942fe
commit 6e80241a59
11 changed files with 1051 additions and 7 deletions

View File

@ -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.PayApplicationService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService; 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.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.asserts.PayErrorCode;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType; import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient; 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.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static com.red.circle.tool.core.text.StringPool.SYMBOL_COMMA;
/** /**
* web 下单处理. * web 下单处理.
* *
@ -119,7 +124,7 @@ public class PayWebPlaceAnOrderCmdExe {
+ "&appType=" + commodity.getType()); + "&appType=" + commodity.getType());
details.setCancelRedirectUrl(details.getRechargeUrl()); details.setCancelRedirectUrl(details.getRechargeUrl());
details.setNewVersion(cmd.getNewVersion()); details.setNewVersion(cmd.getNewVersion());
details.setRequestIp(cmd.getRequestIp()); details.setRequestIp(IpUtils.getIpAddr());
// 记录充值金额 // 记录充值金额
rechargeRecord(details); rechargeRecord(details);

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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.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.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.date.ZonedId; import com.red.circle.tool.core.date.ZonedId;
import com.red.circle.tool.crypto.SecurityUtils; import com.red.circle.tool.crypto.SecurityUtils;
import org.junit.Test; 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.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; 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 { public class ApiTest {
@Test @Test
public void testSeason() { public void testSeason() {
System.out.println(SeasonUtils.getCurrentSeasonCycleKey()); String encryptedKeyPem = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
"MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI2fOZPsY3zJcCAggA\n" +
"MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBC6Hp6grTb4SSxC9yDerqesBIIE\n" +
"0D/MUGhb2wegtLR0tsCRlE7qDMaOjw6NZToAE53vcQFjPes+NWDJ8bmDlljMnOcN\n" +
"lec6WQ+ijbEGIWETmqAcn/NhMChn4KKDD8gFdccgkrtteB5cT6ls7vkdotfGnKDZ\n" +
"HlwQm8C7hIWItjHuMf2MXF0NJipFn5KBLIXTxwwYpsZ4UiO8HlhkDcPI3IuPcPoD\n" +
"WS8xXSlIfmBv7ePiYCbkA6DMIlJ2d4PkjKFHGElQmOh3QDdz5yB8xOpPnL/J0gz8\n" +
"lQLP+587uWJKQVyOatHAMwK9Hj/nMREQL5k8E1Ez5lI24Gtf8TCSCwluWJf8UhCi\n" +
"FeItYuT8A5S1G1V4DG9QC9f0/2C7s/62hwpEo1p+se61IOCaTdhsXpaubWoi7f5x\n" +
"0lRGmmVVo1zSvbSmDk1OevyvlYXmJfuINKyv9vFN18JNNljHs15U8CyrI5/nQnec\n" +
"CklD1NeEiUUnkpqBAHb9u1rUS5UCuFmDgKmBa+ed0HQxyIDcKsb3Da9RqiJlIM+v\n" +
"Ao8N46VDEpbW+r9VYA4emfZOLFfPoR3mVEIIKbVQipPriWsKvtDQvqPlZgRd3NIa\n" +
"fLAi2eXLlu8kiG9KHhG6+Al0HKMtcBjqLJwbUtFBmltPnSxUogq/PiPCL2ApyiGR\n" +
"/lYX9m3qgDKuEjHCjp1+ucW07Sf8wFRdv/kTPoVGjR0aLDg9YDVWDt4alXtuLd+d\n" +
"71wNKvQQsUz8Kp8jngMJXv5zmWRD2uJEDH2uOXzzRZZaAcG7NVzI0REauOKT3cKF\n" +
"p/AArwbjvvP/L3OvlyKt/A8KH4SI/7Yt0n1R+UdEWTrXmveuD4CetpilaF/6cqtW\n" +
"nP3ZmyAktxtY7nVESpDJee8sFDb966ruETl7w+88Lvq2Phd/JKd7rubtYhG/rqkd\n" +
"kb39SZjDi9XTy2RBWkGai67Y6J9VIvcpcR2LIyI4f4ZA6SgIunUA7nWATztR+HJf\n" +
"FPtg0wRPj5C0OwvJBHrK/fqdvmb3Sjv1O3UXehHjbN7MbgXsN9b2WwevZjPZPKe7\n" +
"gKl7KvipX+CrKn5qcaApefvujx/JzChoJ9ELqefV6mEEmaVqeXxZPFOHCt8yZEBd\n" +
"3QabTew9fUQL0WyaB8mJrPTUZWaaAJBE23HRu6QnkVGHGrdYGp3UFFiPirj5LxXz\n" +
"mQmfbkN7rzv3IH0Zoxq7OwlmmsUBxDlmFzsE/Ukb74tfntR3RPDizmUnGEQxZtRK\n" +
"hyv0poJ6GZVwZyogjAY6jcSSjZCpVB4WFnOQ8zZC6GIcw/LcUd0OhACJd9ZSaIrX\n" +
"4TndzX5GtcN38h8x3VtupjjqrNm0a+8wyT9XkcxKt5kBgU+hfhHIotR1/itZJpcm\n" +
"V2Mydzuh0Y/pFMm+vi1ThOcJSnVRFhVVQcoZx2VXbUfYCqHsx8iBls6dnE4NwdLS\n" +
"umgBPqoQQUu0/YFMOadXWuIFMpyP9g7rECvdCrnEipWZWy6+HmqOoStD7WSnnjBQ\n" +
"623JPD/jcQfqfoDJfv3gzLHzGYmFi59TY3j3xjBU1N3glo9+Eg92/EFhOEPvzeuy\n" +
"UhymjuXvZ9UdqAbhC/RG8HbfsKmivjF6YlhpfI8l36m49XJTh+lbeXWRgkBIpQU1\n" +
"33PgQDBfeUgFzSWLX5obgAU/mpf2GTu+jfjQVj9IfJVi\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 @Test

View File

@ -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");
}
}

View File

@ -55,6 +55,12 @@
<artifactId>game-fruit-inner-api</artifactId> <artifactId>game-fruit-inner-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>