mirror of
https://gitee.com/bootx/dax-pay-h5
synced 2026-08-08 14:15:36 +08:00
feat(douyin): 页面级预取 JSAPI 验签包,缩短调起等待
OAuth 完成后预加载 SDK 与 jsapi-config,点支付复用缓存;同步补齐调起上下文与 dypay 新旧回调兼容。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import PayMethodIcon from '@/shared/components/pay/PayMethodIcon.vue'
|
||||
import QrCodeDisplay from '@/shared/components/pay/QrCodeDisplay.vue'
|
||||
import { useGatewayOrderPoll } from '@/shared/hooks/use-gateway-order-poll'
|
||||
import { closeWebview } from '@/shared/pay/close-webview'
|
||||
import { prefetchDouyinJsapi } from '@/shared/pay/douyin'
|
||||
import { useGatewayAuth } from '@/shared/pay/use-gateway-auth'
|
||||
import { detectClientEnv, isValidH5ClientEnv } from '@/shared/utils/client-env'
|
||||
import { formatDateTime } from '@/shared/utils/datetime'
|
||||
@@ -305,8 +306,16 @@ async function loadPage() {
|
||||
}
|
||||
// 授权成功回跳后自动继续支付:保持 ready=false 让遮罩持续到支付发起
|
||||
if (!isTerminal.value && !loadError.value && route.query.autoPay === '1' && selectId.value) {
|
||||
// 抖音: OAuth 回跳后 URL(含 autoPay query) 已稳定, 预取后再自动支付
|
||||
if (clientEnvParam === 'douyin') {
|
||||
prefetchDouyinJsapi({ orderNo })
|
||||
}
|
||||
await pay()
|
||||
}
|
||||
else if (!isTerminal.value && !loadError.value && clientEnvParam === 'douyin') {
|
||||
// 可交互页: 预取 SDK + jsapi-config(点支付复用; 若随后 OAuth 回跳会按新 URL 再预取)
|
||||
prefetchDouyinJsapi({ orderNo })
|
||||
}
|
||||
// 支付未触发或已结束(未跳走):渲染业务内容
|
||||
ready.value = true
|
||||
}
|
||||
@@ -336,7 +345,14 @@ const gatewayAuth = useGatewayAuth({
|
||||
const returnPath
|
||||
= `/cashier/${encodeURIComponent(orderNo)}/${clientEnvParam}`
|
||||
+ `?itemId=${encodeURIComponent(selectId.value)}&autoPay=1`
|
||||
return generateGatewayAuthUrl({ orderNo, authType, returnPath })
|
||||
return generateGatewayAuthUrl({
|
||||
orderNo,
|
||||
authType,
|
||||
returnPath,
|
||||
clientEnv: clientEnvParam,
|
||||
itemId: selectId.value,
|
||||
cashierType: 'h5',
|
||||
})
|
||||
},
|
||||
// 抛错由 pay() 的 try/catch 接管, 与原实现行为一致
|
||||
onError: (msg) => {
|
||||
@@ -372,7 +388,7 @@ async function ensureOpenIdOrRedirect(): Promise<string | null> {
|
||||
* 处理 JSAPI 调起结果
|
||||
*/
|
||||
async function handleJsapi(payload: string) {
|
||||
const status = await invokeJsapiByEnv(clientEnvParam, payload)
|
||||
const status = await invokeJsapiByEnv(clientEnvParam, payload, { orderNo })
|
||||
if (status === 'ok') {
|
||||
// JSAPI 调起支付成功: 仅更新状态,成功卡片由 resultState 渲染、倒计时跳转由 watch 触发
|
||||
order.value.status = 'paid'
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getCodePayInfo,
|
||||
} from '@/shared/api/code-pay'
|
||||
import { closeWebview } from '@/shared/pay/close-webview'
|
||||
import { prefetchDouyinJsapi } from '@/shared/pay/douyin'
|
||||
import { invokeJsapiByEnv } from '@/shared/pay/jsapi'
|
||||
import { useGatewayAuth } from '@/shared/pay/use-gateway-auth'
|
||||
import { isAmountOverMax, yuanToFen } from '@/shared/utils/pay-amount'
|
||||
@@ -111,6 +112,7 @@ export function useCodePayPage(options: UseCodePayPageOptions) {
|
||||
// 仅 needOpenId===true 且尚未拿到 openId → 整段 OAuth(回跳即换 openId)
|
||||
// false / null / 缺省: 不跳转,可直接收款
|
||||
if (info.value.needOpenId === true && !openId.value) {
|
||||
// 禁止在跳转授权前预取(回跳 URL 可能变化, 签名会失效)
|
||||
const ok = await gatewayAuth.ensureOpenId(true, openId.value)
|
||||
if (!ok) {
|
||||
// 已跳转 OAuth 或失败(loadError 已由 onError 写入); 失败时显示错误卡
|
||||
@@ -120,6 +122,10 @@ export function useCodePayPage(options: UseCodePayPageOptions) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 抖音: OAuth 完成或无需授权后预取 SDK + jsapi-config(点支付复用)
|
||||
if (clientEnv === 'douyin') {
|
||||
prefetchDouyinJsapi({ code })
|
||||
}
|
||||
// 正常态:可渲染码牌收款 UI
|
||||
ready.value = true
|
||||
}
|
||||
@@ -201,7 +207,7 @@ export function useCodePayPage(options: UseCodePayPageOptions) {
|
||||
return
|
||||
case 'jsapi':
|
||||
try {
|
||||
await invokeJsapiByEnv(clientEnv, action.payload)
|
||||
await invokeJsapiByEnv(clientEnv, action.payload, { code })
|
||||
markPaid(result)
|
||||
}
|
||||
catch (e: any) {
|
||||
|
||||
@@ -116,6 +116,14 @@ export function generateGatewayAuthUrl(data: {
|
||||
orderNo: string
|
||||
authType: string
|
||||
returnPath: string
|
||||
/** 客户端环境(微信/抖音通道解析用) */
|
||||
clientEnv?: string
|
||||
/** 运行形态 h5/mini */
|
||||
runtime?: string
|
||||
/** 收银台支付项 ID */
|
||||
itemId?: string
|
||||
/** 收银台类型 h5/web/mini */
|
||||
cashierType?: string
|
||||
}): Promise<GatewayAuthUrlResult> {
|
||||
return http.request<GatewayAuthUrlResult>({
|
||||
url: '/unipay/gateway/auth/generate-url',
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
* 3. `sdk.config({...})` 通过签名验证 → `sdk.ready` 回调
|
||||
* 4. `sdk.ttcjpay.dypay({sdk_info, success, fail})` 拉起抖音支付
|
||||
*
|
||||
* 页面级预取: OAuth 完成后可调用 prefetchDouyinJsapi, 并行加载 SDK + 验签包;
|
||||
* 点支付时 invokeDouyinJsapi 复用缓存, 去掉调起路径上的额外 RTT。
|
||||
*
|
||||
* 参考文档:
|
||||
* - JSAPI 调起: https://pay.douyinpay.com/wiki/639fd48f17c2f3021d237f61/64413fddc6217f024ae23ae9.md
|
||||
* - JS 接入指南: https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/js/js-access
|
||||
@@ -34,15 +37,63 @@ export interface DouyinJsapiConfig {
|
||||
signature: string
|
||||
}
|
||||
|
||||
/** ttcjpay.dypay 响应(res.code 是外层 JSB 状态, res.data.code 是支付状态) */
|
||||
/**
|
||||
* ttcjpay.dypay 响应
|
||||
* - 旧版 bridge.call: code 为数字型 JSB 状态, 支付结果在 data.code
|
||||
* - 新版 sdk.ttcjpay.dypay(≥1.0.17): 支付结果在顶层 code(字符串 '0'/'1'/…), 双端结构一致
|
||||
*/
|
||||
interface DypayResult {
|
||||
/** JSB 调用返回码: 1=成功, -1/-100=无权限, -2=客户端无此方法, -3=参数错误, 0=失败 */
|
||||
code?: number
|
||||
/**
|
||||
* 旧版: 数字 JSB 码(1=成功, -1/-100=无权限, -2=无方法, -3=参数错, 0=失败)
|
||||
* 新版: 字符串支付码('0'=成功, '1'=取消, '2'=失败 …)
|
||||
*/
|
||||
code?: number | string
|
||||
data?: {
|
||||
/** 支付状态: '0'=成功, '1'=取消, '2'=失败, '3'=业务传参错误, '4'=下单失败, '-1'=未知 */
|
||||
/** 旧版支付状态: '0'=成功, '1'=取消, '2'=失败, '3'=业务传参错误, '4'=下单失败, '-1'=未知 */
|
||||
code?: string
|
||||
msg?: string
|
||||
}
|
||||
/** 新版失败文案(部分回调可能带 message) */
|
||||
msg?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** 抖音 JSAPI 验签上下文(与后端通道应用解析对齐) */
|
||||
export interface DouyinJsapiContext {
|
||||
orderNo?: string
|
||||
code?: string
|
||||
channelMchNo?: string
|
||||
capability?: string
|
||||
channelAppId?: string
|
||||
}
|
||||
|
||||
/** 页面级预取缓存(url + ctx 一致时复用) */
|
||||
interface DouyinPrefetchCache {
|
||||
key: string
|
||||
configPromise: Promise<DouyinJsapiConfig>
|
||||
}
|
||||
|
||||
let prefetchCache: DouyinPrefetchCache | null = null
|
||||
|
||||
/**
|
||||
* 当前页 URL(去掉 # 及后面), 作为 sdk.config 的 url 参数
|
||||
*/
|
||||
function currentPageUrl(): string {
|
||||
return location.href.split('#')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化预取缓存 key(url + 上下文字段)
|
||||
*/
|
||||
function buildPrefetchKey(url: string, ctx?: DouyinJsapiContext): string {
|
||||
return [
|
||||
url,
|
||||
ctx?.orderNo || '',
|
||||
ctx?.code || '',
|
||||
ctx?.channelMchNo || '',
|
||||
ctx?.capability || '',
|
||||
ctx?.channelAppId || '',
|
||||
].join('|')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,18 +116,85 @@ export async function ensureDouyinSdk(): Promise<void> {
|
||||
* 调后端接口获取 sdk.config 验签包
|
||||
*
|
||||
* @param url 当前页面 URL(不含 # 及后面部分)
|
||||
* @param ctx 通道上下文(与 OAuth 同源; orderNo / code / channelMchNo 三选一)
|
||||
*/
|
||||
export async function fetchDouyinJsapiConfig(url: string): Promise<DouyinJsapiConfig> {
|
||||
export async function fetchDouyinJsapiConfig(
|
||||
url: string,
|
||||
ctx?: DouyinJsapiContext,
|
||||
): Promise<DouyinJsapiConfig> {
|
||||
return http.request<DouyinJsapiConfig>({
|
||||
url: DOUYIN_JSAPI_CONFIG_URL,
|
||||
method: RequestEnum.GET,
|
||||
params: { url },
|
||||
params: {
|
||||
url,
|
||||
orderNo: ctx?.orderNo,
|
||||
code: ctx?.code,
|
||||
channelMchNo: ctx?.channelMchNo,
|
||||
capability: ctx?.capability,
|
||||
channelAppId: ctx?.channelAppId,
|
||||
},
|
||||
}, {
|
||||
// 由调用方自行处理错误展示
|
||||
isShowMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面级预取: 并行加载 JS-SDK + 拉取 sdk.config 验签包
|
||||
*
|
||||
* 须在 OAuth 完成(或无需授权)、当前页 URL 已稳定后调用;
|
||||
* 失败仅 console.warn, 不打断页面, 调起时走即时拉取兜底。
|
||||
*
|
||||
* @param ctx 通道上下文(orderNo/code 等, 与 invokeDouyinJsapi 一致)
|
||||
*/
|
||||
export function prefetchDouyinJsapi(ctx?: DouyinJsapiContext): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const url = currentPageUrl()
|
||||
const key = buildPrefetchKey(url, ctx)
|
||||
// 同 key 已在预取中或已完成, 不重复发起
|
||||
if (prefetchCache?.key === key) {
|
||||
return
|
||||
}
|
||||
const configPromise = fetchDouyinJsapiConfig(url, ctx).catch((e) => {
|
||||
// 预取失败丢弃缓存, 调起时重新拉
|
||||
if (prefetchCache?.key === key) {
|
||||
prefetchCache = null
|
||||
}
|
||||
throw e
|
||||
})
|
||||
prefetchCache = { key, configPromise }
|
||||
// 并行加载 SDK(失败同样静默, 调起时再试)
|
||||
void ensureDouyinSdk().catch((e) => {
|
||||
console.warn('[douyin] prefetch sdk failed', e)
|
||||
})
|
||||
void configPromise.catch((e) => {
|
||||
console.warn('[douyin] prefetch jsapi-config failed', e)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取预取缓存中的验签包; key 不匹配或失败则即时拉取
|
||||
*/
|
||||
async function resolveJsapiConfig(
|
||||
url: string,
|
||||
ctx?: DouyinJsapiContext,
|
||||
): Promise<DouyinJsapiConfig> {
|
||||
const key = buildPrefetchKey(url, ctx)
|
||||
if (prefetchCache?.key === key) {
|
||||
try {
|
||||
return await prefetchCache.configPromise
|
||||
}
|
||||
catch {
|
||||
// 预取失败, 下面即时拉取
|
||||
}
|
||||
}
|
||||
const configPromise = fetchDouyinJsapiConfig(url, ctx)
|
||||
prefetchCache = { key, configPromise }
|
||||
return configPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 sdk.config + sdk.ready, 完成 JSBridge 权限校验
|
||||
* sdk.error 视为验签失败, reject
|
||||
@@ -162,44 +280,53 @@ export function callDypay(sdkInfoJson: string): Promise<DypayResult> {
|
||||
/**
|
||||
* 抖音 JSAPI 调起主入口
|
||||
*
|
||||
* 完整流程: 注入 SDK → 拉取验签包 → sdk.config → ttcjpay.dypay → 解析结果
|
||||
* 完整流程: 注入 SDK → 拉取验签包(优先预取缓存) → sdk.config → ttcjpay.dypay → 解析结果
|
||||
*
|
||||
* @param payBody 通道返回的 sdk_info JSON 字符串
|
||||
* @param ctx 通道上下文(orderNo/code 等, 与 OAuth 同源解析网站应用)
|
||||
* @throws Error('cancel') 用户取消
|
||||
* @throws Error 其他失败
|
||||
*/
|
||||
export async function invokeDouyinJsapi(payBody: string): Promise<void> {
|
||||
export async function invokeDouyinJsapi(
|
||||
payBody: string,
|
||||
ctx?: DouyinJsapiContext,
|
||||
): Promise<void> {
|
||||
// 当前页面 URL(去掉 # 及后面), 作为 sdk.config 的 url 参数
|
||||
const url = location.href.split('#')[0]
|
||||
const url = currentPageUrl()
|
||||
|
||||
// 1. 注入 JS-SDK
|
||||
// 1. 注入 JS-SDK(预取可能已完成)
|
||||
await ensureDouyinSdk()
|
||||
// 2. 拉取 sdk.config 验签包
|
||||
const cfg = await fetchDouyinJsapiConfig(url)
|
||||
// 2. 验签包: 优先用页面级预取缓存
|
||||
const cfg = await resolveJsapiConfig(url, ctx)
|
||||
// 3. sdk.config 验签
|
||||
await douyinSdkConfig(cfg)
|
||||
// 4. ttcjpay.dypay 调起支付
|
||||
const res = await callDypay(payBody)
|
||||
|
||||
// 外层 code 校验(JSB_NO_PERMISSION/JSB_NO_HANDLER 等需明确报错)
|
||||
// 仅数字型外层码才是旧版 JSB 错误(新版支付结果也是字符串 '0'/'1', 勿当 JSB 码)
|
||||
const outerCode = res.code
|
||||
if (outerCode === -1 || outerCode === -100) {
|
||||
throw new Error('Douyin JSB no permission (check JSBridge security domain config)')
|
||||
}
|
||||
if (outerCode === -2) {
|
||||
throw new Error('Douyin JSB no handler (upgrade Douyin App)')
|
||||
}
|
||||
if (outerCode === -3) {
|
||||
throw new Error('Douyin JSB params error (sdk_info invalid)')
|
||||
if (typeof outerCode === 'number') {
|
||||
if (outerCode === -1 || outerCode === -100) {
|
||||
throw new Error('Douyin JSB no permission (check JSBridge security domain config)')
|
||||
}
|
||||
if (outerCode === -2) {
|
||||
throw new Error('Douyin JSB no handler (upgrade Douyin App)')
|
||||
}
|
||||
if (outerCode === -3) {
|
||||
throw new Error('Douyin JSB params error (sdk_info invalid)')
|
||||
}
|
||||
}
|
||||
|
||||
// 支付状态码(在 res.data.code)
|
||||
const payCode = res.data?.code
|
||||
// 支付状态码: 优先旧版 data.code, 兼容新版顶层字符串 code
|
||||
const payCode
|
||||
= res.data?.code
|
||||
?? (typeof res.code === 'string' ? res.code : undefined)
|
||||
if (payCode === '0') {
|
||||
return
|
||||
}
|
||||
if (payCode === '1') {
|
||||
throw new Error('cancel')
|
||||
}
|
||||
throw new Error(`Douyin pay failed: ${payCode || 'unknown'} ${res.data?.msg || ''}`)
|
||||
const failMsg = res.data?.msg || res.msg || res.message || ''
|
||||
throw new Error(`Douyin pay failed: ${payCode || 'unknown'} ${failMsg}`.trim())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* payBody 一般为通道返回的 JSON 字符串
|
||||
*/
|
||||
|
||||
import type { DouyinJsapiContext } from '@/shared/pay/douyin'
|
||||
import { invokeDouyinJsapi } from '@/shared/pay/douyin'
|
||||
|
||||
export interface WechatJsapiPayload {
|
||||
@@ -139,8 +140,15 @@ export function invokeAlipayJsapi(payBody: string): Promise<void> {
|
||||
|
||||
/**
|
||||
* 按 clientEnv 调起 JSAPI
|
||||
* @param clientEnv
|
||||
* @param payBody
|
||||
* @param douyinCtx 抖音验签上下文(orderNo/code), 与通道 OAuth 同源
|
||||
*/
|
||||
export async function invokeJsapiByEnv(clientEnv: string, payBody: string): Promise<void> {
|
||||
export async function invokeJsapiByEnv(
|
||||
clientEnv: string,
|
||||
payBody: string,
|
||||
douyinCtx?: DouyinJsapiContext,
|
||||
): Promise<void> {
|
||||
if (clientEnv === 'wechat') {
|
||||
await invokeWechatJsapi(payBody)
|
||||
return
|
||||
@@ -152,7 +160,7 @@ export async function invokeJsapiByEnv(clientEnv: string, payBody: string): Prom
|
||||
if (clientEnv === 'douyin') {
|
||||
// 抖音: 动态加载 JS-SDK + sdk.config 验签 + ttcjpay.dypay
|
||||
// 抛出 Error('cancel') 表示用户取消, 其他 Error 表示失败
|
||||
await invokeDouyinJsapi(payBody)
|
||||
await invokeDouyinJsapi(payBody, douyinCtx)
|
||||
return
|
||||
}
|
||||
// 云闪付等: 一期无统一桥, 抛出由上层轮询兜底
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/shared/api/gateway'
|
||||
import { useGatewayOrderPoll } from '@/shared/hooks/use-gateway-order-poll'
|
||||
import { closeWebview } from '@/shared/pay/close-webview'
|
||||
import { prefetchDouyinJsapi } from '@/shared/pay/douyin'
|
||||
import { invokeJsapiByEnv } from '@/shared/pay/jsapi'
|
||||
import { useGatewayAuth } from '@/shared/pay/use-gateway-auth'
|
||||
import { buildAggregateEnvPath } from '@/shared/utils/client-env'
|
||||
@@ -159,7 +160,13 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
|
||||
const returnPath = buildAggregateEnvPath(orderNo, clientEnv)
|
||||
// clientEnv='union-pay' 已在 ensureOpenId 中提前 return, 不会进入此分支
|
||||
const authType = clientEnv === 'douyin' ? 'douyin' : clientEnv
|
||||
return generateGatewayAuthUrl({ orderNo, authType, returnPath })
|
||||
return generateGatewayAuthUrl({
|
||||
orderNo,
|
||||
authType,
|
||||
returnPath,
|
||||
clientEnv,
|
||||
runtime: 'h5',
|
||||
})
|
||||
},
|
||||
onError: msg => onError?.(msg),
|
||||
failKey: 'aggregate.authFail',
|
||||
@@ -218,7 +225,7 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
|
||||
break
|
||||
case 'jsapi':
|
||||
try {
|
||||
await invokeJsapiByEnv(clientEnv, action.payload)
|
||||
await invokeJsapiByEnv(clientEnv, action.payload, { orderNo })
|
||||
// JSAPI 桥成功 → 立即标记已支付,模板切到整页成功卡片(与收银台/码牌语义一致)
|
||||
order.value.status = 'paid'
|
||||
clearCachedOrder(orderNo)
|
||||
@@ -322,9 +329,14 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
|
||||
})
|
||||
if (meta.value.needOpenId && !openId.value) {
|
||||
// 需要 OAuth:保持 ready=false,InitLoadingMask 持续显示直到跳转
|
||||
// 禁止在跳转授权前预取(回跳 URL 带 query, 签名会失效)
|
||||
await ensureOpenId()
|
||||
return
|
||||
}
|
||||
// 抖音: OAuth 完成或无需授权后预取 SDK + jsapi-config(点支付复用)
|
||||
if (clientEnv === 'douyin') {
|
||||
prefetchDouyinJsapi({ orderNo })
|
||||
}
|
||||
// 仅配置显式 autoLaunch=true 时自动拉起
|
||||
if (meta.value.autoLaunch === true) {
|
||||
// autoLaunch:保持 ready=false,由 paying 接管遮罩,避免订单卡闪现
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 容器内 JSAPI 调起(微信公众号 / 支付宝生活号 / 抖音 H5)
|
||||
*/
|
||||
|
||||
import type { DouyinJsapiContext } from '@/shared/pay/douyin'
|
||||
import { invokeDouyinJsapi } from '@/shared/pay/douyin'
|
||||
|
||||
/** 微信 JSAPI 调起参数(通道 payBody JSON) */
|
||||
@@ -142,10 +143,12 @@ export async function invokeAlipayTradePay(payBody: string): Promise<'ok' | 'can
|
||||
|
||||
/**
|
||||
* 按 clientEnv 调起 JSAPI
|
||||
* @param douyinCtx 抖音验签上下文(orderNo/code), 与通道 OAuth 同源
|
||||
*/
|
||||
export async function invokeJsapiByEnv(
|
||||
clientEnv: string,
|
||||
payBody: string,
|
||||
douyinCtx?: DouyinJsapiContext,
|
||||
): Promise<'ok' | 'cancel' | 'fail' | 'unsupported'> {
|
||||
if (clientEnv === 'wechat') {
|
||||
return invokeWechatJsapi(payBody)
|
||||
@@ -156,7 +159,7 @@ export async function invokeJsapiByEnv(
|
||||
if (clientEnv === 'douyin') {
|
||||
// 抖音: 调用 invokeDouyinJsapi(throw 'cancel' 表示取消, 其他 Error 表示失败)
|
||||
try {
|
||||
await invokeDouyinJsapi(payBody)
|
||||
await invokeDouyinJsapi(payBody, douyinCtx)
|
||||
return 'ok'
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user