From eca7cd3bef405006875fb65ba5fb98101c607483 Mon Sep 17 00:00:00 2001 From: daxpay Date: Wed, 29 Jul 2026 17:01:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E6=94=B6=E9=93=B6=E5=8F=B0?= =?UTF-8?q?=E6=8A=96=E9=9F=B3=E8=AE=A4=E8=AF=81=E6=94=AF=E6=8C=81=20code/a?= =?UTF-8?q?nonymous=5Fcode=20=E5=88=86=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CashierAuth 与 DouyinMaAuthService 按官方 jscode2session 分传 code 与 anonymous_code;小程序换 openId 归收银台运行时,Web 认证调试注释同步。 Co-authored-by: Cursor --- .../develop/DevelopAuthAdminController.java | 11 +++---- .../auth/develop/DevelopAuthService.java | 6 ++-- .../pay/gateway/CashierAuthService.java | 29 +++++++++++++----- .../param/gateway/CashierAuthParam.java | 12 ++++++-- .../develop/MchDevelopAuthController.java | 10 +++---- .../auth/service/DouyinMaAuthService.java | 30 ++++++++++++++----- .../system/mobile/config/DyMiniAppConfig.java | 4 ++- 7 files changed, 69 insertions(+), 33 deletions(-) diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthAdminController.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthAdminController.java index d304a5700..980a54392 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthAdminController.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/controller/develop/DevelopAuthAdminController.java @@ -1,7 +1,7 @@ package cn.daxpay.open.payment.admin.controller.develop; -import cn.daxpay.open.payment.auth.develop.DevelopAuthService; import cn.daxpay.open.payment.auth.develop.DevelopChannelAuthParam; +import cn.daxpay.open.payment.auth.develop.DevelopAuthService; import cn.daxpay.open.payment.unipay.result.assist.AuthResult; import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult; import cn.daxpay.open.platform.core.annotation.PermCode; @@ -21,16 +21,13 @@ import org.springframework.web.bind.annotation.RestController; /// 认证调试(管理) /// -/// 调试入口: -/// - 支付宝H5平台级配置(中间页授权链接) +/// 调试入口(均为 OAuth 链接 + queryCode 轮询): +/// - 支付宝H5平台级配置 /// - 微信公众号配置(平台级 OAuth) -/// - 抖音 H5 应用配置(平台级 silent_auth 静默授权) +/// - 抖音 H5 应用配置(平台级 silent_auth) /// - 微信支付(直连/服务商, 需商户参数) /// - 抖音支付(直连/服务商, 需商户参数) -/// - 支付宝小程序(暂未实现) -/// - 微信小程序(商户端/运营端, 暂未实现) /// -/// 已实现项均通过查询码轮询认证结果。 @Validated @PermCode(menuCode = PermCodes.Develop.Auth.MENU) @Tag(name = "认证调试服务") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/develop/DevelopAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/develop/DevelopAuthService.java index bb6ec1fca..922d53157 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/develop/DevelopAuthService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/auth/develop/DevelopAuthService.java @@ -27,8 +27,10 @@ import cn.daxpay.open.payment.auth.channel.MerchantChannelAuthService; /// - **微信支付(直连/服务商)**: 委托 [MerchantChannelAuthService] 按支付产品路由认证策略, /// 依赖商户上下文(channelMchNo/产品/能力) /// - **抖音支付(直连/服务商)**: 同微信支付, 显式选抖音网站应用 → [MerchantChannelAuthService] -/// - **支付宝小程序**: 暂未实现 -/// - **微信小程序(商户端/运营端)**: 暂未实现 +/// +/// 小程序(微信/支付宝/抖音)获取 openId 已迁至收银台小程序运行时 +/// ([cn.daxpay.open.payment.trade.runtime.service.pay.gateway.CashierAuthService]), +/// Web 认证调试不再提供小程序 code 直换入口。 /// /// 已实现项共用 queryCode 轮询机制(由 [AuthSessionStore#queryAuthResult] 统一查询)。 @Slf4j diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierAuthService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierAuthService.java index 66669b35a..ce3bc4164 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierAuthService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/pay/gateway/CashierAuthService.java @@ -52,11 +52,10 @@ public class CashierAuthService { public AuthResult auth(CashierAuthParam param) { // channel 字符串 → 枚举(非法值由 findByCode 抛 ConfigNotExistException) ChannelAuthTypeEnum authType = ChannelAuthTypeEnum.findByCode(param.getChannel()); - String authCode = param.getAuthCode(); return switch (authType) { - case WECHAT -> authWechat(authType, authCode); - case ALIPAY -> authAlipay(authType, authCode); - case DOUYIN -> authDouyin(authType, authCode); + case WECHAT -> authWechat(authType, param.getAuthCode()); + case ALIPAY -> authAlipay(authType, param.getAuthCode()); + case DOUYIN -> authDouyin(authType, param.getAuthCode(), param.getAnonymousCode()); default -> throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "error.mobile_app.platformNotFound", authType.getCode()); }; @@ -64,6 +63,7 @@ public class CashierAuthService { /// 微信小程序: jscode2session 换 openId private AuthResult authWechat(ChannelAuthTypeEnum authType, String authCode) { + requireAuthCode(authCode); WxMiniAppConfig config = loadConfig(authType, WxMiniAppConfig.class); WechatAuthResult data = wechatMaAuthService.getOpenId(authCode, config.getAppId(), config.getAppSecret()); if (StrUtil.isBlank(data.getOpenId())) { @@ -78,6 +78,7 @@ public class CashierAuthService { /// 支付宝小程序: alipay.system.oauth.token 换 userId private AuthResult authAlipay(ChannelAuthTypeEnum authType, String authCode) { + requireAuthCode(authCode); AlipayMiniAppConfig config = loadConfig(authType, AlipayMiniAppConfig.class); AlipayAuthConfig authConfig = toAlipayAuthConfig(config); AlipayAuthResult alipayResult = alipayAuthCapability.getUserId(authConfig, authCode); @@ -94,11 +95,16 @@ public class CashierAuthService { .setStatus(ChannelAuthStatusEnum.SUCCESS.getCode()); } - /// 抖音小程序: jscode2session 换 openId - private AuthResult authDouyin(ChannelAuthTypeEnum authType, String authCode) { + /// 抖音小程序: jscode2session 换 openId(code / anonymous_code 分字段) + private AuthResult authDouyin(ChannelAuthTypeEnum authType, String authCode, String anonymousCode) { + if (StrUtil.isAllBlank(authCode, anonymousCode)) { + // 授权码不可为空 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.oauthCode.notBlank"); + } DyMiniAppConfig config = loadConfig(authType, DyMiniAppConfig.class); DouyinAuthResult data = douyinMaAuthService.getOpenId( - config.getAppId(), config.getAppSecret(), authCode); + config.getAppId(), config.getAppSecret(), authCode, anonymousCode); if (StrUtil.isBlank(data.getOpenId())) { // 抖音: 获取用户标识失败 throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, "error.douyin.authFailed", "openId is blank"); @@ -108,6 +114,15 @@ public class CashierAuthService { .setStatus(ChannelAuthStatusEnum.SUCCESS.getCode()); } + /// 微信/支付宝: authCode 必填 + private void requireAuthCode(String authCode) { + if (StrUtil.isBlank(authCode)) { + // 授权码不可为空 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.oauthCode.notBlank"); + } + } + /// 加载收银台小程序配置(统一入口, 内部完成 channel→platform 映射) private T loadConfig(ChannelAuthTypeEnum authType, Class configType) { return mobileAppService.findAppConfig( diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/CashierAuthParam.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/CashierAuthParam.java index e150cf066..0d0f9b129 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/CashierAuthParam.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/CashierAuthParam.java @@ -9,6 +9,9 @@ import lombok.Data; /// /// 收银台小程序(微信/支付宝/抖音)前端通过 uni.login / my.getAuthCode / tt.login /// 获取授权码后, 调用此参数换 openId/userId。无商户签名, 走 [IgnoreAuth] 端点。 +/// +/// 抖音: `authCode`(login code) 与 `anonymousCode` 对应官方分字段 `code` / `anonymous_code`, +/// 至少传一个; 非匿名场景可同时传两者。微信/支付宝仅使用 `authCode`。 @Data @Schema(title = "收银台认证参数") public class CashierAuthParam { @@ -19,9 +22,14 @@ public class CashierAuthParam { @Size(max = 32, message = "{validation.field.channel.size}") private String channel; - /// 授权码(uni.login / my.getAuthCode / tt.login 返回) + /// 授权码(微信 uni.login / 支付宝 my.getAuthCode / 抖音 tt.login 的 code) + /// 抖音匿名登录时可空, 此时须传 [anonymousCode] @Schema(description = "授权码") - @NotBlank(message = "{validation.field.oauthCode.notBlank}") @Size(max = 128, message = "{validation.field.oauthCode.size}") private String authCode; + + /// 抖音匿名登录凭证(tt.login 的 anonymous_code), 仅抖音使用; 与 authCode 至少传一个 + @Schema(description = "抖音匿名授权码") + @Size(max = 128, message = "{validation.field.oauthCode.size}") + private String anonymousCode; } diff --git a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/develop/MchDevelopAuthController.java b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/develop/MchDevelopAuthController.java index c19c5d490..c65f3c68e 100644 --- a/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/develop/MchDevelopAuthController.java +++ b/daxpay-payment/daxpay-payment-merchant/src/main/java/cn/daxpay/open/payment/merchant/controller/develop/MchDevelopAuthController.java @@ -21,16 +21,14 @@ import org.springframework.web.bind.annotation.RestController; /// 认证调试(商户端) /// -/// 调试入口: -/// - 支付宝H5平台级配置(中间页授权链接) +/// 调试入口(均为 OAuth 链接 + queryCode 轮询): +/// - 支付宝H5平台级配置 /// - 微信公众号配置(平台级 OAuth) -/// - 抖音 H5 应用配置(平台级 silent_auth 静默授权) +/// - 抖音 H5 应用配置(平台级 silent_auth) /// - 微信支付(直连/服务商, 需商户参数) /// - 抖音支付(直连/服务商, 需商户参数) -/// - 支付宝小程序(暂未实现) -/// - 微信小程序(商户端/运营端, 暂未实现) /// -/// 已实现项均通过查询码轮询认证结果。 +/// 小程序获取 openId 已迁至收银台小程序运行时, Web 不再提供小程序调试入口。 @Validated @PermCode(menuCode = PermCodes.Develop.Auth.MENU) @Tag(name = "认证调试服务(商户端)") diff --git a/daxpay-platform/daxpay-platform-capability/capability-douyin/src/main/java/cn/daxpay/open/platform/capability/douyin/auth/service/DouyinMaAuthService.java b/daxpay-platform/daxpay-platform-capability/capability-douyin/src/main/java/cn/daxpay/open/platform/capability/douyin/auth/service/DouyinMaAuthService.java index 2771c980e..311e0f336 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-douyin/src/main/java/cn/daxpay/open/platform/capability/douyin/auth/service/DouyinMaAuthService.java +++ b/daxpay-platform/daxpay-platform-capability/capability-douyin/src/main/java/cn/daxpay/open/platform/capability/douyin/auth/service/DouyinMaAuthService.java @@ -28,19 +28,33 @@ public class DouyinMaAuthService { /// 通过授权码换取用户 openId /// - /// 调用抖音小程序 `jscode2session` 接口(POST JSON), - /// `code` 可为 `tt.login` 返回的 `code`(用户授权) 或 `anonymous_code`(静默授权)。 - /// 优先取 `openid`, 其次 `anonymous_openid`。 + /// 调用抖音小程序 `jscode2session` 接口(POST JSON)。 + /// 官方要求 `code` 与 `anonymous_code` 为**分字段**, 至少传一个: + /// - 非匿名登录: 传 `code`(必有 openid); 同时有 `anonymous_code` 时一并传用于数据同步 + /// - 匿名登录: 仅传 `anonymous_code`(返回 anonymous_openid) /// - /// @param appId 抖音小程序 AppId(即开放平台 clientKey) - /// @param appSecret 抖音小程序 AppSecret(即开放平台 clientSecret) - /// @param code tt.login 返回的 code 或 anonymous_code + /// 响应优先取 `openid`, 其次 `anonymous_openid`。 + /// + /// @param appId 抖音小程序 AppId(即开放平台 clientKey) + /// @param appSecret 抖音小程序 AppSecret(即开放平台 clientSecret) + /// @param code tt.login 返回的登录凭证(可空) + /// @param anonymousCode tt.login 返回的匿名登录凭证(可空) /// @return 授权结果(openId) - public DouyinAuthResult getOpenId(String appId, String appSecret, String code) { + public DouyinAuthResult getOpenId(String appId, String appSecret, String code, String anonymousCode) { + if (StrUtil.isAllBlank(code, anonymousCode)) { + // 抖音: 换取用户标识失败: {0} + throw new OperationFailException("error.douyin.authFailed", "code and anonymous_code are blank"); + } JSONObject requestBody = new JSONObject(); requestBody.set("appid", appId); requestBody.set("secret", appSecret); - requestBody.set("code", code); + // 官方分字段: 有值才写入, 禁止把 anonymous_code 塞进 code + if (StrUtil.isNotBlank(code)) { + requestBody.set("code", code); + } + if (StrUtil.isNotBlank(anonymousCode)) { + requestBody.set("anonymous_code", anonymousCode); + } String body; try (HttpResponse response = HttpUtil.createPost(JSCODE2SESSION_URL) diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java index 1abc80d46..08c6f7511 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java @@ -3,9 +3,11 @@ package cn.daxpay.open.platform.system.mobile.config; import lombok.Data; import lombok.experimental.Accessors; -/// # 抖音小程序应用配置(落库 JSON 形状, 预留) +/// # 抖音小程序应用配置(落库 JSON 形状) /// /// 序列化后写入 [cn.daxpay.open.platform.system.entity.mobile.MobileApp#appConfig] 并加密存储。 +/// 供收银台小程序认证([cn.daxpay.open.payment.trade.runtime.service.pay.gateway.CashierAuthService]) +/// 调用 jscode2session 使用。 @Data @Accessors(chain = true) public class DyMiniAppConfig {