diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/DouyinJsapiController.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/DouyinJsapiController.java new file mode 100644 index 000000000..76e602bfd --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/controller/DouyinJsapiController.java @@ -0,0 +1,140 @@ +package cn.daxpay.open.channel.douyin.controller; + +import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp; +import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppAuthConfig; +import cn.daxpay.open.channel.douyin.param.assist.DouyinJsapiConfigParam; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppAuthConfigService; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService; +import cn.daxpay.open.channel.douyin.strategy.direct.auth.DouyinDirectAuthStrategy; +import cn.daxpay.open.payment.common.context.MerchantContextLoader; +import cn.daxpay.open.payment.device.qrcode.dao.DeviceQrCodeManager; +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import cn.daxpay.open.payment.merchant.enums.ClientEnvEnum; +import cn.daxpay.open.payment.merchant.enums.ClientRuntimeEnum; +import cn.daxpay.open.payment.merchant.enums.CodePayFormEnum; +import cn.daxpay.open.payment.merchant.service.gateway.ClientEnvPayResolveService; +import cn.daxpay.open.payment.merchant.service.gateway.CodePayResolveService; +import cn.daxpay.open.payment.route.service.runtime.PayRouteService; +import cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum; +import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; +import cn.daxpay.open.payment.trade.runtime.service.pay.gateway.GatewayPayAssistService; +import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam; +import cn.daxpay.open.platform.capability.douyin.auth.result.DouyinJsapiConfigResult; +import cn.daxpay.open.platform.capability.douyin.auth.service.DouyinOpenTokenService; +import cn.daxpay.open.platform.core.annotation.IgnoreAuth; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.result.Result; +import cn.hutool.core.util.StrUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +/// # 抖音 H5 JSAPI 调起前置 - sdk.config 验签接口 +/// +/// 签名基于**通道商户网站应用**的 clientKey/appSecret 换取的 jsapi_ticket, +/// 与 H5 OAuth([DouyinDirectAuthStrategy]) 同源。 +/// +/// 上下文三选一: orderNo(网关单) / code(码牌) / channelMchNo(+可选 capability)。 +@IgnoreAuth +@Validated +@Tag(name = "抖音 JSAPI 调起辅助") +@RestController +@RequestMapping("/unipay/assist/channel/douyin") +@RequiredArgsConstructor +public class DouyinJsapiController { + + private final DouyinOpenTokenService douyinOpenTokenService; + private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService; + private final DouyinDirectAppAuthConfigService douyinDirectAppAuthConfigService; + private final GatewayPayAssistService gatewayPayAssistService; + private final DeviceQrCodeManager deviceQrCodeManager; + private final MerchantContextLoader merchantContextLoader; + private final ClientEnvPayResolveService clientEnvPayResolveService; + private final CodePayResolveService codePayResolveService; + private final PayRouteService payRouteService; + + @Operation(summary = "获取抖音 JS-SDK sdk.config 验签包") + @GetMapping("/jsapi-config") + public Result getJsapiConfig(@Valid DouyinJsapiConfigParam param) { + ResolvedChannel ctx = resolveChannelContext( + param.getOrderNo(), param.getCode(), + param.getChannelMchNo(), param.getCapability(), param.getChannelAppId()); + DouyinDirectApp app = douyinDirectAppCapabilityService.resolveWebAppForH5Auth( + ctx.channelMchNo(), ctx.capability(), ctx.channelAppId()); + DouyinDirectAppAuthConfig authConfig = + douyinDirectAppAuthConfigService.findByDouyinDirectAppIdForAuth(app.getId()); + if (StrUtil.isBlank(authConfig.getAppSecret())) { + // 抖音: 直连应用授权密钥未配置 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, "error.channel.douyin.appAuthSecretMissing"); + } + DouyinJsapiConfigResult result = douyinOpenTokenService.buildJsapiConfig( + app.getDouyinAppId(), authConfig.getAppSecret(), param.getUrl()); + return Res.ok(result); + } + + private ResolvedChannel resolveChannelContext(String orderNo, String code, String channelMchNo, + String capability, String channelAppId) { + if (StrUtil.isNotBlank(channelMchNo)) { + return new ResolvedChannel(channelMchNo, capability, channelAppId); + } + if (StrUtil.isNotBlank(orderNo)) { + return resolveFromGatewayOrder(orderNo); + } + if (StrUtil.isNotBlank(code)) { + return resolveFromCodePay(code); + } + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.jsapiContextRequired"); + } + + private ResolvedChannel resolveFromGatewayOrder(String orderNo) { + GatewayPayOrder order = gatewayPayAssistService.getOrderAndCheck(orderNo); + if (Objects.equals(order.getStatus(), GatewayOrderStatusEnum.PAYING.getCode()) + && StrUtil.isNotBlank(order.getChannelMchNo())) { + return new ResolvedChannel(order.getChannelMchNo(), order.getCapability(), order.getChannelAppId()); + } + var resolved = clientEnvPayResolveService.resolveRequired( + order.getAppId(), ClientEnvEnum.DOUYIN, ClientRuntimeEnum.H5); + NormalPayParam routeParam = new NormalPayParam(); + routeParam.setMchNo(order.getMchNo()); + routeParam.setAppId(order.getAppId()); + routeParam.setMethod(resolved.method()); + routeParam.setChannelMchNo(resolved.channelMchNo()); + routeParam.setCapability(resolved.capability()); + payRouteService.resolve(routeParam); + return new ResolvedChannel(routeParam.getChannelMchNo(), routeParam.getCapability(), null); + } + + private ResolvedChannel resolveFromCodePay(String code) { + DeviceQrCode entity = deviceQrCodeManager.findByCode(code) + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")); + if (StrUtil.isBlank(entity.getMchNo())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.device.qrcode.notAssigned"); + } + merchantContextLoader.initMch(entity.getMchNo()); + var mchApp = merchantContextLoader.resolveApp(entity.getMchNo(), entity.getAppId()); + CodePayFormEnum payForm = CodePayFormEnum.fromProgramType(entity.getProgramType()); + var resolved = codePayResolveService.resolveRequired(mchApp.getAppId(), ClientEnvEnum.DOUYIN, payForm); + NormalPayParam routeParam = new NormalPayParam(); + routeParam.setMchNo(entity.getMchNo()); + routeParam.setAppId(mchApp.getAppId()); + routeParam.setMethod(resolved.method()); + routeParam.setChannelMchNo(resolved.channelMchNo()); + routeParam.setCapability(resolved.capability()); + payRouteService.resolve(routeParam); + return new ResolvedChannel(routeParam.getChannelMchNo(), routeParam.getCapability(), null); + } + + private record ResolvedChannel(String channelMchNo, String capability, String channelAppId) { + } +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppAuthConfigManager.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppAuthConfigManager.java index 13f85509c..18312a8ca 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppAuthConfigManager.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppAuthConfigManager.java @@ -2,6 +2,7 @@ package cn.daxpay.open.channel.douyin.dao.direct; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppAuthConfig; import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import cn.daxpay.open.platform.core.annotation.IgnoreTenant; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -20,6 +21,12 @@ public class DouyinDirectAppAuthConfigManager extends BaseManager findByDouyinDirectAppIdNotTenant(Long douyinDirectAppId) { + return findByDouyinDirectAppId(douyinDirectAppId); + } + /// 根据应用ID删除授权认证配置 public void deleteByDouyinDirectAppId(Long douyinDirectAppId) { lambdaUpdate() diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java index 0748119a4..a0e1e600e 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java @@ -2,6 +2,7 @@ package cn.daxpay.open.channel.douyin.dao.direct; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp; import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import cn.daxpay.open.platform.core.annotation.IgnoreTenant; import org.springframework.stereotype.Repository; import java.util.List; @@ -11,6 +12,7 @@ import java.util.Optional; /// /// - 配置态 CRUD: [#listByMchNoAndChannelMchNo]、[#existsByChannelMchNoAndDouyinAppId] /// - 支付/回调(已装载 mchNo): 租户内 [#findFirstByChannelMchNo]、[#findFirstByChannelMchNoAndAppType] +/// - 认证引导(无上下文): 方法名带 NotTenant /// @Repository public class DouyinDirectAppManager extends BaseManager { @@ -56,4 +58,24 @@ public class DouyinDirectAppManager extends BaseManager findByChannelMchNoAndDouyinAppId(String channelMchNo, String douyinAppId) { + return lambdaQuery() + .eq(DouyinDirectApp::getChannelMchNo, channelMchNo) + .eq(DouyinDirectApp::getDouyinAppId, douyinAppId) + .oneOpt(); + } + + /// 按通道商户号与 douyinAppId 查询应用(认证引导,忽略租户) + @IgnoreTenant + public Optional findByChannelMchNoAndDouyinAppIdNotTenant(String channelMchNo, String douyinAppId) { + return findByChannelMchNoAndDouyinAppId(channelMchNo, douyinAppId); + } + + /// 按通道商户号与应用类型取首个应用(认证引导,忽略租户) + @IgnoreTenant + public Optional findFirstByChannelMchNoAndAppTypeNotTenant(String channelMchNo, String appType) { + return findFirstByChannelMchNoAndAppType(channelMchNo, appType); + } + } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/assist/DouyinJsapiConfigParam.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/assist/DouyinJsapiConfigParam.java new file mode 100644 index 000000000..5602d5155 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/param/assist/DouyinJsapiConfigParam.java @@ -0,0 +1,35 @@ +package cn.daxpay.open.channel.douyin.param.assist; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 抖音 JSAPI sdk.config 查询参数 +/// +/// GET `/unipay/assist/channel/douyin/jsapi-config` 的 query 绑定对象。 +/// 上下文三选一: orderNo(网关单) / code(码牌) / channelMchNo(+可选 capability/channelAppId)。 +@Data +@Accessors(chain = true) +@Schema(title = "抖音 JSAPI sdk.config 查询参数") +public class DouyinJsapiConfigParam { + + @NotBlank(message = "{validation.field.url.notBlank}") + @Schema(description = "当前页 URL(不含 hash)") + private String url; + + @Schema(description = "网关订单号") + private String orderNo; + + @Schema(description = "码牌编码") + private String code; + + @Schema(description = "通道商户号") + private String channelMchNo; + + @Schema(description = "能力编码") + private String capability; + + @Schema(description = "通道应用ID") + private String channelAppId; +} diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppAuthConfigService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppAuthConfigService.java index ddca6f6c8..d9956b92d 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppAuthConfigService.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppAuthConfigService.java @@ -5,6 +5,7 @@ import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectAppManager; import cn.daxpay.open.channel.douyin.dao.direct.DouyinDirectAppAuthConfigManager; import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppAuthConfig; import cn.daxpay.open.channel.douyin.param.direct.DouyinDirectAppAuthConfigParam; +import cn.daxpay.open.platform.core.annotation.IgnoreTenant; import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; @@ -43,6 +44,28 @@ public class DouyinDirectAppAuthConfigService { return config; } + /// 根据应用ID查询授权认证配置(运行态认证使用, 忽略租户隔离) + /// + /// 与 [#findByDouyinDirectAppId] 的区别: 走 NotTenant 查询路径, 供认证策略(网关端无登录态)调用; + /// [#save] 等配置态仍调原方法(保留租户隔离)。 + @IgnoreTenant + @Transactional(rollbackFor = Exception.class) + public DouyinDirectAppAuthConfig findByDouyinDirectAppIdForAuth(Long douyinDirectAppId) { + var existing = douyinDirectAppAuthConfigManager.findByDouyinDirectAppIdNotTenant(douyinDirectAppId); + if (existing.isPresent()) { + return existing.get(); + } + var app = douyinDirectAppManager.findByIdNotTenant(douyinDirectAppId) + // 抖音: 直连商户应用不存在 + .orElseThrow(() -> new DataNotExistException("error.channel.douyin.mchAppNotFound")); + var config = new DouyinDirectAppAuthConfig() + .setChannelMchNo(app.getChannelMchNo()) + .setDouyinDirectAppId(douyinDirectAppId); + config.setMchNo(app.getMchNo()); + douyinDirectAppAuthConfigManager.save(config); + return config; + } + /// 保存应用授权认证配置(更新) @Transactional(rollbackFor = Exception.class) public void save(DouyinDirectAppAuthConfigParam param) { diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java index b56375829..0bf8793e0 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/service/direct/DouyinDirectAppCapabilityService.java @@ -136,6 +136,29 @@ public class DouyinDirectAppCapabilityService { return resolveApp(channelMchNo, capability); } + /// H5 silent_auth / JS-SDK 验签用网站应用解析(忽略租户) + /// + /// 优先级: channelAppId 显式 > capability 命中且为 web_app > 通道商户首个 web_app。 + /// 勿盲跟 DOUYIN_JSAPI→mini_program 推导。 + @IgnoreTenant + public DouyinDirectApp resolveWebAppForH5Auth(String channelMchNo, String capability, String channelAppId) { + if (StrUtil.isNotBlank(channelAppId)) { + return douyinDirectAppManager.findByChannelMchNoAndDouyinAppIdNotTenant(channelMchNo, channelAppId) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.channelAppIdNotFound", channelAppId)); + } + if (StrUtil.isNotBlank(capability)) { + var byCap = resolveAppNotTenant(channelMchNo, capability); + if (byCap.isPresent() && DouyinAppTypeCode.WEB_APP.equals(byCap.get().getAppType())) { + return byCap.get(); + } + } + return douyinDirectAppManager.findFirstByChannelMchNoAndAppTypeNotTenant( + channelMchNo, DouyinAppTypeCode.WEB_APP) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.channel.douyin.webAppNotFound")); + } + /// 查询抖音直连产品支持的支付能力候选列表(含国际化名称) public List listSupportedCapabilities() { AbsProductStrategy strategy = PaymentStrategyFactory.createByProduct( diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java new file mode 100644 index 000000000..4c547f0d5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/strategy/direct/auth/DouyinDirectAuthStrategy.java @@ -0,0 +1,98 @@ +package cn.daxpay.open.channel.douyin.strategy.direct.auth; + +import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectApp; +import cn.daxpay.open.channel.douyin.entity.direct.DouyinDirectAppAuthConfig; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppAuthConfigService; +import cn.daxpay.open.channel.douyin.service.direct.DouyinDirectAppCapabilityService; +import cn.daxpay.open.payment.auth.AuthSession; +import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; +import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; +import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; +import cn.daxpay.open.payment.unipay.result.assist.AuthResult; +import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; +import cn.daxpay.open.platform.capability.douyin.auth.result.DouyinAuthResult; +import cn.daxpay.open.platform.capability.douyin.auth.service.DouyinH5AuthService; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.code.DaxPayErrorCode; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.daxpay.open.platform.system.service.config.infra.PlatformUrlConfigService; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 抖音直连认证策略 +/// +/// 抖音直连模式(DOUYIN_PAY)下获取用户标识(openId)。复用支付的商户配置体系定位直连应用 +/// (DouyinDirectApp + DouyinDirectAppAuthConfig), 调用 capability-douyin 完成 H5 silent_auth。 +/// +/// H5 silent_auth 强制解析网站应用, 见 [DouyinDirectAppCapabilityService#resolveWebAppForH5Auth]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class DouyinDirectAuthStrategy extends AbsChannelAuthStrategy { + + private final DouyinH5AuthService douyinH5AuthService; + private final DouyinDirectAppCapabilityService douyinDirectAppCapabilityService; + private final DouyinDirectAppAuthConfigService douyinDirectAppAuthConfigService; + private final PlatformUrlConfigService platformUrlConfigService; + + @Override + public ProductEnum getProduct() { + return ProductEnum.DOUYIN_PAY; + } + + /// 生成抖音 H5 静默授权链接 + @Override + public AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param, String authToken) { + DouyinDirectApp app = douyinDirectAppCapabilityService.resolveWebAppForH5Auth( + param.getChannelMchNo(), param.getCapability(), param.getChannelAppId()); + DouyinDirectAppAuthConfig authConfig = douyinDirectAppAuthConfigService.findByDouyinDirectAppIdForAuth(app.getId()); + if (StrUtil.isBlank(authConfig.getAppSecret())) { + // 抖音: 直连应用授权密钥未配置 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, "error.channel.douyin.appAuthSecretMissing"); + } + String redirectUri = buildRedirectUri(); + String authUrl = douyinH5AuthService.buildSilentAuthUrl( + app.getDouyinAppId(), redirectUri, authToken); + return new AuthUrlResult().setAuthUrl(authUrl); + } + + /// 通过授权 code 换取 openId + @Override + public AuthResult doAuth(AuthCodeParam param, AuthSession session) { + String channelMchNo = session != null && StrUtil.isNotBlank(session.getChannelMchNo()) + ? session.getChannelMchNo() : param.getChannelMchNo(); + String capability = session != null && StrUtil.isNotBlank(session.getCapability()) + ? session.getCapability() : param.getCapability(); + String channelAppId = session != null && StrUtil.isNotBlank(session.getChannelAppId()) + ? session.getChannelAppId() : param.getChannelAppId(); + DouyinDirectApp app = douyinDirectAppCapabilityService.resolveWebAppForH5Auth( + channelMchNo, capability, channelAppId); + DouyinDirectAppAuthConfig authConfig = douyinDirectAppAuthConfigService.findByDouyinDirectAppIdForAuth(app.getId()); + if (StrUtil.isBlank(authConfig.getAppSecret())) { + // 抖音: 直连应用授权密钥未配置 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, "error.channel.douyin.appAuthSecretMissing"); + } + DouyinAuthResult data = douyinH5AuthService.getOpenIdByCode( + app.getDouyinAppId(), authConfig.getAppSecret(), param.getAuthCode()); + if (StrUtil.isBlank(data.getOpenId())) { + // 抖音: 获取openId失败 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, "error.channel.douyin.authFailed", "openId is blank"); + } + return new AuthResult() + .setOpenId(data.getOpenId()) + .setAccessToken(data.getAccessToken()); + } + + /// 拼接认证回调地址: {paymentGatewayBaseUrl}/auth/douyin + private String buildRedirectUri() { + String base = platformUrlConfigService.getUrlConfig().getPaymentGatewayBaseUrl(); + if (StrUtil.isBlank(base)) { + // 支付网关前端地址未配置 + throw new BizInfoException(DaxPayErrorCode.CONFIG_ERROR, "error.common.gatewayUrlNotConfigured"); + } + return StrUtil.removeSuffix(base, "/") + "/auth/douyin"; + } +} diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthController.java index 807955e2a..90c3dd213 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthController.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthController.java @@ -66,7 +66,7 @@ public class DevelopAuthController { @PostMapping("/generate-channel-auth-url") public Result generateChannelAuthUrl(@RequestBody GenerateAuthUrlParam param) { // 不加 @Valid: GenerateAuthUrlParam 继承 PaymentCommonParam.reqTime(@NotNull), 但认证不走签名/防重放, 无需 reqTime; - // channel/mchNo 由 ChannelAuthService 业务层兜底校验, 与 unipay ChannelAuthController 同类接口保持一致 + // channel/mchNo 由 ChannelProductAuthService 业务层兜底校验, 与 unipay ChannelAuthController 同类接口保持一致 return Res.ok(developAuthService.generateChannelAuthUrl(param)); } diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopAuthService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopAuthService.java index f011129c7..f751975eb 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopAuthService.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/develop/DevelopAuthService.java @@ -1,7 +1,7 @@ package cn.daxpay.open.payment.admin.service.develop; import cn.daxpay.open.payment.auth.AuthSessionStore; -import cn.daxpay.open.payment.auth.ChannelAuthService; +import cn.daxpay.open.payment.auth.ChannelProductAuthService; import cn.daxpay.open.payment.auth.PlatformAuthService; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; @@ -16,7 +16,7 @@ import org.springframework.stereotype.Service; /// - **支付宝(平台级)**: 委托 [PlatformAuthService] 生成 OAuth 授权链接, 轮询 queryCode 取结果 /// - **微信公众号配置(平台级)**: 委托 [PlatformAuthService], OAuth 重定向取 openId, 仅验证配置是否正确 /// - **抖音H5(平台级)**: 委托 [PlatformAuthService], silent_auth 静默授权取 openId, 仅验证配置是否正确 -/// - **微信支付(直连/服务商)**: 委托 [ChannelAuthService] 按支付产品路由认证策略, +/// - **微信支付(直连/服务商)**: 委托 [ChannelProductAuthService] 按支付产品路由认证策略, /// 依赖商户上下文(channelMchNo/产品/能力) /// - **支付宝小程序**: 暂未实现 /// - **微信小程序(商户端/运营端)**: 暂未实现 @@ -28,7 +28,7 @@ import org.springframework.stereotype.Service; public class DevelopAuthService { private final PlatformAuthService platformAuthService; - private final ChannelAuthService channelAuthService; + private final ChannelProductAuthService channelProductAuthService; private final AuthSessionStore authSessionStore; /// 生成支付宝授权链接(平台级 OAuth + queryCode 轮询) @@ -48,10 +48,10 @@ public class DevelopAuthService { /// 生成微信支付(直连/服务商)授权链接 /// - /// 委托 [ChannelAuthService#generateAuthUrl], 按支付产品路由对应认证策略: + /// 委托 [ChannelProductAuthService#generateAuthUrl], 按支付产品路由对应认证策略: /// 直连(WECHAT_PAY) → WechatDirectAuthStrategy; 服务商(WECHAT_ISV) → WechatIsvAuthStrategy。 public AuthUrlResult generateChannelAuthUrl(GenerateAuthUrlParam param) { - return channelAuthService.generateAuthUrl(param); + return channelProductAuthService.generateAuthUrl(param); } /// 通过查询码获取认证结果 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSession.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSession.java index 34aa2bc1e..a14a36e03 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSession.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSession.java @@ -7,7 +7,7 @@ import lombok.experimental.Accessors; /// /// H5授权重定向场景下, 生成授权链接时将认证所需上下文序列化保存到Redis(以 authToken 为key), /// 授权回调后凭 authToken 恢复, 供认证策略定位通道应用并完成 code 换 openId/userId。 -/// 与 [ChannelAuthService] 的 queryCode 机制(付款码/道通场景)解耦, 独立 key 前缀管理。 +/// 与 [ChannelProductAuthService] 的 queryCode 机制(付款码/道通场景)解耦, 独立 key 前缀管理。 @Data @Accessors(chain = true) public class AuthSession { diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSessionStore.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSessionStore.java index 916e58157..b4c323609 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSessionStore.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/AuthSessionStore.java @@ -15,8 +15,8 @@ import java.util.Objects; /// # 认证会话与结果缓存 /// /// 统一管理通道认证/平台级认证共用的会话上下文(authToken)与轮询结果(queryCode)的 Redis 读写, -/// 与具体认证来源(通道商户策略 / 平台级配置)解耦, 供 [ChannelAuthService]、[PlatformAuthService]、 -/// [ChannelAuthFacade] 及调试入口(DevelopAuthService)复用。 +/// 与具体认证来源(通道商户策略 / 平台级配置)解耦, 供 [ChannelAuthService]、[ChannelProductAuthService]、 +/// [PlatformAuthService] 及调试入口(DevelopAuthService)复用。 /// 授权成功后由 Facade 调用 [#deleteSession] 使 authToken 一次使用失效。 @Slf4j @Service diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthFacade.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthFacade.java deleted file mode 100644 index 856ead491..000000000 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthFacade.java +++ /dev/null @@ -1,103 +0,0 @@ -package cn.daxpay.open.payment.auth; - -import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; -import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; -import cn.daxpay.open.payment.unipay.result.assist.AuthResult; -import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; -import cn.daxpay.open.platform.core.code.DaxPayErrorCode; -import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthTypeEnum; -import cn.daxpay.open.platform.core.exception.BizInfoException; -import cn.hutool.core.util.StrUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Objects; - -/// # 通道认证分发门面 -/// -/// 统一对外的「生成授权链接 / 授权码换用户标识」入口, 按会话来源与 authType 分流到 -/// [PlatformAuthService](平台级配置) 或 [ChannelAuthService](商户级产品策略)。 -/// Controller / 调试入口只做协议适配, 不在 Web 层写业务分支。 -/// -/// ## 分发优先级(auth) -/// 1. session.source = platform_alipay / platform_mp / platform_douyin → 平台服务对应方法 -/// 2. authType=alipay 且无 session → 平台支付宝(小程序等直连兜底) -/// 3. 其余 → 通道策略 -@Slf4j -@Service -@RequiredArgsConstructor -public class ChannelAuthFacade { - - private final AuthSessionStore authSessionStore; - private final PlatformAuthService platformAuthService; - private final ChannelAuthService channelAuthService; - - /// 生成授权链接: 支付宝走平台级 OAuth, 其余按支付产品走通道策略 - public AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param) { - if (isAlipayAuth(param.getAuthType())) { - // 透传 returnPath, 供码牌等业务页授权完成后回跳 - return platformAuthService.generateAlipayAuthUrl(param.getReturnPath()); - } - return channelAuthService.generateAuthUrl(param); - } - - /// 通过 AuthCode 换取认证结果, 成功后销毁会话(一次使用) - public AuthResult auth(AuthCodeParam param) { - AuthSession session = authSessionStore.loadSession(param.getAuthToken()); - // 会话已失效(且非支付宝直连兜底场景): 提示重新生成, 避免下游抛"不支持的能力: null" - // 支付宝平台级 OAuth 不依赖 session 字段, 由 doAuth 内的兜底分支处理, 保持原行为 - if (session == null && !isAlipayAuth(param.getAuthType())) { - // 授权链接已失效, 请重新生成 - throw new BizInfoException(DaxPayErrorCode.OPERATION_FAIL, - "pay.error.assist.authSessionExpired"); - } - AuthResult result = doAuth(param, session); - // 平台级 auth 方法未回填 returnPath 时, 从会话补齐 - if (session != null && StrUtil.isNotBlank(session.getReturnPath()) - && StrUtil.isBlank(result.getReturnPath())) { - result.setReturnPath(session.getReturnPath()); - } - // 成功后失效 authToken, 避免 TTL 内重复消费会话上下文 - authSessionStore.deleteSession(param.getAuthToken()); - return result; - } - - /// 按会话来源把授权码回调分到平台或通道处理 - private AuthResult doAuth(AuthCodeParam param, AuthSession session) { - if (isPlatformAlipay(session)) { - return platformAuthService.authAlipay(param, session); - } - if (isPlatformMp(session)) { - return platformAuthService.authWechatMp(param, session); - } - if (isPlatformDouyin(session)) { - return platformAuthService.authDouyin(param, session); - } - // 无会话且 authType=alipay: 小程序等直连场景兜底 - if (isAlipayAuth(param.getAuthType()) && session == null) { - return platformAuthService.authAlipay(param, null); - } - return channelAuthService.auth(param, session); - } - - /// 是否支付宝认证类型(平台级支付宝走 OAuth) - private boolean isAlipayAuth(String authType) { - return Objects.equals(authType, ChannelAuthTypeEnum.ALIPAY.getCode()); - } - - /// 是否支付宝平台级配置来源 - private boolean isPlatformAlipay(AuthSession session) { - return session != null && AuthSession.SOURCE_PLATFORM_ALIPAY.equals(session.getSource()); - } - - /// 是否微信系统公众号配置来源(平台级) - private boolean isPlatformMp(AuthSession session) { - return session != null && AuthSession.SOURCE_PLATFORM_MP.equals(session.getSource()); - } - - /// 是否抖音 H5 应用配置来源(平台级) - private boolean isPlatformDouyin(AuthSession session) { - return session != null && AuthSession.SOURCE_PLATFORM_DOUYIN.equals(session.getSource()); - } -} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthService.java index f78d58bcb..fe08bf541 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelAuthService.java @@ -1,115 +1,103 @@ package cn.daxpay.open.payment.auth; -import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager; -import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant; -import cn.daxpay.open.payment.common.context.MerchantContextLoader; -import cn.daxpay.open.payment.strategy.PaymentStrategyFactory; -import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; -import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthStatusEnum; +import cn.daxpay.open.platform.core.code.DaxPayErrorCode; +import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthTypeEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -/// # 通道认证服务(商户级) +import java.util.Objects; + +/// # 通道认证服务 /// -/// 负责按支付产品路由认证策略(继承 [AbsChannelAuthStrategy]), 依赖商户上下文定位通道应用, -/// 获取支付所需的用户标识(微信 openId / 支付宝 userId)。H5 授权重定向场景生成 authToken 保存会话。 +/// 统一对外的「生成授权链接 / 授权码换用户标识」入口, 按会话来源与 authType 分流到 +/// [PlatformAuthService](平台级配置) 或 [ChannelProductAuthService](商户级产品策略)。 +/// Controller / 调试入口只做协议适配, 不在 Web 层写业务分支。 /// -/// **职责边界**: 本服务仅处理商户级通道认证; 平台级认证(平台支付宝配置 / 系统公众号配置) -/// 由 [PlatformAuthService] 承担, 会话与结果缓存由 [AuthSessionStore] 统一管理。 -/// 平台级 vs 通道级 的来源分发由 [ChannelAuthFacade] 完成, 请勿在 Controller 再写分流。 +/// ## 分发优先级(auth) +/// 1. session.source = platform_alipay / platform_mp / platform_douyin → 平台服务对应方法 +/// 2. authType=alipay 且无 session → 平台支付宝(小程序等直连兜底) +/// 3. 其余 → 通道产品策略 @Slf4j @Service @RequiredArgsConstructor public class ChannelAuthService { private final AuthSessionStore authSessionStore; - private final MerchantContextLoader merchantContextLoader; - private final ChannelMerchantManager channelMerchantManager; + private final PlatformAuthService platformAuthService; + private final ChannelProductAuthService channelProductAuthService; - /// 获取通道授权链接 - /// - /// 生成 authToken 并委托产品策略([AbsChannelAuthStrategy])生成授权 URL, 会话随 authToken 保存, - /// 授权回调后凭此恢复上下文。同时生成 queryCode 供调试轮询(微信等 OAuth 重定向通道回调 URL - /// 不含 queryCode, 需随会话保存)。 + /// 生成授权链接: 支付宝走平台级 OAuth, 其余按支付产品走通道策略 public AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param) { - initMchContext(param.getAppId(), param.getMchNo()); - // 支付产品: 显式传入优先, 否则从通道商户号反查(调试工具/直接指定通道商户场景) - String product = resolveProduct(param); - var strategy = PaymentStrategyFactory.createByProduct(product, AbsChannelAuthStrategy.class); - // 生成认证会话码并保存上下文, 授权回调后凭此恢复 - String authToken = IdUtil.fastSimpleUUID(); - // 生成 queryCode 供调试轮询(微信等 OAuth 重定向通道回调 URL 不含 queryCode, 需随会话保存) - String queryCode = RandomUtil.randomString(10); - AuthSession session = new AuthSession() - .setProduct(product) - .setChannelMchNo(param.getChannelMchNo()) - .setCapability(param.getCapability()) - .setChannelAppId(param.getChannelAppId()) - .setReturnPath(param.getReturnPath()) - .setQueryCode(queryCode); - authSessionStore.saveSession(authToken, session); - AuthUrlResult authUrlResult = strategy.generateAuthUrl(param, authToken); - // 回填 queryCode 并写入 WAITING 状态供前端轮询 - authUrlResult.setQueryCode(queryCode); - authSessionStore.saveWaitingResult(queryCode); - return authUrlResult; + if (isAlipayAuth(param.getAuthType())) { + // 透传 returnPath, 供码牌等业务页授权完成后回跳 + return platformAuthService.generateAlipayAuthUrl(param.getReturnPath()); + } + return channelProductAuthService.generateAuthUrl(param); } - /// 通过AuthCode获取通道认证结果 - /// - /// @param session 认证会话上下文(H5场景从 authToken 恢复; 小程序直连场景可为空, 此时从 param 取上下文)。 - /// 由认证分发层在调用前通过 [AuthSessionStore#loadSession] 加载后注入。 - public AuthResult auth(AuthCodeParam param, AuthSession session) { - initMchContext(param.getAppId(), param.getMchNo()); - // product 优先从会话恢复, 其次取参数(小程序直连场景) - String product = (session != null && StrUtil.isNotBlank(session.getProduct())) - ? session.getProduct() : param.getProduct(); - var strategy = PaymentStrategyFactory.createByProduct(product, AbsChannelAuthStrategy.class); - AuthResult authResult = strategy.doAuth(param, session); - authResult.setStatus(ChannelAuthStatusEnum.SUCCESS.getCode()); - // 会话恢复场景: 回填来源回跳路径, 供前端跳回业务页面 - if (session != null) { - authResult.setReturnPath(session.getReturnPath()); + /// 通过 AuthCode 换取认证结果, 成功后销毁会话(一次使用) + public AuthResult auth(AuthCodeParam param) { + AuthSession session = authSessionStore.loadSession(param.getAuthToken()); + // 会话已失效(且非支付宝直连兜底场景): 提示重新生成, 避免下游抛"不支持的能力: null" + // 支付宝平台级 OAuth 不依赖 session 字段, 由 doAuth 内的兜底分支处理, 保持原行为 + if (session == null && !isAlipayAuth(param.getAuthType())) { + // 授权链接已失效, 请重新生成 + throw new BizInfoException(DaxPayErrorCode.OPERATION_FAIL, + "pay.error.assist.authSessionExpired"); } - // 写回轮询结果(微信等 OAuth 重定向通道从 session 恢复 queryCode) - authSessionStore.writeResultByQueryCode(param.getQueryCode(), session, authResult); - return authResult; + AuthResult result = doAuth(param, session); + // 平台级 auth 方法未回填 returnPath 时, 从会话补齐 + if (session != null && StrUtil.isNotBlank(session.getReturnPath()) + && StrUtil.isBlank(result.getReturnPath())) { + result.setReturnPath(session.getReturnPath()); + } + // 成功后失效 authToken, 避免 TTL 内重复消费会话上下文 + authSessionStore.deleteSession(param.getAuthToken()); + return result; } - /// 商户上下文初始化: appId 优先(渠道配置/小程序直连), appId 为空则用 mchNo(调试/直接指定通道商户场景), - /// 两者均空时跳过(微信 OAuth 重定向回调仅含 authToken, 商户上下文由 session.channelMchNo 维度定位, 无需线程级 mchNo)。 - private void initMchContext(String appId, String mchNo) { - if (StrUtil.isNotBlank(appId)) { - merchantContextLoader.initMchByApp(appId); - } else if (StrUtil.isNotBlank(mchNo)) { - merchantContextLoader.initMch(mchNo); + /// 按会话来源把授权码回调分到平台或通道产品处理 + private AuthResult doAuth(AuthCodeParam param, AuthSession session) { + if (isPlatformAlipay(session)) { + return platformAuthService.authAlipay(param, session); } + if (isPlatformMp(session)) { + return platformAuthService.authWechatMp(param, session); + } + if (isPlatformDouyin(session)) { + return platformAuthService.authDouyin(param, session); + } + // 无会话且 authType=alipay: 小程序等直连场景兜底 + if (isAlipayAuth(param.getAuthType()) && session == null) { + return platformAuthService.authAlipay(param, null); + } + return channelProductAuthService.auth(param, session); } - /// 解析支付产品: 显式传入优先, 否则从通道商户号(channelMchNo)反查所属产品 - private String resolveProduct(GenerateAuthUrlParam param) { - if (StrUtil.isNotBlank(param.getProduct())) { - return param.getProduct(); - } - // 缺失 product 时必须提供 channelMchNo 才能反查 - if (StrUtil.isBlank(param.getChannelMchNo())) { - // 支付产品与通道商户号至少传其一 - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.error.assist.productOrChannelMchRequired"); - } - return channelMerchantManager.findByChannelMchNo(param.getChannelMchNo()) - .map(ChannelMerchant::getProduct) - .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "pay.error.assist.channelMchNotFound", param.getChannelMchNo())); + /// 是否支付宝认证类型(平台级支付宝走 OAuth) + private boolean isAlipayAuth(String authType) { + return Objects.equals(authType, ChannelAuthTypeEnum.ALIPAY.getCode()); + } + + /// 是否支付宝平台级配置来源 + private boolean isPlatformAlipay(AuthSession session) { + return session != null && AuthSession.SOURCE_PLATFORM_ALIPAY.equals(session.getSource()); + } + + /// 是否微信系统公众号配置来源(平台级) + private boolean isPlatformMp(AuthSession session) { + return session != null && AuthSession.SOURCE_PLATFORM_MP.equals(session.getSource()); + } + + /// 是否抖音 H5 应用配置来源(平台级) + private boolean isPlatformDouyin(AuthSession session) { + return session != null && AuthSession.SOURCE_PLATFORM_DOUYIN.equals(session.getSource()); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelProductAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelProductAuthService.java new file mode 100644 index 000000000..1afe3e1d9 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/ChannelProductAuthService.java @@ -0,0 +1,115 @@ +package cn.daxpay.open.payment.auth; + +import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager; +import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant; +import cn.daxpay.open.payment.common.context.MerchantContextLoader; +import cn.daxpay.open.payment.strategy.PaymentStrategyFactory; +import cn.daxpay.open.payment.strategy.auth.AbsChannelAuthStrategy; +import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; +import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; +import cn.daxpay.open.payment.unipay.result.assist.AuthResult; +import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthStatusEnum; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 通道产品认证服务(商户级) +/// +/// 负责按支付产品路由认证策略(继承 [AbsChannelAuthStrategy]), 依赖商户上下文定位通道应用, +/// 获取支付所需的用户标识(微信 openId / 支付宝 userId)。H5 授权重定向场景生成 authToken 保存会话。 +/// +/// **职责边界**: 本服务仅处理商户级通道认证; 平台级认证(平台支付宝配置 / 系统公众号配置) +/// 由 [PlatformAuthService] 承担, 会话与结果缓存由 [AuthSessionStore] 统一管理。 +/// 平台级 vs 通道级 的来源分发由 [ChannelAuthService] 完成, 请勿在 Controller 再写分流。 +@Slf4j +@Service +@RequiredArgsConstructor +public class ChannelProductAuthService { + + private final AuthSessionStore authSessionStore; + private final MerchantContextLoader merchantContextLoader; + private final ChannelMerchantManager channelMerchantManager; + + /// 获取通道授权链接 + /// + /// 生成 authToken 并委托产品策略([AbsChannelAuthStrategy])生成授权 URL, 会话随 authToken 保存, + /// 授权回调后凭此恢复上下文。同时生成 queryCode 供调试轮询(微信等 OAuth 重定向通道回调 URL + /// 不含 queryCode, 需随会话保存)。 + public AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param) { + initMchContext(param.getAppId(), param.getMchNo()); + // 支付产品: 显式传入优先, 否则从通道商户号反查(调试工具/直接指定通道商户场景) + String product = resolveProduct(param); + var strategy = PaymentStrategyFactory.createByProduct(product, AbsChannelAuthStrategy.class); + // 生成认证会话码并保存上下文, 授权回调后凭此恢复 + String authToken = IdUtil.fastSimpleUUID(); + // 生成 queryCode 供调试轮询(微信等 OAuth 重定向通道回调 URL 不含 queryCode, 需随会话保存) + String queryCode = RandomUtil.randomString(10); + AuthSession session = new AuthSession() + .setProduct(product) + .setChannelMchNo(param.getChannelMchNo()) + .setCapability(param.getCapability()) + .setChannelAppId(param.getChannelAppId()) + .setReturnPath(param.getReturnPath()) + .setQueryCode(queryCode); + authSessionStore.saveSession(authToken, session); + AuthUrlResult authUrlResult = strategy.generateAuthUrl(param, authToken); + // 回填 queryCode 并写入 WAITING 状态供前端轮询 + authUrlResult.setQueryCode(queryCode); + authSessionStore.saveWaitingResult(queryCode); + return authUrlResult; + } + + /// 通过AuthCode获取通道认证结果 + /// + /// @param session 认证会话上下文(H5场景从 authToken 恢复; 小程序直连场景可为空, 此时从 param 取上下文)。 + /// 由认证分发层在调用前通过 [AuthSessionStore#loadSession] 加载后注入。 + public AuthResult auth(AuthCodeParam param, AuthSession session) { + initMchContext(param.getAppId(), param.getMchNo()); + // product 优先从会话恢复, 其次取参数(小程序直连场景) + String product = (session != null && StrUtil.isNotBlank(session.getProduct())) + ? session.getProduct() : param.getProduct(); + var strategy = PaymentStrategyFactory.createByProduct(product, AbsChannelAuthStrategy.class); + AuthResult authResult = strategy.doAuth(param, session); + authResult.setStatus(ChannelAuthStatusEnum.SUCCESS.getCode()); + // 会话恢复场景: 回填来源回跳路径, 供前端跳回业务页面 + if (session != null) { + authResult.setReturnPath(session.getReturnPath()); + } + // 写回轮询结果(微信等 OAuth 重定向通道从 session 恢复 queryCode) + authSessionStore.writeResultByQueryCode(param.getQueryCode(), session, authResult); + return authResult; + } + + /// 商户上下文初始化: appId 优先(渠道配置/小程序直连), appId 为空则用 mchNo(调试/直接指定通道商户场景), + /// 两者均空时跳过(微信 OAuth 重定向回调仅含 authToken, 商户上下文由 session.channelMchNo 维度定位, 无需线程级 mchNo)。 + private void initMchContext(String appId, String mchNo) { + if (StrUtil.isNotBlank(appId)) { + merchantContextLoader.initMchByApp(appId); + } else if (StrUtil.isNotBlank(mchNo)) { + merchantContextLoader.initMch(mchNo); + } + } + + /// 解析支付产品: 显式传入优先, 否则从通道商户号(channelMchNo)反查所属产品 + private String resolveProduct(GenerateAuthUrlParam param) { + if (StrUtil.isNotBlank(param.getProduct())) { + return param.getProduct(); + } + // 缺失 product 时必须提供 channelMchNo 才能反查 + if (StrUtil.isBlank(param.getChannelMchNo())) { + // 支付产品与通道商户号至少传其一 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.assist.productOrChannelMchRequired"); + } + return channelMerchantManager.findByChannelMchNo(param.getChannelMchNo()) + .map(ChannelMerchant::getProduct) + .orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.assist.channelMchNotFound", param.getChannelMchNo())); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/PlatformAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/PlatformAuthService.java index b6b74c360..b8f1cae78 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/PlatformAuthService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/PlatformAuthService.java @@ -15,6 +15,7 @@ import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.code.DaxPayErrorCode; import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthStatusEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.daxpay.open.platform.system.entity.config.platform.auth.PlatformAlipayAuthConfig; import cn.daxpay.open.platform.system.entity.config.platform.auth.PlatformDouyinH5AuthConfig; import cn.daxpay.open.platform.system.entity.config.platform.auth.PlatformWechatMpAuthConfig; import cn.daxpay.open.platform.system.service.config.auth.PlatformAlipayAuthConfigService; @@ -33,12 +34,13 @@ import org.springframework.stereotype.Service; /// 承载不依赖商户上下文、由平台级配置驱动的认证场景: /// - **支付宝**: 平台级支付宝配置, OAuth 重定向, 调试/支付共用; URL 拼装见 [#buildAlipayAuthUrl], /// 通道策略 [cn.daxpay.open.payment.strategy.auth.AlipayAuthStrategy] 亦委托本服务, 避免双轨实现 -/// - **微信系统公众号配置**: 平台级微信公众号配置([PlatformWechatMpAuthConfig]), OAuth 重定向, 仅调试场景 -/// - **抖音 H5 应用**: 平台级抖音 H5 配置([PlatformDouyinH5AuthConfig]), silent_auth 静默授权, 仅调试场景 +/// - **微信系统公众号配置**: 平台级微信公众号配置([PlatformWechatMpAuthConfig]), OAuth 重定向, **仅调试场景** +/// (网关聚合/收银台/码牌支付走通道应用策略, 不再消费本配置) +/// - **抖音 H5 应用**: 平台级抖音 H5 配置([PlatformDouyinH5AuthConfig]), silent_auth 静默授权, **仅调试场景** /// -/// 与按支付产品路由策略的 [ChannelAuthService] 解耦: 本服务不读取商户级通道配置, +/// 与按支付产品路由策略的 [ChannelProductAuthService] 解耦: 本服务不读取商户级通道配置, /// 只消费平台级配置并调用 capability(alipay/wechat/douyin) 用授权码换 token/openId; 会话与结果缓存委托 [AuthSessionStore]。 -/// 对外分发入口见 [ChannelAuthFacade]。 +/// 对外分发入口见 [ChannelAuthService]。 /// /// 三通道统一模式: 固定 redirect_uri + OAuth state 透传 authToken, 回调后从 state 恢复会话。 @Slf4j @@ -105,7 +107,7 @@ public class PlatformAuthService { /// 仅拼装支付宝 OAuth 授权 URL(不创建会话) /// /// 供 [cn.daxpay.open.payment.strategy.auth.AlipayAuthStrategy] 复用: 通道侧已由 - /// [ChannelAuthService] 创建 session/queryCode, 策略层只负责拼 URL, 避免与平台路径双份实现。 + /// [ChannelProductAuthService] 创建 session/queryCode, 策略层只负责拼 URL, 避免与平台路径双份实现。 public String buildAlipayAuthUrl(String authToken) { AlipayAuthConfig config = platformAlipayAuthConfigService.toCapabilityConfig(); if (!alipayAuthCapability.isConfigured(config)) { diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AbsChannelAuthStrategy.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AbsChannelAuthStrategy.java index 51e719aee..24d91c31e 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AbsChannelAuthStrategy.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AbsChannelAuthStrategy.java @@ -1,6 +1,7 @@ package cn.daxpay.open.payment.strategy.auth; import cn.daxpay.open.payment.auth.AuthSession; +import cn.daxpay.open.payment.auth.ChannelProductAuthService; import cn.daxpay.open.payment.strategy.PaymentStrategy; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; @@ -15,7 +16,7 @@ public abstract class AbsChannelAuthStrategy implements PaymentStrategy { /// 获取授权链接 /// - /// @param authToken 认证会话码, 由上层 [ChannelAuthService] 生成注入, + /// @param authToken 认证会话码, 由上层 [ChannelProductAuthService] 生成注入, /// 策略负责将其拼入回调地址; 授权回跳时凭此恢复上下文。 public abstract AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param, String authToken); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AlipayAuthStrategy.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AlipayAuthStrategy.java index 611280477..61090c3c0 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AlipayAuthStrategy.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/strategy/auth/AlipayAuthStrategy.java @@ -18,11 +18,11 @@ import org.springframework.stereotype.Service; /// /// ## 适用场景 /// - 支付场景获取支付宝 userId(如 ALIPAY_JSAPI 需要) -/// - 经 [ChannelAuthService] 按 product=ALIPAY 路由时的 H5 OAuth / 小程序直连 +/// - 经 [ChannelProductAuthService] 按 product=ALIPAY 路由时的 H5 OAuth / 小程序直连 /// /// ## 回调机制 /// 与微信/抖音策略同构: 回调地址固定为 `{paymentGatewayBaseUrl}/auth/alipay`, -/// 会话标识 authToken 通过 OAuth state 参数透传(会话由 [ChannelAuthService] 管理)。 +/// 会话标识 authToken 通过 OAuth state 参数透传(会话由 [ChannelProductAuthService] 管理)。 @Slf4j @Service @RequiredArgsConstructor @@ -35,7 +35,7 @@ public class AlipayAuthStrategy extends AbsChannelAuthStrategy { return ProductEnum.ALIPAY; } - /// 生成支付宝授权链接(委托平台服务拼 URL; session/queryCode 已由 ChannelAuthService 创建) + /// 生成支付宝授权链接(委托平台服务拼 URL; session/queryCode 已由 ChannelProductAuthService 创建) @Override public AuthUrlResult generateAuthUrl(GenerateAuthUrlParam param, String authToken) { return new AuthUrlResult().setAuthUrl(platformAuthService.buildAlipayAuthUrl(authToken)); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java index b93a9d139..a2e720c2b 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/mq/PayArtemisConstants.java @@ -1,5 +1,8 @@ package cn.daxpay.open.payment.trade.runtime.mq; +import cn.daxpay.open.payment.trade.runtime.consumer.GatewayTimeoutConsumer; +import cn.daxpay.open.payment.trade.runtime.consumer.NormalPayTimeoutConsumer; + /// # 支付业务 Artemis 消息地址常量 /// /// 支付核心业务使用的 Artemis address 常量,地址命名遵循 kebab-case 约定。 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayAuthService.java index 0e70ac4d1..97720febd 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayAuthService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayAuthService.java @@ -1,46 +1,234 @@ package cn.daxpay.open.payment.trade.runtime.service.pay.gateway; -import cn.daxpay.open.payment.auth.PlatformAuthService; +import cn.daxpay.open.payment.auth.ChannelAuthService; +import cn.daxpay.open.payment.merchant.dao.gateway.GatewayCashierItemManager; +import cn.daxpay.open.payment.merchant.entity.gateway.GatewayCashierItem; +import cn.daxpay.open.payment.merchant.enums.CashierItemResolveModeEnum; +import cn.daxpay.open.payment.merchant.enums.ClientEnvEnum; +import cn.daxpay.open.payment.merchant.enums.ClientRuntimeEnum; +import cn.daxpay.open.payment.merchant.enums.GatewayCashierTypeEnum; +import cn.daxpay.open.payment.merchant.service.gateway.ClientEnvPayResolveService; +import cn.daxpay.open.payment.route.service.runtime.PayRouteService; +import cn.daxpay.open.payment.trade.enums.GatewayOrderStatusEnum; +import cn.daxpay.open.payment.trade.enums.GatewayPayTypeEnum; import cn.daxpay.open.payment.trade.order.entity.GatewayPayOrder; +import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.unipay.param.gateway.GatewayAuthUrlParam; +import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam; import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; import cn.daxpay.open.platform.core.code.CommonErrorCode; import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthTypeEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.daxpay.open.platform.core.exception.DataNotExistException; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Objects; +import java.util.Set; + /// # 网关 H5 授权服务 /// /// 公开端(无商户签名)根据网关订单生成 OAuth 链接, 用于收银台/聚合页取 openId。 +/// 统一委托 [ChannelAuthService]: +/// - **支付宝**: 服务内走平台级 OAuth(本服务跳过通道路由) +/// - **微信/抖音**: 先解析支付路由再交 ChannelAuthService → 通道产品策略 +/// /// 安全约束: /// - 订单必须存在且可支付([GatewayPayAssistService#getOrderAndCheck]) /// - returnPath 仅允许站内业务相对路径, 防止开放重定向 -/// -/// 一期使用平台级认证配置(与调试工具同源); 通道级按支付项解析可二期扩展。 @Slf4j @Service @RequiredArgsConstructor public class GatewayAuthService { + private static final Set MINI_CLIENT_ENVS = Set.of( + ClientEnvEnum.WECHAT.getCode(), + ClientEnvEnum.ALIPAY.getCode(), + ClientEnvEnum.UNION_PAY.getCode(), + ClientEnvEnum.DOUYIN.getCode() + ); + + /// 网关 H5 授权支持的认证类型 + private static final Set SUPPORTED_AUTH_TYPES = Set.of( + ChannelAuthTypeEnum.ALIPAY, + ChannelAuthTypeEnum.WECHAT, + ChannelAuthTypeEnum.DOUYIN + ); + private final GatewayPayAssistService gatewayPayAssistService; - private final PlatformAuthService platformAuthService; + private final ChannelAuthService channelAuthService; + private final ClientEnvPayResolveService clientEnvPayResolveService; + private final PayRouteService payRouteService; + private final GatewayCashierItemManager gatewayCashierItemManager; /// 生成授权链接 public AuthUrlResult generateAuthUrl(GatewayAuthUrlParam param) { - // 校验订单可支付并装载商户上下文 GatewayPayOrder order = gatewayPayAssistService.getOrderAndCheck(param.getOrderNo()); String returnPath = sanitizeReturnPath(param.getReturnPath(), order.getOrderNo()); ChannelAuthTypeEnum authType = ChannelAuthTypeEnum.findByCode(param.getAuthType()); - return switch (authType) { - case WECHAT -> platformAuthService.generateWechatMpAuthUrl(returnPath); - case ALIPAY -> platformAuthService.generateAlipayAuthUrl(returnPath); - case DOUYIN -> platformAuthService.generateDouyinAuthUrl(returnPath); + + if (!SUPPORTED_AUTH_TYPES.contains(authType)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.clientEnvNotSupport"); + } + + GenerateAuthUrlParam authParam = new GenerateAuthUrlParam(); + authParam.setMchNo(order.getMchNo()); + authParam.setAppId(order.getAppId()); + authParam.setReturnPath(returnPath); + authParam.setAuthType(authType.getCode()); + + // 支付宝由 ChannelAuthService 走平台 OAuth, 无需通道应用路由; 微信/抖音须同源 resolve + if (authType != ChannelAuthTypeEnum.ALIPAY) { + RouteSnapshot route = resolveRoute(order, param); + authParam.setProduct(route.product()); + authParam.setChannelMchNo(route.channelMchNo()); + authParam.setCapability(route.capability()); + authParam.setChannelAppId(route.channelAppId()); + } + return channelAuthService.generateAuthUrl(authParam); + } + + /// 解析通道路由: 订单已锁定优先用快照; 否则按聚合/收银台与支付同源解析 + private RouteSnapshot resolveRoute(GatewayPayOrder order, GatewayAuthUrlParam param) { + // 支付中已回填通道信息: 与锁定通道同源, 避免二次路由漂移 + if (Objects.equals(order.getStatus(), GatewayOrderStatusEnum.PAYING.getCode()) + && StrUtil.isNotBlank(order.getChannelMchNo()) + && StrUtil.isNotBlank(order.getProduct())) { + return new RouteSnapshot( + order.getProduct(), + order.getChannelMchNo(), + order.getCapability(), + order.getChannelAppId()); + } + + String gatewayType = order.getGatewayType(); + if (Objects.equals(gatewayType, GatewayPayTypeEnum.AGGREGATE.getCode())) { + return resolveAggregateRoute(order, param); + } + if (Objects.equals(gatewayType, GatewayPayTypeEnum.CASHIER.getCode())) { + return resolveCashierRoute(order, param); + } + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.typeMismatch"); + } + + /// 聚合: ClientEnvPayResolve + PayRouteService(与 AggregatePayService 同源) + private RouteSnapshot resolveAggregateRoute(GatewayPayOrder order, GatewayAuthUrlParam param) { + if (StrUtil.isBlank(param.getClientEnv())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.clientEnv.notBlank"); + } + ClientEnvEnum clientEnv = ClientEnvEnum.findByCode(param.getClientEnv()); + ClientRuntimeEnum runtime = ClientRuntimeEnum.ofOrDefault(param.getRuntime()); + var resolved = clientEnvPayResolveService.resolveRequired(order.getAppId(), clientEnv, runtime); + + NormalPayParam routeParam = new NormalPayParam(); + routeParam.setMchNo(order.getMchNo()); + routeParam.setAppId(order.getAppId()); + routeParam.setMethod(resolved.method()); + routeParam.setChannelMchNo(resolved.channelMchNo()); + routeParam.setCapability(resolved.capability()); + payRouteService.resolve(routeParam); + return new RouteSnapshot( + routeParam.getProduct(), + routeParam.getChannelMchNo(), + routeParam.getCapability(), + null); + } + + /// 收银台: 按 itemId 解析 METHOD/DIRECT 后再路由(与 CashierPayService 同源) + private RouteSnapshot resolveCashierRoute(GatewayPayOrder order, GatewayAuthUrlParam param) { + if (param.getItemId() == null) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.itemId.notBlank"); + } + if (StrUtil.isBlank(param.getCashierType())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.cashierType.notBlank"); + } + GatewayCashierTypeEnum typeEnum = GatewayCashierTypeEnum.findByCode(param.getCashierType()); + String bucketClientEnv = normalizeClientEnvForBucket(typeEnum, param.getClientEnv()); + GatewayCashierItem item = loadAndCheckItem(param.getItemId(), order.getAppId(), typeEnum, bucketClientEnv); + + String method = null; + String channelMchNo = null; + String capability = null; + CashierItemResolveModeEnum resolveMode = CashierItemResolveModeEnum.findByCode(item.getResolveMode()); + switch (resolveMode) { + case METHOD -> { + if (StrUtil.isBlank(item.getMethod())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemMethodRequired"); + } + method = item.getMethod(); + } + case DIRECT -> { + if (StrUtil.isBlank(item.getChannelMchNo()) || StrUtil.isBlank(item.getCapability())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemChannelMchRequired"); + } + channelMchNo = item.getChannelMchNo(); + capability = item.getCapability(); + method = item.getMethod(); + } default -> throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.clientEnvNotSupport"); - }; + } + + NormalPayParam routeParam = new NormalPayParam(); + routeParam.setMchNo(order.getMchNo()); + routeParam.setAppId(order.getAppId()); + routeParam.setMethod(method); + routeParam.setChannelMchNo(channelMchNo); + routeParam.setCapability(capability); + payRouteService.resolve(routeParam); + return new RouteSnapshot( + routeParam.getProduct(), + routeParam.getChannelMchNo(), + routeParam.getCapability(), + null); + } + + private GatewayCashierItem loadAndCheckItem(Long itemId, String appId, + GatewayCashierTypeEnum typeEnum, String bucketClientEnv) { + GatewayCashierItem item = gatewayCashierItemManager.findById(itemId) + .orElseThrow(() -> new DataNotExistException("pay.error.gateway.cashierItemNotFound")); + if (!Objects.equals(item.getAppId(), appId)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemNotFound"); + } + if (!Objects.equals(item.getCashierType(), typeEnum.getCode())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemNotFound"); + } + if (!typeEnum.requiresClientEnv()) { + if (StrUtil.isNotBlank(item.getClientEnv())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemNotFound"); + } + } else if (!Objects.equals(item.getClientEnv(), bucketClientEnv)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.cashierItemNotFound"); + } + return item; + } + + private String normalizeClientEnvForBucket(GatewayCashierTypeEnum typeEnum, String clientEnv) { + if (!typeEnum.requiresClientEnv()) { + return null; + } + if (StrUtil.isBlank(clientEnv)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.clientEnvRequired"); + } + ClientEnvEnum env = ClientEnvEnum.findByCode(clientEnv); + if (typeEnum == GatewayCashierTypeEnum.MINI && !MINI_CLIENT_ENVS.contains(env.getCode())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "pay.error.gateway.clientEnvNotSupport"); + } + return env.getCode(); } /// 校验并规范化 returnPath: 必须是以 / 开头的相对路径, 禁止协议/外链/反斜杠 @@ -50,7 +238,6 @@ public class GatewayAuthService { "validation.field.returnPath.notBlank"); } String path = returnPath.trim(); - // 禁止绝对 URL / 协议相对 / 反斜杠逃逸 if (!path.startsWith("/") || path.startsWith("//") || path.contains("://") @@ -59,7 +246,6 @@ public class GatewayAuthService { throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.returnPathInvalid"); } - // 仅允许网关业务落地前缀, 且路径中应含本订单号(防跨单串跳) boolean allowedPrefix = path.startsWith("/cashier/") || path.startsWith("/aggregate/") || path.startsWith("/h/"); @@ -67,11 +253,14 @@ public class GatewayAuthService { throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.returnPathInvalid"); } - // 去掉 hash, 保留 query int hash = path.indexOf('#'); if (hash >= 0) { path = path.substring(0, hash); } return path; } + + /// 路由快照(供组装 GenerateAuthUrlParam) + private record RouteSnapshot(String product, String channelMchNo, String capability, String channelAppId) { + } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java index a7efa7ef7..8f632338b 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/GatewayPayHandleService.java @@ -28,6 +28,7 @@ import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformPa import cn.daxpay.open.platform.system.service.config.security.PlatformSecurityConfigService; import cn.daxpay.open.payment.trade.util.PayTradeInitUtil; import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Lazy; @@ -41,6 +42,7 @@ import java.util.Objects; /// 在 product/method 已解析后: 懒创建 Trade → 调通道策略 → 回写容器。 @Slf4j @Service +@AllArgsConstructor public class GatewayPayHandleService { private final GatewayPayOrderManager gatewayPayOrderManager; @@ -57,28 +59,6 @@ public class GatewayPayHandleService { /// 自注入,保证 [GatewayPayHandleService#createTrade] / [GatewayPayHandleService#paySuccess] 走 Spring 事务代理 private final GatewayPayHandleService self; - public GatewayPayHandleService(GatewayPayOrderManager gatewayPayOrderManager, - PayTradeManager payTradeManager, - PayRouteService payRouteService, - MerchantContextLoader merchantContextLoader, - PayUniHandleService payUniHandleService, - GatewayPayAssistService gatewayPayAssistService, - LockExecutor lockExecutor, - ObjectProvider payRiskCheckerProvider, - PlatformSecurityConfigService platformSecurityConfigService, - @Lazy GatewayPayHandleService self) { - this.gatewayPayOrderManager = gatewayPayOrderManager; - this.payTradeManager = payTradeManager; - this.payRouteService = payRouteService; - this.merchantContextLoader = merchantContextLoader; - this.payUniHandleService = payUniHandleService; - this.gatewayPayAssistService = gatewayPayAssistService; - this.lockExecutor = lockExecutor; - this.payRiskCheckerProvider = payRiskCheckerProvider; - this.platformSecurityConfigService = platformSecurityConfigService; - this.self = self; - } - /// 发起网关支付 /// /// @param channelMchNo 通道商户号(DIRECT 模式传入跳过路由, 为空走路由解析) @@ -156,10 +136,7 @@ public class GatewayPayHandleService { String errMsg = (e instanceof PayFailureException) ? e.getMessage() : "支付出现异常: " + e.getMessage(); payUniHandleService.payFail(existing, errMsg); - if (e instanceof RuntimeException re) { - throw re; - } - throw new PayFailureException(errMsg); + throw (RuntimeException) e; } NormalPayResult payResult = self.paySuccess(current, existing, result); // 风控事后补录: 用通道回写 buyerId 补充命中检查, 仅记录不阻断资金态 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/AuthCodeParam.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/AuthCodeParam.java index 9ee4cc19b..203155982 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/AuthCodeParam.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/AuthCodeParam.java @@ -17,10 +17,6 @@ import lombok.experimental.Accessors; @Schema(title = "通道认证参数") public class AuthCodeParam extends MerchantPaymentCommonParam { - /// 通道(支付宝平台级认证可不传) - @Schema(description = "通道") - private String channel; - /// 认证类型, 如果通道支持多种类型的情况下, 参数必传 /// @see ChannelAuthTypeEnum @Schema(description = "认证类型") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/GenerateAuthUrlParam.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/GenerateAuthUrlParam.java index 75d4d6c9a..d9d33cf20 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/GenerateAuthUrlParam.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/assist/GenerateAuthUrlParam.java @@ -1,10 +1,8 @@ package cn.daxpay.open.payment.unipay.param.assist; import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthTypeEnum; -import cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum; import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; import lombok.EqualsAndHashCode; @@ -18,19 +16,13 @@ import lombok.experimental.Accessors; @Schema(title = "生成授权链接参数") public class GenerateAuthUrlParam extends MerchantPaymentCommonParam { - /// 通道 - /// @see ChannelEnum - @NotBlank(message = "{validation.field.channel.notBlank}") - @Schema(description = "通道") - private String channel; - /// 认证类型, 如果通道支持多种类型的情况下, 不传默认为微信场景 /// @see ChannelAuthTypeEnum @Schema(description = "认证类型") private String authType = ChannelAuthTypeEnum.WECHAT.getCode(); /// 支付产品编码, 决定走哪个通道产品的认证策略 - /// 可选: 缺失时由 [cn.daxpay.open.payment.auth.ChannelAuthService] 从通道商户号(channelMchNo)反查 + /// 可选: 缺失时由 [cn.daxpay.open.payment.auth.ChannelProductAuthService] 从通道商户号(channelMchNo)反查 /// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum @Size(max = 32, message = "{validation.field.product.size}") @Schema(description = "支付产品编码") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayAuthUrlParam.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayAuthUrlParam.java index 704d131e1..a48dc03c3 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayAuthUrlParam.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayAuthUrlParam.java @@ -8,7 +8,7 @@ import lombok.Data; /// # 网关 H5 生成授权链接参数(无商户签名) /// /// 凭网关订单装载商户上下文后生成 OAuth 链接; 用于收银台/聚合等落地页取 openId。 -/// 当前一期走平台级认证配置(微信公众号/支付宝/抖音 H5); 通道级(按支付项 DIRECT)可后续扩展。 +/// 微信/抖音按支付路由读通道商户应用; 支付宝仍走平台级配置。 @Data @Schema(title = "网关授权链接参数") public class GatewayAuthUrlParam { @@ -29,4 +29,26 @@ public class GatewayAuthUrlParam { @NotBlank(message = "{validation.field.returnPath.notBlank}") @Size(max = 200, message = "{validation.field.returnPath.size}") private String returnPath; + + /// 客户端环境(聚合/收银台 H5 解析支付方式用; 支付宝可空) + /// @see cn.daxpay.open.payment.merchant.enums.ClientEnvEnum + @Schema(description = "客户端环境 wechat/alipay/douyin/union_pay") + @Size(max = 32, message = "{validation.field.clientEnv.size}") + private String clientEnv; + + /// 运行形态(聚合解析用; 空默认 h5) + /// @see cn.daxpay.open.payment.merchant.enums.ClientRuntimeEnum + @Schema(description = "运行形态 h5/mini") + @Size(max = 16, message = "{validation.field.runtime.size}") + private String runtime; + + /// 收银台支付项 ID(收银台场景必填) + @Schema(description = "收银台支付项ID") + private Long itemId; + + /// 收银台类型 h5/web/mini(收银台场景必填) + /// @see cn.daxpay.open.payment.merchant.enums.GatewayCashierTypeEnum + @Schema(description = "收银台类型") + @Size(max = 16, message = "{validation.field.cashierType.size}") + private String cashierType; } diff --git a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/client/service/CodePayAssistService.java b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/client/service/CodePayAssistService.java index 5e1baf0ec..af067e677 100644 --- a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/client/service/CodePayAssistService.java +++ b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/client/service/CodePayAssistService.java @@ -1,6 +1,6 @@ package cn.daxpay.open.payment.unipay.client.service; -import cn.daxpay.open.payment.auth.ChannelAuthFacade; +import cn.daxpay.open.payment.auth.ChannelAuthService; import cn.daxpay.open.payment.common.context.MerchantContextLoader; import cn.daxpay.open.payment.common.util.PayMethodOpenIdSupport; import cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum; @@ -30,7 +30,6 @@ import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult; import cn.daxpay.open.platform.common.spring.util.WebServletUtil; import cn.daxpay.open.platform.core.code.CommonCode; import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; import cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum; import cn.daxpay.open.platform.core.enums.unipay.ChannelAuthTypeEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; @@ -62,7 +61,7 @@ public class CodePayAssistService { private final CodePayResolveService codePayResolveService; private final NormalPayService normalPayService; private final PayRouteService payRouteService; - private final ChannelAuthFacade channelAuthFacade; + private final ChannelAuthService channelAuthService; private final NormalPayOrderManager normalPayOrderManager; /// 风控检查器(可选 SPI:用于判断是否存在 openId 黑名单, 决定是否触发强制 OAuth) private final ObjectProvider payRiskCheckerProvider; @@ -186,11 +185,7 @@ public class CodePayAssistService { authParam.setCapability(routeParam.getCapability()); authParam.setReturnPath(returnPath); authParam.setAuthType(this.mapAuthType(clientEnv)); - // channel: 由产品反推 - if (StrUtil.isNotBlank(routeParam.getProduct())) { - authParam.setChannel(ProductEnum.findByCode(routeParam.getProduct()).getChannel()); - } - return channelAuthFacade.generateAuthUrl(authParam); + return channelAuthService.generateAuthUrl(authParam); } /// 查询码牌订单状态(忽略租户; 仅 cashier_code 来源) @@ -308,6 +303,7 @@ public class CodePayAssistService { return switch (clientEnv) { case ALIPAY -> ChannelAuthTypeEnum.ALIPAY.getCode(); case WECHAT -> ChannelAuthTypeEnum.WECHAT.getCode(); + case DOUYIN -> ChannelAuthTypeEnum.DOUYIN.getCode(); default -> ChannelAuthTypeEnum.WECHAT.getCode(); }; } diff --git a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/ChannelAuthController.java b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/ChannelAuthController.java index 875c40d17..7e3c404af 100644 --- a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/ChannelAuthController.java +++ b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/ChannelAuthController.java @@ -3,7 +3,7 @@ package cn.daxpay.open.payment.unipay.trade.controller; import cn.daxpay.open.platform.core.annotation.IgnoreAuth; import cn.daxpay.open.platform.core.rest.Res; import cn.daxpay.open.platform.core.rest.result.Result; -import cn.daxpay.open.payment.auth.ChannelAuthFacade; +import cn.daxpay.open.payment.auth.ChannelAuthService; import cn.daxpay.open.payment.unipay.param.assist.AuthCodeParam; import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam; import cn.daxpay.open.payment.common.result.DaxResult; @@ -22,9 +22,9 @@ import org.springframework.web.bind.annotation.RestController; /// # 通道认证服务 /// /// 统一支付网关对外的通道 OAuth 入口(取 openId/userId, **非登录**)。 -/// 业务分发委托 [ChannelAuthFacade]: +/// 业务分发委托 [ChannelAuthService]: /// - **生成授权链接**: authType=ALIPAY 走平台级支付宝 OAuth; 其余按支付产品走通道策略 -/// - **授权码回调**: 按 session.source 走平台分支, 否则走通道策略 +/// - **授权码回调**: 按 session.source 走平台分支, 否则走通道产品策略 @IgnoreAuth @Tag(name = "通道认证服务") @RestController @@ -32,25 +32,25 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class ChannelAuthController { - private final ChannelAuthFacade channelAuthFacade; + private final ChannelAuthService channelAuthService; @PaymentVerify @Operation(summary = "获取授权链接") @PostMapping("/generate-auth-url") public DaxResult generateAuthUrl(@RequestBody GenerateAuthUrlParam param) { - return DaxRes.ok(channelAuthFacade.generateAuthUrl(param)); + return DaxRes.ok(channelAuthService.generateAuthUrl(param)); } @Operation(summary = "通过AuthCode获取认证结果") @PostMapping("/auth") public Result auth(@RequestBody AuthCodeParam param) { - return Res.ok(channelAuthFacade.auth(param)); + return Res.ok(channelAuthService.auth(param)); } @Operation(summary = "通过AuthCode获取并设置认证结果") @PostMapping("/auth-and-set") public Result authAndSet(@RequestBody AuthCodeParam param) { - channelAuthFacade.auth(param); + channelAuthService.auth(param); return Res.ok(); } } diff --git a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/DouyinJsapiController.java b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/DouyinJsapiController.java deleted file mode 100644 index 4490ae5d5..000000000 --- a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/trade/controller/DouyinJsapiController.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.daxpay.open.payment.unipay.trade.controller; - -import cn.daxpay.open.platform.capability.douyin.auth.result.DouyinJsapiConfigResult; -import cn.daxpay.open.platform.capability.douyin.auth.service.DouyinOpenTokenService; -import cn.daxpay.open.platform.core.annotation.IgnoreAuth; -import cn.daxpay.open.platform.core.code.CommonErrorCode; -import cn.daxpay.open.platform.core.exception.BizInfoException; -import cn.daxpay.open.platform.core.rest.Res; -import cn.daxpay.open.platform.core.rest.result.Result; -import cn.daxpay.open.platform.system.entity.config.platform.auth.PlatformDouyinH5AuthConfig; -import cn.daxpay.open.platform.system.service.config.auth.PlatformDouyinH5AuthConfigService; -import cn.hutool.core.util.StrUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -/// # 抖音 H5 JSAPI 调起前置 - sdk.config 验签接口 -/// -/// 给前端 H5(抖音 APP webview) 调用: 前端拉起 `ttcjpay.dypay` 前必须先 `window.DouyinOpenJSBridge.config({...})` -/// 通过签名验证, 此接口返回该签名包(clientKey + timestamp + nonceStr + signature)。 -/// -/// 签名基于平台级 [PlatformDouyinH5AuthConfig](clientKey/clientSecret) 换取的 jsapi_ticket 计算, -/// 不依赖商户上下文, 故接口路径不挂 `@PaymentVerify`, 仅 `@IgnoreAuth` 跳过登录即可。 -/// -/// 算法: `MD5(jsapi_ticket={}&nonce_str={}×tamp={}&url={})`, 字典序拼接。 -/// -/// 参考文档: -/// - JS 接入指南: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/js/js-access -/// - 验证签名: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/js/signature -@IgnoreAuth -@Tag(name = "抖音 JSAPI 调起辅助") -@RestController -@RequestMapping("/unipay/assist/channel/douyin") -@RequiredArgsConstructor -public class DouyinJsapiController { - - private final PlatformDouyinH5AuthConfigService platformDouyinH5AuthConfigService; - private final DouyinOpenTokenService douyinOpenTokenService; - - @Operation(summary = "获取抖音 JS-SDK sdk.config 验签包") - @Parameter(name = "url", description = "调用 JS 接口页面的完整 URL(不含 # 及后面部分)", required = true) - @GetMapping("/jsapi-config") - public Result getJsapiConfig(@RequestParam("url") String url) { - if (StrUtil.isBlank(url)) { - throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, - "error.channel.douyin.jsapiConfigParamBlank"); - } - PlatformDouyinH5AuthConfig config = platformDouyinH5AuthConfigService.getDouyinH5AuthConfig(); - if (StrUtil.hasBlank(config.getClientKey(), config.getClientSecret())) { - // 抖音 H5 应用认证配置不完整, 请先在「平台配置」中配置 clientKey/clientSecret - throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, - "error.channel.douyin.h5AuthNotConfigured"); - } - DouyinJsapiConfigResult result = douyinOpenTokenService.buildJsapiConfig( - config.getClientKey(), config.getClientSecret(), url); - return Res.ok(result); - } -} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json index 4d709ae08..b25d61ff1 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Failed to acquire Douyin client_access_token refresh lock, please retry later", "ticketLockFailed": "Failed to acquire Douyin jsapi_ticket refresh lock, please retry later", "jsapiConfigParamBlank": "Douyin sdk.config signing parameter (clientKey/clientSecret/url) is blank", - "h5AuthNotConfigured": "Douyin H5 app auth config is incomplete, please configure clientKey/clientSecret in Platform Config first" + "h5AuthNotConfigured": "Douyin H5 app auth config is incomplete, please configure clientKey/clientSecret in Platform Config first", + "authFailed": "Failed to get user id (openId): {0}", + "channelAppIdNotFound": "Douyin AppId [{0}] is not configured in the system", + "webAppNotFound": "No Douyin web app under this channel merchant; cannot complete H5 auth/signing", + "appAuthSecretMissing": "Douyin direct app auth secret is missing; configure appSecret on the channel merchant app first", + "jsapiContextRequired": "Douyin JSAPI signing requires one of orderNo, code, or channelMchNo" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 822fbbe9e..cc2767938 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "Private bucket cannot be blank" }, + "url": { + "notBlank": "URL cannot be blank" + }, "urlType": { "notBlank": "URL type cannot be blank" }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "YeePay ISV number cannot be blank" + }, + "itemId": { + "notBlank": "Cashier item ID cannot be blank" + }, + "runtime": { + "size": "Runtime code cannot exceed 16 characters" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json index 4413a7016..783c99ccd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Gagal mendapatkan kunci pembaruan client_access_token Douyin, coba lagi nanti", "ticketLockFailed": "Gagal mendapatkan kunci pembaruan jsapi_ticket Douyin, coba lagi nanti", "jsapiConfigParamBlank": "Parameter tanda tangan sdk.config Douyin (clientKey/clientSecret/url) kosong", - "h5AuthNotConfigured": "Konfigurasi autentikasi aplikasi H5 Douyin tidak lengkap, mohon konfigurasikan clientKey/clientSecret di Konfigurasi Platform terlebih dahulu" + "h5AuthNotConfigured": "Konfigurasi autentikasi aplikasi H5 Douyin tidak lengkap, mohon konfigurasikan clientKey/clientSecret di Konfigurasi Platform terlebih dahulu", + "authFailed": "Gagal mendapatkan id pengguna (openId): {0}", + "channelAppIdNotFound": "Douyin AppId [{0}] tidak dikonfigurasi di sistem", + "webAppNotFound": "Tidak ada aplikasi web Douyin pada merchant saluran; tidak dapat menyelesaikan auth/signing H5", + "appAuthSecretMissing": "Rahasia auth aplikasi langsung Douyin hilang; konfigurasikan appSecret pada aplikasi merchant saluran", + "jsapiContextRequired": "Penandatanganan JSAPI Douyin memerlukan salah satu dari orderNo, code, atau channelMchNo" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json index 70db936db..c1f027a0b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "Keranjang pribadi tidak boleh kosong" }, + "url": { + "notBlank": "URL tidak boleh kosong" + }, "urlType": { "notBlank": "Jenis URL tidak boleh kosong" }, @@ -803,6 +806,12 @@ }, "blockOverseasIp": { "notNull": "Saklar blokir IP luar negeri tidak boleh kosong" + }, + "itemId": { + "notBlank": "ID item kasir tidak boleh kosong" + }, + "runtime": { + "size": "Kode runtime tidak boleh lebih dari 16 karakter" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json index cd227a9c7..1a41a423a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Douyin client_access_token 更新ロックの取得に失敗しました、後でもう一度お試しください", "ticketLockFailed": "Douyin jsapi_ticket 更新ロックの取得に失敗しました、後でもう一度お試しください", "jsapiConfigParamBlank": "Douyin sdk.config 署名パラメータ(clientKey/clientSecret/url)が空です", - "h5AuthNotConfigured": "Douyin H5 アプリ認証設定が不完全です。先に「プラットフォーム設定」で clientKey/clientSecret を設定してください" + "h5AuthNotConfigured": "Douyin H5 アプリ認証設定が不完全です。先に「プラットフォーム設定」で clientKey/clientSecret を設定してください", + "authFailed": "ユーザー識別子(openId)の取得に失敗しました: {0}", + "channelAppIdNotFound": "指定の Douyin AppId [{0}] はシステムに未設定です", + "webAppNotFound": "チャネル販売者に Douyin Web アプリが未設定のため、H5 認証/署名できません", + "appAuthSecretMissing": "Douyin 直連アプリの認証シークレット未設定。チャネル販売者アプリで appSecret を設定してください", + "jsapiContextRequired": "Douyin JSAPI 署名には orderNo / code / channelMchNo のいずれかが必要です" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json index 875f319e6..df1abd853 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "プライベートバケットを空白にすることはできません" }, + "url": { + "notBlank": "URLを空白にすることはできません" + }, "urlType": { "notBlank": "URL タイプを空白にすることはできません" }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "YeePay ISV番号は必須です" + }, + "itemId": { + "notBlank": "キャッシャー支払項目IDは必須です" + }, + "runtime": { + "size": "実行形態コードは16文字以内です" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json index 2e00abdf2..27071d6f0 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Douyin client_access_token 갱신 잠금 획득에 실패했습니다, 나중에 다시 시도해 주세요", "ticketLockFailed": "Douyin jsapi_ticket 갱신 잠금 획득에 실패했습니다, 나중에 다시 시도해 주세요", "jsapiConfigParamBlank": "Douyin sdk.config 서명 매개변수(clientKey/clientSecret/url)가 비어 있습니다", - "h5AuthNotConfigured": "Douyin H5 앱 인증 구성이 불완전합니다. 먼저 '플랫폼 구성'에서 clientKey/clientSecret을 구성해 주세요" + "h5AuthNotConfigured": "Douyin H5 앱 인증 구성이 불완전합니다. 먼저 '플랫폼 구성'에서 clientKey/clientSecret을 구성해 주세요", + "authFailed": "사용자 식별자(openId)를 가져오지 못했습니다: {0}", + "channelAppIdNotFound": "지정한 Douyin AppId [{0}]가 시스템에 구성되지 않았습니다", + "webAppNotFound": "채널 판매자에 Douyin 웹 앱이 없어 H5 인증/서명을 완료할 수 없습니다", + "appAuthSecretMissing": "Douyin 직연동 앱 인증 시크릿이 없습니다. 채널 판매자 앱에서 appSecret을 구성하세요", + "jsapiContextRequired": "Douyin JSAPI 서명에는 orderNo, code 또는 channelMchNo 중 하나가 필요합니다" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json index dd301bef9..387bd74f4 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "비공개 버킷은 비워둘 수 없습니다." }, + "url": { + "notBlank": "URL은 비워둘 수 없습니다" + }, "urlType": { "notBlank": "URL 유형은 비워둘 수 없습니다." }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "YeePay ISV 번호는 필수입니다" + }, + "itemId": { + "notBlank": "계산대 결제 항목 ID는 필수입니다" + }, + "runtime": { + "size": "런타임 코드는 16자를 초과할 수 없습니다" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json index 442a87e68..453d759bd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Gagal mendapatkan kunci pembaharuan client_access_token Douyin, sila cuba lagi kemudian", "ticketLockFailed": "Gagal mendapatkan kunci pembaharuan jsapi_ticket Douyin, sila cuba lagi kemudian", "jsapiConfigParamBlank": "Parameter tandatangan sdk.config Douyin (clientKey/clientSecret/url) kosong", - "h5AuthNotConfigured": "Konfigurasi pengesahan aplikasi H5 Douyin tidak lengkap, sila konfigurasikan clientKey/clientSecret dalam Konfigurasi Platform terlebih dahulu" + "h5AuthNotConfigured": "Konfigurasi pengesahan aplikasi H5 Douyin tidak lengkap, sila konfigurasikan clientKey/clientSecret dalam Konfigurasi Platform terlebih dahulu", + "authFailed": "Gagal mendapatkan id pengguna (openId): {0}", + "channelAppIdNotFound": "Douyin AppId [{0}] tidak dikonfigurasi dalam sistem", + "webAppNotFound": "Tiada apl web Douyin di bawah peniaga saluran; tidak dapat melengkapkan auth/tandatangan H5", + "appAuthSecretMissing": "Rahsia auth apl terus Douyin tiada; konfigurasikan appSecret pada apl peniaga saluran dahulu", + "jsapiContextRequired": "Tandatangan JSAPI Douyin memerlukan salah satu daripada orderNo, code atau channelMchNo" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json index 8000043be..2c6d95f6b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "Baldi peribadi tidak boleh kosong" }, + "url": { + "notBlank": "URL tidak boleh kosong" + }, "urlType": { "notBlank": "Jenis URL tidak boleh kosong" }, @@ -803,6 +806,12 @@ }, "blockOverseasIp": { "notNull": "Suis sekatan IP luar negara tidak boleh kosong" + }, + "itemId": { + "notBlank": "ID item kasir tidak boleh kosong" + }, + "runtime": { + "size": "Kod runtime tidak boleh melebihi 16 aksara" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json index efb95421e..25051c891 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "ไม่สามารถรับล็อกการรีเฟรช client_access_token ของ Douyin ได้ โปรดลองอีกครั้งในภายหลัง", "ticketLockFailed": "ไม่สามารถรับล็อกการรีเฟรช jsapi_ticket ของ Douyin ได้ โปรดลองอีกครั้งในภายหลัง", "jsapiConfigParamBlank": "พารามิเตอร์ลายเซ็น sdk.config ของ Douyin (clientKey/clientSecret/url) ว่างเปล่า", - "h5AuthNotConfigured": "การกำหนดค่าการตรวจสอบสิทธิ์แอป H5 Douyin ไม่สมบูรณ์ โปรดกำหนดค่า clientKey/clientSecret ในการกำหนดค่าแพลตฟอร์มก่อน" + "h5AuthNotConfigured": "การกำหนดค่าการตรวจสอบสิทธิ์แอป H5 Douyin ไม่สมบูรณ์ โปรดกำหนดค่า clientKey/clientSecret ในการกำหนดค่าแพลตฟอร์มก่อน", + "authFailed": "ไม่สามารถรับรหัสผู้ใช้ (openId) ได้: {0}", + "channelAppIdNotFound": "Douyin AppId [{0}] ยังไม่ได้ตั้งค่าในระบบ", + "webAppNotFound": "ไม่มีแอปเว็บ Douyin ภายใต้ผู้ค้าช่องทาง ไม่สามารถยืนยันตัวตน/ลงนาม H5 ได้", + "appAuthSecretMissing": "ไม่มี appSecret การยืนยันตัวตนของแอป Douyin ตรง; ตั้งค่าในแอปผู้ค้าช่องทางก่อน", + "jsapiContextRequired": "การลงนาม JSAPI Douyin ต้องระบุ orderNo หรือ code หรือ channelMchNo" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json index 8c61b0119..b10343444 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "ที่เก็บข้อมูลส่วนตัวต้องไม่เว้นว่าง" }, + "url": { + "notBlank": "URL ไม่สามารถเว้นว่างได้" + }, "urlType": { "notBlank": "ประเภท URL ไม่สามารถเว้นว่างได้" }, @@ -803,6 +806,12 @@ }, "blockOverseasIp": { "notNull": "สวิตช์บล็อก IP ต่างประเทศต้องไม่ว่าง" + }, + "itemId": { + "notBlank": "ต้องระบุรหัสรายการแคชเชียร์" + }, + "runtime": { + "size": "รหัส runtime ต้องไม่เกิน 16 ตัวอักษร" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json index 59933c8ab..93393c764 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "Không thể lấy khóa làm mới client_access_token Douyin, vui lòng thử lại sau", "ticketLockFailed": "Không thể lấy khóa làm mới jsapi_ticket Douyin, vui lòng thử lại sau", "jsapiConfigParamBlank": "Tham số ký sdk.config Douyin (clientKey/clientSecret/url) bị trống", - "h5AuthNotConfigured": "Cấu hình xác thực ứng dụng H5 Douyin không đầy đủ, vui lòng cấu hình clientKey/clientSecret trong Cấu hình nền tảng trước" + "h5AuthNotConfigured": "Cấu hình xác thực ứng dụng H5 Douyin không đầy đủ, vui lòng cấu hình clientKey/clientSecret trong Cấu hình nền tảng trước", + "authFailed": "Không lấy được định danh người dùng (openId): {0}", + "channelAppIdNotFound": "Douyin AppId [{0}] chưa được cấu hình trong hệ thống", + "webAppNotFound": "Merchant kênh chưa cấu hình ứng dụng web Douyin; không thể hoàn tất xác thực/ký H5", + "appAuthSecretMissing": "Thiếu appSecret xác thực ứng dụng Douyin trực tiếp; hãy cấu hình trên ứng dụng merchant kênh", + "jsapiContextRequired": "Ký JSAPI Douyin cần một trong orderNo, code hoặc channelMchNo" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json index c77141587..6b6acc337 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "Nhóm riêng tư không được để trống" }, + "url": { + "notBlank": "URL không được để trống" + }, "urlType": { "notBlank": "Loại URL không được để trống" }, @@ -803,6 +806,12 @@ }, "blockOverseasIp": { "notNull": "Công tắc chặn IP nước ngoài không được để trống" + }, + "itemId": { + "notBlank": "ID mục thanh toán quầy không được để trống" + }, + "runtime": { + "size": "Mã runtime không được vượt quá 16 ký tự" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json index 206acb997..4f325cd6f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "获取抖音 client_access_token 刷新锁失败, 请稍后重试", "ticketLockFailed": "获取抖音 jsapi_ticket 刷新锁失败, 请稍后重试", "jsapiConfigParamBlank": "抖音 sdk.config 验签参数(clientKey/clientSecret/url)存在空值", - "h5AuthNotConfigured": "抖音 H5 应用认证配置不完整, 请先在「平台配置」中配置 clientKey/clientSecret" + "h5AuthNotConfigured": "抖音 H5 应用认证配置不完整, 请先在「平台配置」中配置 clientKey/clientSecret", + "authFailed": "获取用户标识(openId)失败: {0}", + "channelAppIdNotFound": "指定的抖音应用AppId[{0}]未在系统中配置", + "webAppNotFound": "通道商户未配置抖音网站应用, 无法完成 H5 授权/验签", + "appAuthSecretMissing": "抖音直连应用授权密钥未配置, 请先在通道商户应用中配置 appSecret", + "jsapiContextRequired": "抖音 JSAPI 验签须传入 orderNo、code 或 channelMchNo 之一" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index 68321463d..6da0627eb 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "私有存储桶不能为空" }, + "url": { + "notBlank": "URL不能为空" + }, "urlType": { "notBlank": "端点类型不能为空" }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "易宝服务商号不能为空" + }, + "itemId": { + "notBlank": "收银台支付项ID不可为空" + }, + "runtime": { + "size": "运行形态编码不可超过16位" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json index 51de5de6e..256cb9841 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "取得抖音 client_access_token 更新鎖失敗, 請稍後重試", "ticketLockFailed": "取得抖音 jsapi_ticket 更新鎖失敗, 請稍後重試", "jsapiConfigParamBlank": "抖音 sdk.config 驗簽參數(clientKey/clientSecret/url)存在空值", - "h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret" + "h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret", + "authFailed": "取得使用者標識(openId)失敗: {0}", + "channelAppIdNotFound": "指定的抖音應用AppId[{0}]未在系統中配置", + "webAppNotFound": "通道商戶未配置抖音網站應用, 無法完成 H5 授權/驗簽", + "appAuthSecretMissing": "抖音直連應用授權密鑰未配置, 請先在通道商戶應用中配置 appSecret", + "jsapiContextRequired": "抖音 JSAPI 驗簽須傳入 orderNo、code 或 channelMchNo 之一" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json index af39a9ac3..94b330311 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "私有存儲桶不能為空" }, + "url": { + "notBlank": "URL不能為空" + }, "urlType": { "notBlank": "端點類型不能為空" }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "易寶服務商號不能為空" + }, + "itemId": { + "notBlank": "收銀台支付項ID不可為空" + }, + "runtime": { + "size": "運行形態編碼不可超過16位" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json index d0ee40d7e..5acf2a8d1 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/channel/douyin.json @@ -11,5 +11,10 @@ "tokenLockFailed": "取得抖音 client_access_token 更新鎖失敗, 請稍後重試", "ticketLockFailed": "取得抖音 jsapi_ticket 更新鎖失敗, 請稍後重試", "jsapiConfigParamBlank": "抖音 sdk.config 驗簽參數(clientKey/clientSecret/url)存在空值", - "h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret" + "h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret", + "authFailed": "取得使用者標識(openId)失敗: {0}", + "channelAppIdNotFound": "指定的抖音應用AppId[{0}]未在系統中配置", + "webAppNotFound": "通道商戶未配置抖音網站應用, 無法完成 H5 授權/驗簽", + "appAuthSecretMissing": "抖音直連應用授權密鑰未配置, 請先在通道商戶應用中配置 appSecret", + "jsapiContextRequired": "抖音 JSAPI 驗簽須傳入 orderNo、code 或 channelMchNo 之一" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json index f7df951e0..5ed7908dc 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json @@ -420,6 +420,9 @@ "privateBucket": { "notBlank": "私有儲存桶不能為空" }, + "url": { + "notBlank": "URL不能為空" + }, "urlType": { "notBlank": "端點型別不能為空" }, @@ -803,6 +806,12 @@ }, "yopIsvNo": { "notBlank": "易寶服務商號不能為空" + }, + "itemId": { + "notBlank": "收銀台支付項ID不可為空" + }, + "runtime": { + "size": "執行形態編碼不可超過16位" } } }