mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-11 15:15:33 +08:00
feat(douyin): H5 JSAPI 调起前置 sdk.config 验签链路
抖音 H5 在 APP webview 内调用 ttcjpay.dypay 前必须通过 DouyinOpenJSBridge.config 验签, 需基于 jsapi_ticket + url 计算 MD5 signature。 - 主应用 DouyinPayService.toPayResult 透传 JSAPI 类型, client/enums 同步加 JSAPI 枚举 - capability-douyin 新增 DouyinOpenTokenService: client_token(POST) + jsapi_ticket(GET, 注意 getticket 一个词) Redis 缓存(7200s, 提前 5 分钟刷新) + LockExecutor 防并发刷新, buildJsapiConfig 算 MD5 signature - 新增 DouyinJsapiController: GET /unipay/assist/channel/douyin/jsapi-config, @IgnoreAuth 不强制商户签名 - 补齐 error.channel.douyin.* 5 个 message key, 10 语同步
This commit is contained in:
@@ -8,6 +8,8 @@ public enum DouyinPayBodyType {
|
||||
QR_CODE,
|
||||
/// 跳转链接(H5 支付)
|
||||
LINK,
|
||||
/// 标识符(JSAPI/APP 返回的 prepayId)
|
||||
/// JSAPI 调起参数(JSON, 含 appId/timeStamp/nonceStr/package/signType/paySign)
|
||||
JSAPI,
|
||||
/// 标识符(APP 返回的 prepayId)
|
||||
IDENTIFIER
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public class DouyinPayService {
|
||||
switch (bodyType) {
|
||||
case QR_CODE -> bo.setPayBodyType(PayBodyTypeEnum.QR_CODE);
|
||||
case LINK -> bo.setPayBodyType(PayBodyTypeEnum.LINK);
|
||||
case JSAPI -> bo.setPayBodyType(PayBodyTypeEnum.JSAPI);
|
||||
case IDENTIFIER -> bo.setPayBodyType(PayBodyTypeEnum.IDENTIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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<DouyinJsapiConfigResult> 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);
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,11 @@
|
||||
<artifactId>hutool-json</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<!-- Redis + 分布式锁封装(JSAPI 验签所需的 client_token / jsapi_ticket 缓存) -->
|
||||
<dependency>
|
||||
<groupId>cn.daxpay.open</groupId>
|
||||
<artifactId>common-redis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.daxpay.open.platform.capability.douyin.auth.result;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 抖音 JS-SDK config 验签结果
|
||||
///
|
||||
/// 返回给前端的 `window.DouyinOpenJSBridge.config()` 必需参数包,
|
||||
/// 前端拿到后直接透传给 `sdk.config({params: {...}})` 完成鉴权。
|
||||
///
|
||||
/// 参考文档:
|
||||
/// - 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
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DouyinJsapiConfigResult {
|
||||
|
||||
/// 抖音开放平台 Client Key(网站应用 appid)
|
||||
private String clientKey;
|
||||
|
||||
/// 时间戳(秒, 字符串)
|
||||
private String timestamp;
|
||||
|
||||
/// 随机字符串
|
||||
private String nonceStr;
|
||||
|
||||
/// 服务端计算的 MD5 签名(基于 jsapi_ticket + nonce_str + timestamp + url)
|
||||
private String signature;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package cn.daxpay.open.platform.capability.douyin.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.capability.douyin.auth.result.DouyinJsapiConfigResult;
|
||||
import cn.daxpay.open.platform.common.redis.lock.LockExecutor;
|
||||
import cn.daxpay.open.platform.common.redis.lock.TryLockResult;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/// # 抖音开放平台 Token / Ticket 管理服务
|
||||
///
|
||||
/// 用于 H5 JSAPI 调起前置 `sdk.config` 验签:
|
||||
/// - **client_access_token**: 由 client_key + client_secret 换取, 缓存 ~7200s
|
||||
/// - **jsapi_ticket**: 由 client_access_token 换取, 缓存 ~7200s
|
||||
///
|
||||
/// 参考范式: [cn.daxpay.open.platform.capability.wechat.token.service.WechatTokenService]
|
||||
///
|
||||
/// 参考文档:
|
||||
/// - client_token: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/account-permission/client-token
|
||||
/// - get_ticket: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/tools-ability/jsb-management/get-jsticket
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DouyinOpenTokenService {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
private final LockExecutor lockExecutor;
|
||||
|
||||
/// client_token 换取地址
|
||||
private static final String CLIENT_TOKEN_URL = "https://open.douyin.com/oauth/client_token/";
|
||||
/// jsapi_ticket 换取地址(注意是 getticket 一个词, 无下划线; HTTP 方法为 GET, 不要 body)
|
||||
private static final String JSAPI_TICKET_URL = "https://open.douyin.com/js/getticket/";
|
||||
|
||||
/// Redis Key 前缀
|
||||
private static final String CLIENT_TOKEN_KEY = "douyin:open:client_token:";
|
||||
private static final String TICKET_KEY = "douyin:open:jsapi_ticket:";
|
||||
private static final String CLIENT_TOKEN_LOCK_KEY = "douyin:open:client_token:lock:";
|
||||
private static final String TICKET_LOCK_KEY = "douyin:open:jsapi_ticket:lock:";
|
||||
|
||||
/// 缓存过期时间(抖音 token/ticket 有效期 7200s, 提前 5 分钟刷新)
|
||||
private static final Duration CACHE_EXPIRE = Duration.ofSeconds(7200 - 300);
|
||||
|
||||
/// 获取 client_access_token(自动刷新, 支持多副本部署)
|
||||
public String getClientAccessToken(String clientKey, String clientSecret) {
|
||||
String cacheKey = CLIENT_TOKEN_KEY + clientKey;
|
||||
String token = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
return refreshClientAccessToken(clientKey, clientSecret);
|
||||
}
|
||||
|
||||
/// 强制刷新 client_access_token(分布式锁 + 双重检查)
|
||||
public String refreshClientAccessToken(String clientKey, String clientSecret) {
|
||||
String lockKey = CLIENT_TOKEN_LOCK_KEY + clientKey;
|
||||
String cacheKey = CLIENT_TOKEN_KEY + clientKey;
|
||||
TryLockResult<String> result = lockExecutor.tryExecute(lockKey,
|
||||
30_000L, 5_000L, () -> {
|
||||
// 双重检查
|
||||
String cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
log.info("刷新抖音 client_access_token, clientKey: {}", clientKey);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("client_key", clientKey);
|
||||
body.put("client_secret", clientSecret);
|
||||
body.put("grant_type", "client_credential");
|
||||
String token = postJsonAndExtract(CLIENT_TOKEN_URL, body, "access_token");
|
||||
redisTemplate.opsForValue().set(cacheKey, token, CACHE_EXPIRE);
|
||||
log.info("刷新抖音 client_access_token 成功, clientKey: {}", clientKey);
|
||||
return token;
|
||||
});
|
||||
if (!result.acquired()) {
|
||||
log.warn("获取抖音 client_access_token 刷新锁失败, clientKey: {}", clientKey);
|
||||
String cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR,
|
||||
"error.channel.douyin.tokenLockFailed");
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
/// 获取 jsapi_ticket(自动刷新, 依赖 client_access_token)
|
||||
public String getJsapiTicket(String clientKey, String clientSecret) {
|
||||
String cacheKey = TICKET_KEY + clientKey;
|
||||
String ticket = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(ticket)) {
|
||||
return ticket;
|
||||
}
|
||||
return refreshJsapiTicket(clientKey, clientSecret);
|
||||
}
|
||||
|
||||
/// 强制刷新 jsapi_ticket(分布式锁 + 双重检查)
|
||||
public String refreshJsapiTicket(String clientKey, String clientSecret) {
|
||||
String lockKey = TICKET_LOCK_KEY + clientKey;
|
||||
String cacheKey = TICKET_KEY + clientKey;
|
||||
TryLockResult<String> result = lockExecutor.tryExecute(lockKey,
|
||||
30_000L, 5_000L, () -> {
|
||||
String cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
String accessToken = getClientAccessToken(clientKey, clientSecret);
|
||||
log.info("刷新抖音 jsapi_ticket, clientKey: {}", clientKey);
|
||||
// getticket 是 GET + access-token header, 无 body
|
||||
// 错误用 POST/带 body 会报 28001007 参数不合法
|
||||
String ticket = getWithAuth(JSAPI_TICKET_URL, accessToken, "ticket");
|
||||
redisTemplate.opsForValue().set(cacheKey, ticket, CACHE_EXPIRE);
|
||||
log.info("刷新抖音 jsapi_ticket 成功, clientKey: {}", clientKey);
|
||||
return ticket;
|
||||
});
|
||||
if (!result.acquired()) {
|
||||
log.warn("获取抖音 jsapi_ticket 刷新锁失败, clientKey: {}", clientKey);
|
||||
String cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR,
|
||||
"error.channel.douyin.ticketLockFailed");
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
/// 生成给前端的 sdk.config 验签包(MD5 signature)
|
||||
///
|
||||
/// 签名拼接(字典序): `jsapi_ticket={}&nonce_str={}×tamp={}&url={}`
|
||||
/// 详情参考: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/js/signature
|
||||
public DouyinJsapiConfigResult buildJsapiConfig(String clientKey, String clientSecret, String url) {
|
||||
if (StrUtil.hasBlank(clientKey, clientSecret, url)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"error.channel.douyin.jsapiConfigParamBlank");
|
||||
}
|
||||
String ticket = getJsapiTicket(clientKey, clientSecret);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
String nonceStr = RandomUtil.randomString(32);
|
||||
|
||||
// 字典序拼接(jsapi_ticket < nonce_str < timestamp < url)
|
||||
String signStr = String.format("jsapi_ticket=%s&nonce_str=%s×tamp=%s&url=%s",
|
||||
ticket, nonceStr, timestamp, url);
|
||||
String signature = cn.hutool.crypto.SecureUtil.md5(signStr);
|
||||
|
||||
return new DouyinJsapiConfigResult()
|
||||
.setClientKey(clientKey)
|
||||
.setTimestamp(timestamp)
|
||||
.setNonceStr(nonceStr)
|
||||
.setSignature(signature);
|
||||
}
|
||||
|
||||
/// POST JSON 调抖音接口, 提取 data.{field}
|
||||
private String postJsonAndExtract(String url, Map<String, Object> body, String field) {
|
||||
return postJson(url, body, field);
|
||||
}
|
||||
|
||||
/// GET 调抖音接口(仅带 access-token header, 无 body), 提取 data.{field}
|
||||
///
|
||||
/// 适用于 jsapi_ticket 等纯授权接口, 参考文档:
|
||||
/// https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/tools-ability/jsb-management/get-jsticket
|
||||
private String getWithAuth(String url, String accessToken, String field) {
|
||||
HttpRequest request = HttpUtil.createGet(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("access-token", accessToken);
|
||||
return doRequestAndExtract(request, url, field);
|
||||
}
|
||||
|
||||
/// 统一 POST JSON 调用, 解析 `data.{field}`, 失败抛业务异常
|
||||
private String postJson(String url, Map<String, Object> body, String field) {
|
||||
HttpRequest request = HttpUtil.createPost(url)
|
||||
.header("Content-Type", "application/json");
|
||||
request.body(JSONUtil.toJsonStr(body));
|
||||
return doRequestAndExtract(request, url, field);
|
||||
}
|
||||
|
||||
/// 执行请求并解析 `data.{field}`, 失败抛业务异常
|
||||
private String doRequestAndExtract(HttpRequest request, String url, String field) {
|
||||
String respBody;
|
||||
try (HttpResponse response = request.execute()) {
|
||||
respBody = response.body();
|
||||
}
|
||||
log.info("抖音开放平台响应: url={}, body={}", url, respBody);
|
||||
JSONObject obj = JSONUtil.parseObj(respBody);
|
||||
JSONObject data = obj.getJSONObject("data");
|
||||
if (data == null) {
|
||||
throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR,
|
||||
"error.channel.douyin.openApiFailed", obj.getStr("message", respBody));
|
||||
}
|
||||
// 抖音错误码: data.error_code 非 0 视为失败
|
||||
int errorCode = data.getInt("error_code", -1);
|
||||
if (errorCode != 0) {
|
||||
throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR,
|
||||
"error.channel.douyin.openApiFailed",
|
||||
data.getStr("description", "error_code=" + errorCode));
|
||||
}
|
||||
String value = data.getStr(field);
|
||||
if (StrUtil.isBlank(value)) {
|
||||
throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR,
|
||||
"error.channel.douyin.openApiFailed", field + " is blank");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "Payment capability [{0}] is duplicated under this channel merchant",
|
||||
"payFailed": "Douyin payment error: {0}",
|
||||
"closeFailed": "Douyin close order error: {0}",
|
||||
"notSupportMethod": "Unsupported Douyin payment method: {0}"
|
||||
"notSupportMethod": "Unsupported Douyin payment method: {0}",
|
||||
"openApiFailed": "Douyin OpenPlatform API call failed: {0}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "Kemampuan pembayaran [{0}] diduplikasi di bawah saluran pedagang ini",
|
||||
"payFailed": "Kesalahan pembayaran Douyin: {0}",
|
||||
"closeFailed": "Kesalahan penutupan pesanan Douyin: {0}",
|
||||
"notSupportMethod": "Metode pembayaran Douyin yang tidak didukung: {0}"
|
||||
"notSupportMethod": "Metode pembayaran Douyin yang tidak didukung: {0}",
|
||||
"openApiFailed": "Panggilan API Douyin OpenPlatform gagal: {0}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "支払い機能 [{0}] はこのチャネル販売者の下で複製されています",
|
||||
"payFailed": "Douyin 支払いエラー: {0}",
|
||||
"closeFailed": "Douyin 成約注文エラー: {0}",
|
||||
"notSupportMethod": "サポートされていない Douyin 支払い方法: {0}"
|
||||
"notSupportMethod": "サポートされていない Douyin 支払い方法: {0}",
|
||||
"openApiFailed": "Douyin オープンプラットフォーム API 呼び出し失敗: {0}",
|
||||
"tokenLockFailed": "Douyin client_access_token 更新ロックの取得に失敗しました、後でもう一度お試しください",
|
||||
"ticketLockFailed": "Douyin jsapi_ticket 更新ロックの取得に失敗しました、後でもう一度お試しください",
|
||||
"jsapiConfigParamBlank": "Douyin sdk.config 署名パラメータ(clientKey/clientSecret/url)が空です",
|
||||
"h5AuthNotConfigured": "Douyin H5 アプリ認証設定が不完全です。先に「プラットフォーム設定」で clientKey/clientSecret を設定してください"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "이 채널 판매자에 결제 기능 [{0}]이(가) 중복되었습니다.",
|
||||
"payFailed": "Douyin 결제 오류: {0}",
|
||||
"closeFailed": "Douyin 마감 주문 오류: {0}",
|
||||
"notSupportMethod": "지원되지 않는 Douyin 결제 수단: {0}"
|
||||
"notSupportMethod": "지원되지 않는 Douyin 결제 수단: {0}",
|
||||
"openApiFailed": "Douyin 오픈플랫폼 API 호출 실패: {0}",
|
||||
"tokenLockFailed": "Douyin client_access_token 갱신 잠금 획득에 실패했습니다, 나중에 다시 시도해 주세요",
|
||||
"ticketLockFailed": "Douyin jsapi_ticket 갱신 잠금 획득에 실패했습니다, 나중에 다시 시도해 주세요",
|
||||
"jsapiConfigParamBlank": "Douyin sdk.config 서명 매개변수(clientKey/clientSecret/url)가 비어 있습니다",
|
||||
"h5AuthNotConfigured": "Douyin H5 앱 인증 구성이 불완전합니다. 먼저 '플랫폼 구성'에서 clientKey/clientSecret을 구성해 주세요"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "Keupayaan pembayaran [{0}] diduakan di bawah pedagang saluran ini",
|
||||
"payFailed": "Ralat pembayaran Douyin: {0}",
|
||||
"closeFailed": "Ralat tutup pesanan Douyin: {0}",
|
||||
"notSupportMethod": "Kaedah pembayaran Douyin tidak disokong: {0}"
|
||||
"notSupportMethod": "Kaedah pembayaran Douyin tidak disokong: {0}",
|
||||
"openApiFailed": "Panggilan API Douyin OpenPlatform gagal: {0}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "ความสามารถในการชำระเงิน [{0}] ซ้ำกันภายใต้ผู้ขายช่องทางนี้",
|
||||
"payFailed": "ข้อผิดพลาดในการชำระเงิน Douyin: {0}",
|
||||
"closeFailed": "ข้อผิดพลาดในการสั่งซื้อปิด Douyin: {0}",
|
||||
"notSupportMethod": "วิธีการชำระเงิน Douyin ที่ไม่รองรับ: {0}"
|
||||
"notSupportMethod": "วิธีการชำระเงิน Douyin ที่ไม่รองรับ: {0}",
|
||||
"openApiFailed": "การเรียก API แพลตฟอร์มเปิด Douyin ล้มเหลว: {0}",
|
||||
"tokenLockFailed": "ไม่สามารถรับล็อกการรีเฟรช client_access_token ของ Douyin ได้ โปรดลองอีกครั้งในภายหลัง",
|
||||
"ticketLockFailed": "ไม่สามารถรับล็อกการรีเฟรช jsapi_ticket ของ Douyin ได้ โปรดลองอีกครั้งในภายหลัง",
|
||||
"jsapiConfigParamBlank": "พารามิเตอร์ลายเซ็น sdk.config ของ Douyin (clientKey/clientSecret/url) ว่างเปล่า",
|
||||
"h5AuthNotConfigured": "การกำหนดค่าการตรวจสอบสิทธิ์แอป H5 Douyin ไม่สมบูรณ์ โปรดกำหนดค่า clientKey/clientSecret ในการกำหนดค่าแพลตฟอร์มก่อน"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "Khả năng thanh toán [{0}] bị trùng lặp trong kênh này của người bán",
|
||||
"payFailed": "Lỗi thanh toán Douyin: {0}",
|
||||
"closeFailed": "Lỗi đóng lệnh Douyin: {0}",
|
||||
"notSupportMethod": "Phương thức thanh toán Douyin không được hỗ trợ: {0}"
|
||||
"notSupportMethod": "Phương thức thanh toán Douyin không được hỗ trợ: {0}",
|
||||
"openApiFailed": "Gọi API Nền tảng mở Douyin không thành công: {0}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "同一通道商户下支付能力[{0}]重复配置",
|
||||
"payFailed": "抖音支付异常: {0}",
|
||||
"closeFailed": "抖音关闭订单异常: {0}",
|
||||
"notSupportMethod": "暂不支持的抖音支付方式: {0}"
|
||||
"notSupportMethod": "暂不支持的抖音支付方式: {0}",
|
||||
"openApiFailed": "抖音开放平台接口调用失败: {0}",
|
||||
"tokenLockFailed": "获取抖音 client_access_token 刷新锁失败, 请稍后重试",
|
||||
"ticketLockFailed": "获取抖音 jsapi_ticket 刷新锁失败, 请稍后重试",
|
||||
"jsapiConfigParamBlank": "抖音 sdk.config 验签参数(clientKey/clientSecret/url)存在空值",
|
||||
"h5AuthNotConfigured": "抖音 H5 应用认证配置不完整, 请先在「平台配置」中配置 clientKey/clientSecret"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "同一通道商户下支付能力[{0}]重複配置",
|
||||
"payFailed": "抖音支付異常: {0}",
|
||||
"closeFailed": "抖音關閉訂單異常: {0}",
|
||||
"notSupportMethod": "暫不支持的抖音支付方式: {0}"
|
||||
"notSupportMethod": "暫不支持的抖音支付方式: {0}",
|
||||
"openApiFailed": "抖音開放平台介面呼叫失敗: {0}",
|
||||
"tokenLockFailed": "取得抖音 client_access_token 更新鎖失敗, 請稍後重試",
|
||||
"ticketLockFailed": "取得抖音 jsapi_ticket 更新鎖失敗, 請稍後重試",
|
||||
"jsapiConfigParamBlank": "抖音 sdk.config 驗簽參數(clientKey/clientSecret/url)存在空值",
|
||||
"h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"capabilityDuplicate": "同一通道商戶下支付能力[{0}]重複配置",
|
||||
"payFailed": "抖音支付異常: {0}",
|
||||
"closeFailed": "抖音關閉訂單異常: {0}",
|
||||
"notSupportMethod": "暫不支援的抖音支付方式: {0}"
|
||||
"notSupportMethod": "暫不支援的抖音支付方式: {0}",
|
||||
"openApiFailed": "抖音開放平台介面呼叫失敗: {0}",
|
||||
"tokenLockFailed": "取得抖音 client_access_token 更新鎖失敗, 請稍後重試",
|
||||
"ticketLockFailed": "取得抖音 jsapi_ticket 更新鎖失敗, 請稍後重試",
|
||||
"jsapiConfigParamBlank": "抖音 sdk.config 驗簽參數(clientKey/clientSecret/url)存在空值",
|
||||
"h5AuthNotConfigured": "抖音 H5 應用認證配置不完整, 請先在「平台配置」中配置 clientKey/clientSecret"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user