diff --git a/.gitignore b/.gitignore index 85fda3b5..5b3ad3d4 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,5 @@ target/ devops-monitor-id_rsa.key /.claude/ + +build.md diff --git a/rc-service/rc-service-order/order-adapter/src/main/java/com/red/circle/order/adapter/app/WebPayRestController.java b/rc-service/rc-service-order/order-adapter/src/main/java/com/red/circle/order/adapter/app/WebPayRestController.java index 59862605..6c085852 100644 --- a/rc-service/rc-service-order/order-adapter/src/main/java/com/red/circle/order/adapter/app/WebPayRestController.java +++ b/rc-service/rc-service-order/order-adapter/src/main/java/com/red/circle/order/adapter/app/WebPayRestController.java @@ -5,8 +5,13 @@ import com.red.circle.common.business.dto.cmd.IdStringCmd; import com.red.circle.order.app.dto.clientobject.pay.ApplicationCommodityCardV2CO; import com.red.circle.order.app.dto.clientobject.pay.PayApplicationCO; import com.red.circle.order.app.dto.clientobject.pay.PayCountryCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5ContextCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5OptionsCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5OrderCO; import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO; import com.red.circle.order.app.dto.clientobject.pay.UserProfileAndCountryCO; +import com.red.circle.order.app.dto.cmd.PayWebH5OptionsCmd; +import com.red.circle.order.app.dto.cmd.PayWebH5PlaceOrderCmd; import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderCmd; import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderReceiptCmd; import com.red.circle.order.app.dto.cmd.PayWebApplicationCommodityCmd; @@ -91,6 +96,58 @@ public class WebPayRestController { return webPayService.getChannelCommodityV2(cmd); } + /** + * H5 充值账号上下文. + * + * @eo.name H5 充值账号上下文. + * @eo.url /h5/context + * @eo.method get + * @eo.request-type formdata + */ + @GetMapping("/h5/context") + public PayWebH5ContextCO h5Context(@Validated PayWebUserCmd cmd) { + return webPayService.getH5Context(cmd); + } + + /** + * H5 充值商品列表. + * + * @eo.name H5 充值商品列表. + * @eo.url /h5/options + * @eo.method get + * @eo.request-type formdata + */ + @GetMapping("/h5/options") + public PayWebH5OptionsCO h5Options(@Validated PayWebH5OptionsCmd cmd) { + return webPayService.getH5Options(cmd); + } + + /** + * H5 MiFaPay 下单. + * + * @eo.name H5 MiFaPay 下单. + * @eo.url /h5/orders + * @eo.method post + * @eo.request-type json + */ + @PostMapping("/h5/orders") + public PayWebH5OrderCO h5Order(@RequestBody @Validated PayWebH5PlaceOrderCmd cmd) { + return webPayService.createH5Order(cmd); + } + + /** + * H5 订单状态. + * + * @eo.name H5 订单状态. + * @eo.url /h5/orders/{orderId} + * @eo.method get + * @eo.request-type formdata + */ + @GetMapping("/h5/orders/{orderId}") + public PayWebH5OrderCO h5OrderStatus(@PathVariable("orderId") String orderId) { + return webPayService.getH5Order(orderId); + } + /** * 下单. * diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebH5OptionsQryExe.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebH5OptionsQryExe.java new file mode 100644 index 00000000..7f46eeba --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebH5OptionsQryExe.java @@ -0,0 +1,158 @@ +package com.red.circle.order.app.command.pay.web; + +import com.red.circle.common.business.core.amount.PayAmountArithmeticUtils; +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.framework.core.response.CommonErrorCode; +import com.red.circle.order.app.dto.clientobject.pay.PayCountryCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5AccountCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5ContextCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5OptionsCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5ProductCO; +import com.red.circle.order.app.dto.clientobject.pay.UserProfileAndCountryCO; +import com.red.circle.order.app.dto.clientobject.pay.WebSiteUseProfileCO; +import com.red.circle.order.app.dto.cmd.PayWebH5OptionsCmd; +import com.red.circle.order.app.dto.cmd.PayWebUserCmd; +import com.red.circle.order.infra.database.rds.entity.pay.PayApplication; +import com.red.circle.order.infra.database.rds.entity.pay.PayApplicationCommodity; +import com.red.circle.order.infra.database.rds.entity.pay.PayCountry; +import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodityService; +import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService; +import com.red.circle.order.infra.database.rds.service.pay.PayCountryService; +import com.red.circle.order.inner.model.enums.PayApplicationCommodityType; +import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient; +import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO; +import com.red.circle.tool.core.collection.CollectionUtils; +import com.red.circle.tool.core.text.StringUtils; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * Aslan H5 充值上下文和商品查询。 + * + *
商品价格必须从后台商品表实时读取,H5 本地 JSON 只决定 MiFaPay 国家和方式,不能参与金额计算。
+ * + * @author tf + */ +@Component +@RequiredArgsConstructor +public class PayWebH5OptionsQryExe { + + private final PayCountryService payCountryService; + private final PayApplicationService payApplicationService; + private final SysCountryCodeClient sysCountryCodeClient; + private final UserProfileAndCountryQueryExe userProfileAndCountryQueryExe; + private final PayApplicationCommodityService payApplicationCommodityService; + + public PayWebH5ContextCO context(PayWebUserCmd cmd) { + defaultType(cmd); + UserProfileAndCountryCO source = userProfileAndCountryQueryExe.execute(cmd); + WebSiteUseProfileCO userProfile = source.getUserProfile(); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, userProfile); + + return new PayWebH5ContextCO() + .setAuthMode("display_user_id") + .setAccount(new PayWebH5AccountCO() + .setUserId(userProfile.getId()) + .setDisplayUserId(userProfile.getId()) + .setUsername(userProfile.getUserNickname()) + .setAvatar(userProfile.getUserAvatar()) + .setAccount(userProfile.getAccount()) + .setSysOrigin(userProfile.getSysOrigin()) + .setFreightAgent(userProfile.getFreightAgent()) + .setSuperFreightAgent(userProfile.getSuperFreightAgent()) + .setAudienceType(resolveAudienceType(userProfile))) + .setCountryList(source.getCountryList()) + .setRegionId(source.getRegionId()); + } + + public PayWebH5OptionsCO options(PayWebH5OptionsCmd cmd) { + defaultType(cmd); + PayApplication application = payApplicationService.getById(cmd.getApplicationId()); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, application); + + PayCountry payCountry = Objects.isNull(cmd.getPayCountryId()) + ? payCountryService.getShelfSortMax() + : payCountryService.getById(cmd.getPayCountryId()); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, payCountry); + + SysCountryCodeDTO country = ResponseAssert.requiredSuccess( + sysCountryCodeClient.getById(payCountry.getCountryId()) + ); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, country); + + List前端只提交商品 id 和支付方式标识;金额、金币数、商品类型、币种和汇率全部从后台表重新生成。
+ * + * @author tf + */ +@Component +@RequiredArgsConstructor +public class PayWebH5PlaceOrderCmdExe { + + private static final String PROVIDER_MIFAPAY = "mifapay"; + private static final String FACTORY_MIFAPAY = "MIFA_PAY"; + + private final PayCountryService payCountryService; + private final PayApplicationService payApplicationService; + private final SysCountryCodeClient sysCountryCodeClient; + private final PayPlaceAnOrderService payPlaceAnOrderService; + private final InAppPurchaseDetailsService inAppPurchaseDetailsService; + private final PayApplicationCommodityService payApplicationCommodityService; + + public PayWebH5OrderCO execute(PayWebH5PlaceOrderCmd cmd) { + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + PROVIDER_MIFAPAY.equalsIgnoreCase(Objects.toString(cmd.getProviderCode(), ""))); + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + Objects.nonNull(cmd.getApplicationId()) + && Objects.nonNull(cmd.getGoodsId()) + && Objects.nonNull(cmd.getPayCountryId()) + && Objects.nonNull(cmd.getUserId()) + && StringUtils.isNotBlank(cmd.getPayWay()) + && StringUtils.isNotBlank(cmd.getPayType())); + + PayPlaceAnOrderDetailsV2 details = buildDetails(cmd); + PlaceAnOrderResponseCO response = payPlaceAnOrderService.doRequest(details); + return new PayWebH5OrderCO() + .setOrderId(details.getOrderId()) + .setProviderCode(PROVIDER_MIFAPAY) + .setPayUrl(response.getRequestUrl()) + .setStatus("redirected") + .setProviderAmountMinor(minor(details.getAmount(), 100L)) + .setUsdMinorAmount(minor(details.getAmountUsd(), 100L)) + .setCurrencyCode(details.getCurrency()) + .setFailureReason(""); + } + + public PayWebH5OrderCO getOrder(String orderId) { + InAppPurchaseDetails order = inAppPurchaseDetailsService.getById(orderId); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, order); + return new PayWebH5OrderCO() + .setOrderId(order.getId()) + .setProviderCode(PROVIDER_MIFAPAY) + .setStatus(toH5Status(order.getStatus())) + .setProviderAmountMinor(minor(order.getAmount(), 100L)) + .setUsdMinorAmount(minor(order.getAmountUsd(), 100L)) + .setCurrencyCode(order.getCurrency()) + .setFailureReason(order.getReason()); + } + + private PayPlaceAnOrderDetailsV2 buildDetails(PayWebH5PlaceOrderCmd cmd) { + PayApplication application = payApplicationService.getById(cmd.getApplicationId()); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, application); + + PayApplicationCommodity commodity = payApplicationCommodityService + .getAppCommodity(cmd.getGoodsId(), cmd.getApplicationId()); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_MAPPING_INFO, commodity); + ResponseAssert.isTrue(CommonErrorCode.QUANTITY_MUST_GT_ZERO, + ArithmeticUtils.gtZero(commodity.getObtainGoldsQuantity())); + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + Boolean.TRUE.equals(commodity.getShelf())); + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + Objects.isNull(cmd.getType()) || Objects.equals(cmd.getType().name(), commodity.getType())); + + PayCountry payCountry = payCountryService.getById(cmd.getPayCountryId()); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, payCountry); + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + Boolean.TRUE.equals(payCountry.getShelf())); + ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR, + Objects.isNull(commodity.getPayCountryId()) + || Objects.equals(commodity.getPayCountryId(), payCountry.getId())); + + SysCountryCodeDTO sysCountryCode = ResponseAssert.requiredSuccess( + sysCountryCodeClient.getById(payCountry.getCountryId()) + ); + ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, sysCountryCode); + + BigDecimal amount = PayAmountArithmeticUtils.multiplyUpNotPoint( + commodity.getAmountUsd(), + payCountry.getUsdExchangeRate() + ); + PayPlaceAnOrderDetailsV2 details = new PayPlaceAnOrderDetailsV2(); + details.setOrderId(IdWorkerUtils.getIdStr()); + details.setReceiptType(InAppPurchaseReceiptType.PAYMENT); + details.setApplicationId(Objects.toString(application.getId())); + details.setAppCommodityType(commodity.getType()); + details.setSysOrigin(application.getAppCode()); + details.setAcceptUserId(cmd.getUserId()); + details.setCreateUserId(cmd.getUserId()); + details.setAmount(amount); + details.setAmountUsd(commodity.getAmountUsd()); + details.setProducts(List.of(new InAppPurchaseProduct() + .setId(commodity.getId()) + .setName(commodity.getType()) + .setCode(commodity.getType()) + .setDescribe(productDescriptor(commodity)) + .setContent(commodity.getObtainGoldsQuantity().stripTrailingZeros().toPlainString()) + .setAmountUsd(commodity.getAmountUsd()) + .setQuantity(1))); + details.setProductDescriptor(productDescriptor(commodity)); + details.setChannelDetails(mifaPayChannel(cmd, payCountry)); + details.setCountry(sysCountryCode); + details.setPayCountry(payCountry); + details.setRechargeUrl(StringUtils.isBlankOrElse(cmd.getReturnUrl(), "")); + details.setSuccessRedirectUrl(StringUtils.isBlankOrElse(cmd.getReturnUrl(), "")); + details.setCancelRedirectUrl(StringUtils.isBlankOrElse(cmd.getReturnUrl(), "")); + details.setNewVersion(Boolean.TRUE); + details.setRequestIp(IpUtils.getIpAddr()); + return details; + } + + private PayCountryChannelDetails mifaPayChannel(PayWebH5PlaceOrderCmd cmd, PayCountry payCountry) { + String channelCode = StringUtils.isNotBlank(cmd.getChannelCode()) + ? cmd.getChannelCode() + : cmd.getPayWay() + "_" + cmd.getPayType(); + // 这里刻意不查后台渠道明细表:H5 支付国家和方式来自 app JSON,本对象只为复用现有 MiFaPay 策略和订单快照。 + return new PayCountryChannelDetails() + .setId(0L) + .setPayCountryId(payCountry.getId()) + .setChannelCode(channelCode) + .setFactoryCode(FACTORY_MIFAPAY) + .setFactoryChannel(cmd.getPayType()) + .setFactoryCurrencyPoint(2) + .setShelf(Boolean.TRUE); + } + + private String productDescriptor(PayApplicationCommodity commodity) { + return PayApplicationCommodityType.GOLD.eq(commodity.getType()) + ? commodity.getType() + : commodity.getAmountUsd().stripTrailingZeros().toPlainString() + + "USD = " + + StringUtils.isBlankOrElse(commodity.getContent(), "0") + + commodity.getType(); + } + + private String toH5Status(InAppPurchaseStatus status) { + if (Objects.equals(status, InAppPurchaseStatus.SUCCESS)) { + return "credited"; + } + if (Objects.equals(status, InAppPurchaseStatus.FAIL) + || Objects.equals(status, InAppPurchaseStatus.CANCEL) + || Objects.equals(status, InAppPurchaseStatus.REFUND)) { + return "failed"; + } + return "pending"; + } + + private Long minor(BigDecimal amount, Long scale) { + if (Objects.isNull(amount)) { + return 0L; + } + return amount.multiply(BigDecimal.valueOf(scale)).setScale(0, RoundingMode.DOWN).longValue(); + } +} 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 index 78734694..6135f746 100644 --- 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 @@ -31,7 +31,7 @@ public class MiFaPayService { private final MiFaPayProperties miFaPayProperties; private final RcHttpClient rcHttpClient = RcHttpClient.builder().build(); - public MiFaPayService(@Value("${spring.profiles.active:dev}") String active, + public MiFaPayService(@Value("${spring.profiles.active:prod}") String active, MiFaPayProperties miFaPayProperties) { this.active = active; this.miFaPayProperties = miFaPayProperties; diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/WebPayServiceImpl.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/WebPayServiceImpl.java index a8715424..b9cd204a 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/WebPayServiceImpl.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/WebPayServiceImpl.java @@ -3,12 +3,19 @@ package com.red.circle.order.app.service; import com.red.circle.common.business.dto.cmd.IdLongCmd; import com.red.circle.order.app.command.pay.PayApplicationQryExe; import com.red.circle.order.app.command.pay.web.PayWebApplicationCommodityV2QueryExe; +import com.red.circle.order.app.command.pay.web.PayWebH5OptionsQryExe; +import com.red.circle.order.app.command.pay.web.PayWebH5PlaceOrderCmdExe; import com.red.circle.order.app.command.pay.web.PayWebOpenPayCountryQueryExe; import com.red.circle.order.app.command.pay.web.UserProfileAndCountryQueryExe; import com.red.circle.order.app.dto.clientobject.pay.ApplicationCommodityCardV2CO; import com.red.circle.order.app.dto.clientobject.pay.PayApplicationCO; import com.red.circle.order.app.dto.clientobject.pay.PayCountryCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5ContextCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5OptionsCO; +import com.red.circle.order.app.dto.clientobject.pay.PayWebH5OrderCO; import com.red.circle.order.app.dto.clientobject.pay.UserProfileAndCountryCO; +import com.red.circle.order.app.dto.cmd.PayWebH5OptionsCmd; +import com.red.circle.order.app.dto.cmd.PayWebH5PlaceOrderCmd; import com.red.circle.order.app.dto.cmd.PayWebApplicationCommodityCmd; import com.red.circle.order.app.dto.cmd.PayWebUserCmd; import java.util.List; @@ -28,6 +35,8 @@ public class WebPayServiceImpl implements WebPayService { private final PayWebOpenPayCountryQueryExe payWebOpenPayCountryQueryExe; private final UserProfileAndCountryQueryExe userProfileAndCountryQueryExe; private final PayWebApplicationCommodityV2QueryExe payWebApplicationCommodityV2QueryExe; + private final PayWebH5OptionsQryExe payWebH5OptionsQryExe; + private final PayWebH5PlaceOrderCmdExe payWebH5PlaceOrderCmdExe; @Override public PayApplicationCO getApplication(IdLongCmd cmd) { @@ -50,4 +59,24 @@ public class WebPayServiceImpl implements WebPayService { return userProfileAndCountryQueryExe.execute(cmd); } + @Override + public PayWebH5ContextCO getH5Context(PayWebUserCmd cmd) { + return payWebH5OptionsQryExe.context(cmd); + } + + @Override + public PayWebH5OptionsCO getH5Options(PayWebH5OptionsCmd cmd) { + return payWebH5OptionsQryExe.options(cmd); + } + + @Override + public PayWebH5OrderCO createH5Order(PayWebH5PlaceOrderCmd cmd) { + return payWebH5PlaceOrderCmdExe.execute(cmd); + } + + @Override + public PayWebH5OrderCO getH5Order(String orderId) { + return payWebH5PlaceOrderCmdExe.getOrder(orderId); + } + } diff --git a/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5AccountCO.java b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5AccountCO.java new file mode 100644 index 00000000..efea6fc1 --- /dev/null +++ b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5AccountCO.java @@ -0,0 +1,48 @@ +package com.red.circle.order.app.dto.clientobject.pay; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.red.circle.framework.dto.ClientObject; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * H5 充值页账号快照,只暴露确认充值对象需要的公开字段。 + * + * @author tf + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class PayWebH5AccountCO extends ClientObject { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @JsonProperty("user_id") + private Long userId; + + @JsonSerialize(using = ToStringSerializer.class) + @JsonProperty("display_user_id") + private Long displayUserId; + + private String username; + + private String avatar; + + private String account; + + @JsonProperty("sys_origin") + private String sysOrigin; + + @JsonProperty("audience_type") + private String audienceType; + + @JsonProperty("freight_agent") + private Boolean freightAgent; + + @JsonProperty("super_freight_agent") + private Boolean superFreightAgent; +} diff --git a/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5ContextCO.java b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5ContextCO.java new file mode 100644 index 00000000..63365ffe --- /dev/null +++ b/rc-service/rc-service-order/order-client/src/main/java/com/red/circle/order/app/dto/clientobject/pay/PayWebH5ContextCO.java @@ -0,0 +1,32 @@ +package com.red.circle.order.app.dto.clientobject.pay; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.red.circle.framework.dto.ClientObject; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * H5 充值页上下文,账号和国家都来自 Aslan 服务端,前端只拿它做展示和后续商品查询条件。 + * + * @author tf + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class PayWebH5ContextCO extends ClientObject { + + private static final long serialVersionUID = 1L; + + @JsonProperty("auth_mode") + private String authMode; + + private PayWebH5AccountCO account; + + @JsonProperty("country_list") + private List