diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java index d319c6b8..b77ba82e 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java @@ -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); diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/MiFaPayWebPayPlaceAnOrderStrategy.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/MiFaPayWebPayPlaceAnOrderStrategy.java new file mode 100644 index 00000000..f72cd8e7 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/MiFaPayWebPayPlaceAnOrderStrategy.java @@ -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 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; + } + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/MiFaPayPlaceAnOrderParam.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/MiFaPayPlaceAnOrderParam.java new file mode 100644 index 00000000..e7584fef --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/MiFaPayPlaceAnOrderParam.java @@ -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 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; + } + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/MiFaPayService.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/MiFaPayService.java new file mode 100644 index 00000000..7fa41712 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/MiFaPayService.java @@ -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; + } +} diff --git a/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/MiFaPayResponseCO.java b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/MiFaPayResponseCO.java new file mode 100644 index 00000000..eea998a3 --- /dev/null +++ b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/MiFaPayResponseCO.java @@ -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; + } +} diff --git a/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/config/MiFaPayProperties.java b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/config/MiFaPayProperties.java new file mode 100644 index 00000000..caf2f103 --- /dev/null +++ b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/config/MiFaPayProperties.java @@ -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; +} diff --git a/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/IpUtils.java b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/IpUtils.java new file mode 100644 index 00000000..e671a91a --- /dev/null +++ b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/IpUtils.java @@ -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); + } + +} diff --git a/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/MemberInfoGenerator.java b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/MemberInfoGenerator.java new file mode 100644 index 00000000..a5f6bef2 --- /dev/null +++ b/rc-service/rc-service-order/order-infrastructure/src/main/java/com/red/circle/order/infra/util/MemberInfoGenerator.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-start/src/test/java/ApiTest.java b/rc-service/rc-service-other/other-start/src/test/java/ApiTest.java index 4f7f4879..bfccd157 100644 --- a/rc-service/rc-service-other/other-start/src/test/java/ApiTest.java +++ b/rc-service/rc-service-other/other-start/src/test/java/ApiTest.java @@ -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 diff --git a/rc-service/rc-service-other/other-start/src/test/java/util/KeyUtils.java b/rc-service/rc-service-other/other-start/src/test/java/util/KeyUtils.java new file mode 100644 index 00000000..70493b3e --- /dev/null +++ b/rc-service/rc-service-other/other-start/src/test/java/util/KeyUtils.java @@ -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"); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/pom.xml b/rc-service/rc-service-other/pom.xml index cc90f5be..97d2a04d 100644 --- a/rc-service/rc-service-other/pom.xml +++ b/rc-service/rc-service-other/pom.xml @@ -55,6 +55,12 @@ game-fruit-inner-api + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + +