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:
DaxPay Dev
2026-07-21 13:40:35 +08:00
parent 6bd6382470
commit 00c74d5492
16 changed files with 379 additions and 11 deletions

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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={}&timestamp={}&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);
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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={}&timestamp={}&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&timestamp=%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;
}
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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 を設定してください"
}

View File

@@ -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을 구성해 주세요"
}

View File

@@ -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"
}

View File

@@ -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 ในการกำหนดค่าแพลตฟอร์มก่อน"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}