支持快手行业电子凭证多店铺授权
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE kuaishou_industry_vouchers
|
||||||
|
ADD COLUMN IF NOT EXISTS seller_id TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kuaishou_industry_vouchers_seller_id
|
||||||
|
ON kuaishou_industry_vouchers(seller_id);
|
||||||
@@ -54,6 +54,7 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
order_id,
|
order_id,
|
||||||
task_id,
|
task_id,
|
||||||
unit_index,
|
unit_index,
|
||||||
|
seller_id,
|
||||||
token,
|
token,
|
||||||
status,
|
status,
|
||||||
valid_start_time,
|
valid_start_time,
|
||||||
@@ -77,15 +78,20 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
$8,
|
$8,
|
||||||
$9,
|
$9,
|
||||||
$10,
|
$10,
|
||||||
$11::jsonb,
|
$11,
|
||||||
$12,
|
$12::jsonb,
|
||||||
$13,
|
$13,
|
||||||
$14::jsonb,
|
$14,
|
||||||
$15,
|
$15::jsonb,
|
||||||
$16
|
$16,
|
||||||
|
$17
|
||||||
)
|
)
|
||||||
ON CONFLICT (oid, unit_index) DO UPDATE
|
ON CONFLICT (oid, unit_index) DO UPDATE
|
||||||
SET
|
SET
|
||||||
|
seller_id = CASE
|
||||||
|
WHEN EXCLUDED.seller_id <> '' THEN EXCLUDED.seller_id
|
||||||
|
ELSE kuaishou_industry_vouchers.seller_id
|
||||||
|
END,
|
||||||
token = CASE
|
token = CASE
|
||||||
WHEN EXCLUDED.token <> '' THEN EXCLUDED.token
|
WHEN EXCLUDED.token <> '' THEN EXCLUDED.token
|
||||||
ELSE kuaishou_industry_vouchers.token
|
ELSE kuaishou_industry_vouchers.token
|
||||||
@@ -104,6 +110,7 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
normalizeNullableId(input.orderId),
|
normalizeNullableId(input.orderId),
|
||||||
normalizeNullableId(input.taskId),
|
normalizeNullableId(input.taskId),
|
||||||
input.unitIndex,
|
input.unitIndex,
|
||||||
|
input.sellerId || '',
|
||||||
input.token || '',
|
input.token || '',
|
||||||
input.status || 'UNUSED',
|
input.status || 'UNUSED',
|
||||||
input.validStartTime || 0,
|
input.validStartTime || 0,
|
||||||
@@ -234,6 +241,10 @@ function normalizeVoucherPatchColumns(
|
|||||||
columns.push({ column: 'token', value: patch.token || '' })
|
columns.push({ column: 'token', value: patch.token || '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.sellerId !== undefined) {
|
||||||
|
columns.push({ column: 'seller_id', value: patch.sellerId || '' })
|
||||||
|
}
|
||||||
|
|
||||||
if (patch.status !== undefined) {
|
if (patch.status !== undefined) {
|
||||||
columns.push({ column: 'status', value: patch.status || 'UNUSED' })
|
columns.push({ column: 'status', value: patch.status || 'UNUSED' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ router.post(
|
|||||||
data: {
|
data: {
|
||||||
filePath: String(result.filePath || '').trim(),
|
filePath: String(result.filePath || '').trim(),
|
||||||
enabled: Boolean(result.source?.enabled),
|
enabled: Boolean(result.source?.enabled),
|
||||||
|
shopCount: Array.isArray(result.source?.shops) ? result.source.shops.length : 0,
|
||||||
hasAccessToken: Boolean(result.source?.hasAccessToken),
|
hasAccessToken: Boolean(result.source?.hasAccessToken),
|
||||||
hasRefreshToken: Boolean(result.source?.hasRefreshToken),
|
hasRefreshToken: Boolean(result.source?.hasRefreshToken),
|
||||||
accessTokenStatus: String(result.source?.accessTokenStatus || '').trim(),
|
accessTokenStatus: String(result.source?.accessTokenStatus || '').trim(),
|
||||||
@@ -56,7 +57,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry-source/refresh-token',
|
'/kuaishou-industry-source/refresh-token',
|
||||||
createJsonHandler(() => refreshAdminKuaishouIndustryAccessToken(), {
|
createJsonHandler((req) => refreshAdminKuaishouIndustryAccessToken(req.body as JsonRecord), {
|
||||||
successMessage: '快手行业电子凭证 accessToken 已刷新',
|
successMessage: '快手行业电子凭证 accessToken 已刷新',
|
||||||
errorMessage: '刷新快手行业电子凭证 accessToken 失败',
|
errorMessage: '刷新快手行业电子凭证 accessToken 失败',
|
||||||
scope: '[admin/platform-config/kuaishou-industry-source/refresh-token]',
|
scope: '[admin/platform-config/kuaishou-industry-source/refresh-token]',
|
||||||
@@ -68,8 +69,9 @@ router.post(
|
|||||||
targetId: 'kuaishou_industry_source',
|
targetId: 'kuaishou_industry_source',
|
||||||
data: {
|
data: {
|
||||||
refreshed: Boolean(result.refreshed),
|
refreshed: Boolean(result.refreshed),
|
||||||
accessTokenExpiresAt: String(result.source?.accessTokenExpiresAt || '').trim(),
|
sellerId: String(result.shop?.sellerId || '').trim(),
|
||||||
accessTokenStatus: String(result.source?.accessTokenStatus || '').trim(),
|
accessTokenExpiresAt: String(result.shop?.accessTokenExpiresAt || '').trim(),
|
||||||
|
accessTokenStatus: String(result.shop?.accessTokenStatus || '').trim(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -95,9 +97,10 @@ router.post(
|
|||||||
targetId: 'kuaishou_industry_source',
|
targetId: 'kuaishou_industry_source',
|
||||||
data: {
|
data: {
|
||||||
refreshed: Boolean(result.refreshed),
|
refreshed: Boolean(result.refreshed),
|
||||||
accessTokenExpiresAt: String(result.source?.accessTokenExpiresAt || '').trim(),
|
sellerId: String(result.shop?.sellerId || '').trim(),
|
||||||
refreshTokenExpiresAt: String(result.source?.refreshTokenExpiresAt || '').trim(),
|
accessTokenExpiresAt: String(result.shop?.accessTokenExpiresAt || '').trim(),
|
||||||
accessTokenStatus: String(result.source?.accessTokenStatus || '').trim(),
|
refreshTokenExpiresAt: String(result.shop?.refreshTokenExpiresAt || '').trim(),
|
||||||
|
accessTokenStatus: String(result.shop?.accessTokenStatus || '').trim(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { maskSecret } from '../../../utils/masking.js'
|
import { maskSecret } from '../../../utils/masking.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import {
|
import {
|
||||||
|
findKuaishouIndustryShopConfig,
|
||||||
getKuaishouIndustrySourceConfig,
|
getKuaishouIndustrySourceConfig,
|
||||||
getKuaishouIndustrySourceFilePath,
|
getKuaishouIndustrySourceFilePath,
|
||||||
saveKuaishouIndustrySourceConfig,
|
saveKuaishouIndustrySourceConfig,
|
||||||
|
type KuaishouIndustryShopConfig,
|
||||||
type KuaishouIndustrySourceConfig,
|
type KuaishouIndustrySourceConfig,
|
||||||
} from '../../platforms/kuaishou-industry/source-config-service.js'
|
} from '../../platforms/kuaishou-industry/source-config-service.js'
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +19,9 @@ const SECRET_FIELDS = [
|
|||||||
'appSecret',
|
'appSecret',
|
||||||
'signSecret',
|
'signSecret',
|
||||||
'messageSecret',
|
'messageSecret',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const SHOP_SECRET_FIELDS = [
|
||||||
'accessToken',
|
'accessToken',
|
||||||
'refreshToken',
|
'refreshToken',
|
||||||
] as const
|
] as const
|
||||||
@@ -49,8 +54,10 @@ export function updateAdminKuaishouIndustrySourceConfig(payload: JsonObject = {}
|
|||||||
shopId: readConfigString(payload, 'shopId', current.shopId),
|
shopId: readConfigString(payload, 'shopId', current.shopId),
|
||||||
shopName: readConfigString(payload, 'shopName', current.shopName),
|
shopName: readConfigString(payload, 'shopName', current.shopName),
|
||||||
version: readConfigString(payload, 'version', current.version),
|
version: readConfigString(payload, 'version', current.version),
|
||||||
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current.accessTokenExpiresAt, { allowBlank: true }),
|
shops: normalizeShopConfigPayloads(
|
||||||
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current.refreshTokenExpiresAt, { allowBlank: true }),
|
Array.isArray(payload.shops) ? payload.shops : current.shops,
|
||||||
|
current.shops,
|
||||||
|
),
|
||||||
...resolveSecretPatch(payload, current),
|
...resolveSecretPatch(payload, current),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,12 +67,15 @@ export function updateAdminKuaishouIndustrySourceConfig(payload: JsonObject = {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAdminKuaishouIndustryAccessToken() {
|
export async function refreshAdminKuaishouIndustryAccessToken(payload: JsonObject = {}) {
|
||||||
const result = await refreshKuaishouIndustryAccessToken()
|
const result = await refreshKuaishouIndustryAccessToken({
|
||||||
|
sellerId: String(payload.sellerId || '').trim(),
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: getKuaishouIndustrySourceFilePath(),
|
filePath: getKuaishouIndustrySourceFilePath(),
|
||||||
refreshed: result.refreshed,
|
refreshed: result.refreshed,
|
||||||
|
shop: mapAdminKuaishouIndustryShopConfig(result.shop, result.config),
|
||||||
source: mapAdminKuaishouIndustrySourceConfig(result.config),
|
source: mapAdminKuaishouIndustrySourceConfig(result.config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,17 +89,23 @@ export async function exchangeAdminKuaishouIndustryAuthorizationCode(payload: Js
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await exchangeKuaishouIndustryAuthorizationCode(code)
|
const result = await exchangeKuaishouIndustryAuthorizationCode(code, {
|
||||||
|
sellerId: String(payload.sellerId || '').trim(),
|
||||||
|
shopName: String(payload.shopName || '').trim(),
|
||||||
|
customShopName: String(payload.customShopName || '').trim(),
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: getKuaishouIndustrySourceFilePath(),
|
filePath: getKuaishouIndustrySourceFilePath(),
|
||||||
refreshed: result.refreshed,
|
refreshed: result.refreshed,
|
||||||
|
shop: mapAdminKuaishouIndustryShopConfig(result.shop, result.config),
|
||||||
source: mapAdminKuaishouIndustrySourceConfig(result.config),
|
source: mapAdminKuaishouIndustrySourceConfig(result.config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConfig) {
|
function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConfig) {
|
||||||
const accessTokenStatus = resolveAccessTokenStatus(config)
|
const primaryShop = config.shops[0] || null
|
||||||
|
const accessTokenStatus = resolveAccessTokenStatus(primaryShop || config)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: config.enabled !== false,
|
enabled: config.enabled !== false,
|
||||||
@@ -127,12 +143,47 @@ function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConf
|
|||||||
shopId: config.shopId,
|
shopId: config.shopId,
|
||||||
shopName: config.shopName,
|
shopName: config.shopName,
|
||||||
version: config.version,
|
version: config.version,
|
||||||
|
shops: config.shops.map((shop) => mapAdminKuaishouIndustryShopConfig(shop, config)),
|
||||||
lastRefreshedAt: config.lastRefreshedAt,
|
lastRefreshedAt: config.lastRefreshedAt,
|
||||||
lastRefreshError: config.lastRefreshError,
|
lastRefreshError: config.lastRefreshError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildKuaishouIndustryAuthorizationUrl(config: KuaishouIndustrySourceConfig): string {
|
function mapAdminKuaishouIndustryShopConfig(
|
||||||
|
shop: KuaishouIndustryShopConfig,
|
||||||
|
config: KuaishouIndustrySourceConfig,
|
||||||
|
) {
|
||||||
|
const accessTokenStatus = resolveAccessTokenStatus(shop)
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: shop.enabled !== false,
|
||||||
|
sellerId: shop.sellerId,
|
||||||
|
shopId: shop.shopId,
|
||||||
|
shopName: shop.shopName,
|
||||||
|
customShopName: shop.customShopName,
|
||||||
|
authState: shop.authState,
|
||||||
|
authorizationUrl: buildKuaishouIndustryAuthorizationUrl(config, shop),
|
||||||
|
accessToken: '',
|
||||||
|
accessTokenMasked: maskSecret(shop.accessToken),
|
||||||
|
hasAccessToken: Boolean(shop.accessToken),
|
||||||
|
refreshToken: '',
|
||||||
|
refreshTokenMasked: maskSecret(shop.refreshToken),
|
||||||
|
hasRefreshToken: Boolean(shop.refreshToken),
|
||||||
|
accessTokenExpiresAt: shop.accessTokenExpiresAt,
|
||||||
|
refreshTokenExpiresAt: shop.refreshTokenExpiresAt,
|
||||||
|
accessTokenStatus: accessTokenStatus.status,
|
||||||
|
accessTokenExpiresInSeconds: accessTokenStatus.expiresInSeconds,
|
||||||
|
openId: shop.openId,
|
||||||
|
grantedScopes: shop.grantedScopes,
|
||||||
|
lastRefreshedAt: shop.lastRefreshedAt,
|
||||||
|
lastRefreshError: shop.lastRefreshError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKuaishouIndustryAuthorizationUrl(
|
||||||
|
config: KuaishouIndustrySourceConfig,
|
||||||
|
shop?: Pick<KuaishouIndustryShopConfig, 'sellerId' | 'authState'> | null,
|
||||||
|
): string {
|
||||||
if (!config.authBaseUrl || !config.appKey || !config.redirectUri || !config.scopes) {
|
if (!config.authBaseUrl || !config.appKey || !config.redirectUri || !config.scopes) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -143,8 +194,9 @@ function buildKuaishouIndustryAuthorizationUrl(config: KuaishouIndustrySourceCon
|
|||||||
url.searchParams.set('redirect_uri', config.redirectUri)
|
url.searchParams.set('redirect_uri', config.redirectUri)
|
||||||
url.searchParams.set('scope', normalizeScopeText(config.scopes))
|
url.searchParams.set('scope', normalizeScopeText(config.scopes))
|
||||||
url.searchParams.set('response_type', 'code')
|
url.searchParams.set('response_type', 'code')
|
||||||
if (config.authState) {
|
const state = String(shop?.authState || config.authState || shop?.sellerId || '').trim()
|
||||||
url.searchParams.set('state', config.authState)
|
if (state) {
|
||||||
|
url.searchParams.set('state', state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
@@ -174,7 +226,96 @@ function resolveSecretPatch(
|
|||||||
return patch
|
return patch
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAccessTokenStatus(config: KuaishouIndustrySourceConfig) {
|
function normalizeShopConfigPayloads(
|
||||||
|
rawShops: unknown[],
|
||||||
|
currentShops: KuaishouIndustryShopConfig[] = [],
|
||||||
|
): KuaishouIndustryShopConfig[] {
|
||||||
|
return rawShops
|
||||||
|
.map((rawShop, index) => normalizeShopConfigPayload(rawShop, index, currentShops))
|
||||||
|
.filter((shop): shop is KuaishouIndustryShopConfig => Boolean(shop))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeShopConfigPayload(
|
||||||
|
rawShop: unknown,
|
||||||
|
index: number,
|
||||||
|
currentShops: KuaishouIndustryShopConfig[],
|
||||||
|
): KuaishouIndustryShopConfig | null {
|
||||||
|
if (!rawShop || typeof rawShop !== 'object' || Array.isArray(rawShop)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = rawShop as JsonObject
|
||||||
|
const current = resolveCurrentShopConfig(payload, index, currentShops)
|
||||||
|
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', { allowBlank: true })
|
||||||
|
const shopId = readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) || sellerId
|
||||||
|
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', { allowBlank: true })
|
||||||
|
const customShopName = readConfigString(payload, 'customShopName', current?.customShopName || '', { allowBlank: true })
|
||||||
|
const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '')
|
||||||
|
const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '')
|
||||||
|
const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true })
|
||||||
|
|
||||||
|
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current?.enabled !== false,
|
||||||
|
sellerId,
|
||||||
|
shopId,
|
||||||
|
shopName,
|
||||||
|
customShopName,
|
||||||
|
authState: readConfigString(payload, 'authState', current?.authState || '', { allowBlank: true }),
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current?.accessTokenExpiresAt || '', { allowBlank: true }),
|
||||||
|
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current?.refreshTokenExpiresAt || '', { allowBlank: true }),
|
||||||
|
openId,
|
||||||
|
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', { allowBlank: true })),
|
||||||
|
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', { allowBlank: true }),
|
||||||
|
lastRefreshError: readConfigString(payload, 'lastRefreshError', current?.lastRefreshError || '', { allowBlank: true }),
|
||||||
|
...resolveShopSecretPatch(payload, current),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCurrentShopConfig(
|
||||||
|
payload: JsonObject,
|
||||||
|
index: number,
|
||||||
|
currentShops: KuaishouIndustryShopConfig[],
|
||||||
|
): KuaishouIndustryShopConfig | null {
|
||||||
|
const sellerId = String(payload.sellerId || payload.shopId || '').trim()
|
||||||
|
if (sellerId) {
|
||||||
|
const sourceLike = { shops: currentShops } as KuaishouIndustrySourceConfig
|
||||||
|
const matched = findKuaishouIndustryShopConfig(sellerId, sourceLike)
|
||||||
|
if (matched) {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentShops[index] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveShopSecretPatch(
|
||||||
|
payload: JsonObject,
|
||||||
|
current: KuaishouIndustryShopConfig | null,
|
||||||
|
): Partial<KuaishouIndustryShopConfig> {
|
||||||
|
const patch: Partial<KuaishouIndustryShopConfig> = {}
|
||||||
|
for (const field of SHOP_SECRET_FIELDS) {
|
||||||
|
patch[field] = readSecretString(payload, field, current?.[field] || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSecretString(
|
||||||
|
payload: JsonObject,
|
||||||
|
field: string,
|
||||||
|
fallback: string,
|
||||||
|
): string {
|
||||||
|
const text = String(payload[field] || '').trim()
|
||||||
|
return text || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAccessTokenStatus(config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>) {
|
||||||
if (!config.accessToken) {
|
if (!config.accessToken) {
|
||||||
return {
|
return {
|
||||||
status: 'missing',
|
status: 'missing',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustrySou
|
|||||||
shopId: String(config.shopId || KUISHOU_INDUSTRY_PROVIDER).trim() || KUISHOU_INDUSTRY_PROVIDER,
|
shopId: String(config.shopId || KUISHOU_INDUSTRY_PROVIDER).trim() || KUISHOU_INDUSTRY_PROVIDER,
|
||||||
shopName: String(config.shopName || '快手行业电子凭证').trim() || '快手行业电子凭证',
|
shopName: String(config.shopName || '快手行业电子凭证').trim() || '快手行业电子凭证',
|
||||||
version: String(config.version || '1').trim() || '1',
|
version: String(config.version || '1').trim() || '1',
|
||||||
|
shops: Array.isArray(config.shops) ? config.shops : [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
|||||||
|
|
||||||
type ConsumeCallbackInput = {
|
type ConsumeCallbackInput = {
|
||||||
oid: string
|
oid: string
|
||||||
|
sellerId?: string
|
||||||
etickets: Array<{
|
etickets: Array<{
|
||||||
id: string
|
id: string
|
||||||
code?: string
|
code?: string
|
||||||
@@ -41,7 +42,10 @@ export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ su
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResult = await ensureKuaishouIndustryAccessToken({ config })
|
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||||
|
config,
|
||||||
|
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||||
|
})
|
||||||
config = {
|
config = {
|
||||||
...config,
|
...config,
|
||||||
...tokenResult.config,
|
...tokenResult.config,
|
||||||
@@ -111,6 +115,7 @@ export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ su
|
|||||||
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
||||||
url,
|
url,
|
||||||
oid: input.oid,
|
oid: input.oid,
|
||||||
|
sellerId: input.sellerId || '',
|
||||||
status: input.status,
|
status: input.status,
|
||||||
consumeType: input.consumeType,
|
consumeType: input.consumeType,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,12 +32,16 @@ export async function handleConsumeCode(rawBody: JsonObject = {}) {
|
|||||||
const normalizedOid = params.oid
|
const normalizedOid = params.oid
|
||||||
|
|
||||||
let consumedCount = 0
|
let consumedCount = 0
|
||||||
|
let callbackSellerId = params.sellerId
|
||||||
|
|
||||||
for (const eticket of params.etickets) {
|
for (const eticket of params.etickets) {
|
||||||
const voucher = await findKuaishouIndustryVoucherByCode(String(eticket.id || ''), normalizedOid)
|
const voucher = await findKuaishouIndustryVoucherByCode(String(eticket.id || ''), normalizedOid)
|
||||||
if (!voucher) {
|
if (!voucher) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (!callbackSellerId) {
|
||||||
|
callbackSellerId = String(voucher.seller_id || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
const task = voucher.task_id ? await getTaskById(voucher.task_id) : null
|
const task = voucher.task_id ? await getTaskById(voucher.task_id) : null
|
||||||
const consumed = await consumeKuaishouIndustryVoucher(voucher, {
|
const consumed = await consumeKuaishouIndustryVoucher(voucher, {
|
||||||
@@ -88,6 +92,7 @@ export async function handleConsumeCode(rawBody: JsonObject = {}) {
|
|||||||
|
|
||||||
fireConsumeCallback({
|
fireConsumeCallback({
|
||||||
oid: normalizedOid,
|
oid: normalizedOid,
|
||||||
|
sellerId: callbackSellerId,
|
||||||
etickets: params.etickets.map((e) => ({
|
etickets: params.etickets.map((e) => ({
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
code: e.code,
|
code: e.code,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
|||||||
|
|
||||||
type DestroyCallbackInput = {
|
type DestroyCallbackInput = {
|
||||||
oid: string
|
oid: string
|
||||||
|
sellerId?: string
|
||||||
etickets?: Array<{
|
etickets?: Array<{
|
||||||
id: string
|
id: string
|
||||||
code?: string
|
code?: string
|
||||||
@@ -31,7 +32,10 @@ export async function destroyCallback(input: DestroyCallbackInput): Promise<{ su
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResult = await ensureKuaishouIndustryAccessToken({ config })
|
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||||
|
config,
|
||||||
|
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||||
|
})
|
||||||
config = {
|
config = {
|
||||||
...config,
|
...config,
|
||||||
...tokenResult.config,
|
...tokenResult.config,
|
||||||
@@ -94,6 +98,7 @@ export async function destroyCallback(input: DestroyCallbackInput): Promise<{ su
|
|||||||
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
||||||
url,
|
url,
|
||||||
oid: input.oid,
|
oid: input.oid,
|
||||||
|
sellerId: input.sellerId || '',
|
||||||
reason: input.reason,
|
reason: input.reason,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
|||||||
const targetVouchers = targetIds.size > 0
|
const targetVouchers = targetIds.size > 0
|
||||||
? vouchers.filter((voucher) => targetIds.has(String(voucher.voucher_code || '').trim()))
|
? vouchers.filter((voucher) => targetIds.has(String(voucher.voucher_code || '').trim()))
|
||||||
: vouchers
|
: vouchers
|
||||||
|
const callbackSellerId = params.sellerId
|
||||||
|
|| String(targetVouchers.find((voucher) => String(voucher.seller_id || '').trim())?.seller_id || '').trim()
|
||||||
|
|
||||||
for (const voucher of targetVouchers) {
|
for (const voucher of targetVouchers) {
|
||||||
const status = String(voucher.status || '').trim().toUpperCase()
|
const status = String(voucher.status || '').trim().toUpperCase()
|
||||||
@@ -70,6 +72,7 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
|||||||
|
|
||||||
fireDestroyCallback({
|
fireDestroyCallback({
|
||||||
oid: normalizedOid,
|
oid: normalizedOid,
|
||||||
|
sellerId: callbackSellerId,
|
||||||
etickets: params.etickets.map((e) => ({
|
etickets: params.etickets.map((e) => ({
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
code: e.code,
|
code: e.code,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function normalizeDestroyCodePayload(raw: JsonObject = {}) {
|
|||||||
paramRaw: normalizeIndustryString(raw.param),
|
paramRaw: normalizeIndustryString(raw.param),
|
||||||
|
|
||||||
oid: normalizeIndustryString(param.oid),
|
oid: normalizeIndustryString(param.oid),
|
||||||
|
sellerId: normalizeIndustryString(param.sellerId),
|
||||||
reason: normalizeIndustryString(param.reason),
|
reason: normalizeIndustryString(param.reason),
|
||||||
etickets: normalizeDestroyEtickets(param.etickets),
|
etickets: normalizeDestroyEtickets(param.etickets),
|
||||||
}
|
}
|
||||||
@@ -71,6 +72,7 @@ export function normalizeQueryCodePayload(raw: JsonObject = {}) {
|
|||||||
paramRaw: normalizeIndustryString(raw.param),
|
paramRaw: normalizeIndustryString(raw.param),
|
||||||
|
|
||||||
oid: normalizeIndustryString(param.oid),
|
oid: normalizeIndustryString(param.oid),
|
||||||
|
sellerId: normalizeIndustryString(param.sellerId),
|
||||||
eticketId: normalizeIndustryString(param.eticketId),
|
eticketId: normalizeIndustryString(param.eticketId),
|
||||||
sendType: normalizeIndustryString(param.sendType) || 'VIRTUAL',
|
sendType: normalizeIndustryString(param.sendType) || 'VIRTUAL',
|
||||||
eticketType: normalizeIndustryString(param.eticketType),
|
eticketType: normalizeIndustryString(param.eticketType),
|
||||||
@@ -93,6 +95,13 @@ export function assertSendCodePayload(payload: ReturnType<typeof normalizeSendCo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!payload.sellerId) {
|
||||||
|
throw createHttpError('缺少 sellerId', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_seller_id',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
assertCommonPayload(payload, config)
|
assertCommonPayload(payload, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +147,7 @@ export function normalizeConsumeCodePayload(raw: JsonObject = {}) {
|
|||||||
paramRaw: normalizeIndustryString(raw.param),
|
paramRaw: normalizeIndustryString(raw.param),
|
||||||
|
|
||||||
oid: normalizeIndustryString(param.oid),
|
oid: normalizeIndustryString(param.oid),
|
||||||
|
sellerId: normalizeIndustryString(param.sellerId),
|
||||||
etickets: normalizeConsumeEtickets(param.etickets),
|
etickets: normalizeConsumeEtickets(param.etickets),
|
||||||
status: normalizeIndustryString(param.status),
|
status: normalizeIndustryString(param.status),
|
||||||
consumeType: normalizeIndustryString(param.consumeType),
|
consumeType: normalizeIndustryString(param.consumeType),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
|||||||
|
|
||||||
type SendCallbackInput = {
|
type SendCallbackInput = {
|
||||||
oid: string
|
oid: string
|
||||||
|
sellerId?: string
|
||||||
sendType: string
|
sendType: string
|
||||||
etickets: Array<{
|
etickets: Array<{
|
||||||
id: string
|
id: string
|
||||||
@@ -36,7 +37,10 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenResult = await ensureKuaishouIndustryAccessToken({ config })
|
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||||
|
config,
|
||||||
|
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||||
|
})
|
||||||
config = {
|
config = {
|
||||||
...config,
|
...config,
|
||||||
...tokenResult.config,
|
...tokenResult.config,
|
||||||
@@ -100,11 +104,14 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/send`
|
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/send`
|
||||||
const requestLog = buildSendCallbackRequestLog({
|
const requestLog = {
|
||||||
|
sellerId: input.sellerId || '',
|
||||||
|
...buildSendCallbackRequestLog({
|
||||||
url,
|
url,
|
||||||
signParams,
|
signParams,
|
||||||
bizParams,
|
bizParams,
|
||||||
})
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog)
|
logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
const voucher = await upsertKuaishouIndustryVoucher({
|
const voucher = await upsertKuaishouIndustryVoucher({
|
||||||
oid: normalizedOid,
|
oid: normalizedOid,
|
||||||
unitIndex,
|
unitIndex,
|
||||||
|
sellerId: params.sellerId,
|
||||||
token: params.token,
|
token: params.token,
|
||||||
orderId: order?.id || null,
|
orderId: order?.id || null,
|
||||||
taskId: task?.id || null,
|
taskId: task?.id || null,
|
||||||
@@ -99,6 +100,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
oid: normalizedOid,
|
oid: normalizedOid,
|
||||||
sendType: params.sendType,
|
sendType: params.sendType,
|
||||||
etickets,
|
etickets,
|
||||||
|
sellerId: params.sellerId,
|
||||||
token: params.token,
|
token: params.token,
|
||||||
eticketType: params.eticketType,
|
eticketType: params.eticketType,
|
||||||
ext: params.ext,
|
ext: params.ext,
|
||||||
@@ -112,6 +114,7 @@ function fireSendCallback(input: {
|
|||||||
oid: string
|
oid: string
|
||||||
sendType: string
|
sendType: string
|
||||||
etickets: ReturnType<typeof buildIndustryEticketItem>[]
|
etickets: ReturnType<typeof buildIndustryEticketItem>[]
|
||||||
|
sellerId: string
|
||||||
token: string
|
token: string
|
||||||
eticketType?: string
|
eticketType?: string
|
||||||
ext?: string
|
ext?: string
|
||||||
@@ -138,6 +141,7 @@ function fireSendCallback(input: {
|
|||||||
|
|
||||||
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调已调度', {
|
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调已调度', {
|
||||||
oid: input.oid,
|
oid: input.oid,
|
||||||
|
sellerId: input.sellerId,
|
||||||
sendType: input.sendType,
|
sendType: input.sendType,
|
||||||
sendNum,
|
sendNum,
|
||||||
totalGoodsValue,
|
totalGoodsValue,
|
||||||
@@ -151,6 +155,7 @@ function fireSendCallback(input: {
|
|||||||
oid: input.oid,
|
oid: input.oid,
|
||||||
sendType: input.sendType,
|
sendType: input.sendType,
|
||||||
etickets: eticketItemsWithGoodsValue,
|
etickets: eticketItemsWithGoodsValue,
|
||||||
|
sellerId: input.sellerId,
|
||||||
sendNum,
|
sendNum,
|
||||||
totalGoodsValue,
|
totalGoodsValue,
|
||||||
token: input.token,
|
token: input.token,
|
||||||
|
|||||||
@@ -6,9 +6,29 @@ import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
|||||||
const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json')
|
const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json')
|
||||||
const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com'
|
const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com'
|
||||||
const DEFAULT_AUTH_BASE_URL = 'https://open.kwaixiaodian.com'
|
const DEFAULT_AUTH_BASE_URL = 'https://open.kwaixiaodian.com'
|
||||||
|
const LEGACY_DEFAULT_REDIRECT_URI = 'https://ks.khhao.com/oauth-callback'
|
||||||
|
const DEFAULT_REDIRECT_URI = 'https://ks.khhao.com/admin/platform-shops?tab=kuaishouIndustry'
|
||||||
|
const DEFAULT_SCOPES = 'merchant_item'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
|
export type KuaishouIndustryShopConfig = {
|
||||||
|
enabled: boolean
|
||||||
|
sellerId: string
|
||||||
|
shopId: string
|
||||||
|
shopName: string
|
||||||
|
customShopName: string
|
||||||
|
authState: string
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
accessTokenExpiresAt: string
|
||||||
|
refreshTokenExpiresAt: string
|
||||||
|
openId: string
|
||||||
|
grantedScopes: string
|
||||||
|
lastRefreshedAt: string
|
||||||
|
lastRefreshError: string
|
||||||
|
}
|
||||||
|
|
||||||
export type KuaishouIndustrySourceConfig = {
|
export type KuaishouIndustrySourceConfig = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
@@ -32,6 +52,7 @@ export type KuaishouIndustrySourceConfig = {
|
|||||||
shopId: string
|
shopId: string
|
||||||
shopName: string
|
shopName: string
|
||||||
version: string
|
version: string
|
||||||
|
shops: KuaishouIndustryShopConfig[]
|
||||||
lastRefreshedAt: string
|
lastRefreshedAt: string
|
||||||
lastRefreshError: string
|
lastRefreshError: string
|
||||||
}
|
}
|
||||||
@@ -65,35 +86,83 @@ export function patchKuaishouIndustrySourceConfig(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listKuaishouIndustryShopConfigs(
|
||||||
|
source: KuaishouIndustrySourceConfig = getKuaishouIndustrySourceConfig(),
|
||||||
|
): KuaishouIndustryShopConfig[] {
|
||||||
|
return Array.isArray(source.shops) ? source.shops : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findKuaishouIndustryShopConfig(
|
||||||
|
sellerId: unknown,
|
||||||
|
source: KuaishouIndustrySourceConfig = getKuaishouIndustrySourceConfig(),
|
||||||
|
): KuaishouIndustryShopConfig | null {
|
||||||
|
const normalizedSellerId = String(sellerId || '').trim()
|
||||||
|
if (!normalizedSellerId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return listKuaishouIndustryShopConfigs(source)
|
||||||
|
.find((shop) => String(shop.sellerId || shop.shopId || '').trim() === normalizedSellerId) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchKuaishouIndustryShopConfig(
|
||||||
|
sellerId: unknown,
|
||||||
|
patch: Partial<KuaishouIndustryShopConfig>,
|
||||||
|
): KuaishouIndustrySourceConfig {
|
||||||
|
const source = getKuaishouIndustrySourceConfig()
|
||||||
|
const normalizedSellerId = String(patch.sellerId || sellerId || '').trim()
|
||||||
|
const existing = findKuaishouIndustryShopConfig(normalizedSellerId, source)
|
||||||
|
const nextShop = normalizeKuaishouIndustryShopConfig({
|
||||||
|
...(existing || {}),
|
||||||
|
...patch,
|
||||||
|
sellerId: normalizedSellerId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!nextShop) {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
const shops = listKuaishouIndustryShopConfigs(source)
|
||||||
|
.filter((shop) => String(shop.sellerId || shop.shopId || '').trim() !== normalizedSellerId)
|
||||||
|
|
||||||
|
return saveKuaishouIndustrySourceConfig({
|
||||||
|
...source,
|
||||||
|
shops: [...shops, nextShop],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
||||||
const fallback = createDefaultKuaishouIndustrySourceConfig()
|
const fallback = createDefaultKuaishouIndustrySourceConfig()
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
const shops = normalizeKuaishouIndustryShopList(source)
|
||||||
|
const primaryShop = shops[0] || createEmptyKuaishouIndustryShopConfig()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : fallback.enabled,
|
enabled: typeof source.enabled === 'boolean' ? source.enabled : fallback.enabled,
|
||||||
baseUrl: normalizeUrlLike(source.baseUrl, fallback.baseUrl),
|
baseUrl: normalizeUrlLike(source.baseUrl, fallback.baseUrl),
|
||||||
authBaseUrl: normalizeUrlLike(source.authBaseUrl, fallback.authBaseUrl),
|
authBaseUrl: normalizeUrlLike(source.authBaseUrl, fallback.authBaseUrl),
|
||||||
redirectUri: normalizeString(source.redirectUri, fallback.redirectUri),
|
redirectUri: normalizeRedirectUri(source.redirectUri, fallback.redirectUri),
|
||||||
scopes: normalizeString(source.scopes, fallback.scopes),
|
scopes: normalizeScopeText(normalizeString(source.scopes, fallback.scopes)),
|
||||||
authState: normalizeString(source.authState, fallback.authState),
|
authState: normalizeString(source.authState, fallback.authState),
|
||||||
appKey: normalizeString(source.appKey, fallback.appKey),
|
appKey: normalizeString(source.appKey, fallback.appKey),
|
||||||
appSecret: normalizeString(source.appSecret, fallback.appSecret),
|
appSecret: normalizeString(source.appSecret, fallback.appSecret),
|
||||||
signSecret: normalizeString(source.signSecret, fallback.signSecret),
|
signSecret: normalizeString(source.signSecret, fallback.signSecret),
|
||||||
messageSecret: normalizeString(source.messageSecret, fallback.messageSecret),
|
messageSecret: normalizeString(source.messageSecret, fallback.messageSecret),
|
||||||
accessToken: normalizeString(source.accessToken, fallback.accessToken),
|
accessToken: primaryShop.accessToken,
|
||||||
refreshToken: normalizeString(source.refreshToken, fallback.refreshToken),
|
refreshToken: primaryShop.refreshToken,
|
||||||
accessTokenExpiresAt: normalizeNullableIso(source.accessTokenExpiresAt),
|
accessTokenExpiresAt: primaryShop.accessTokenExpiresAt,
|
||||||
refreshTokenExpiresAt: normalizeNullableIso(source.refreshTokenExpiresAt),
|
refreshTokenExpiresAt: primaryShop.refreshTokenExpiresAt,
|
||||||
openId: normalizeString(source.openId, fallback.openId),
|
openId: primaryShop.openId,
|
||||||
grantedScopes: normalizeString(source.grantedScopes, fallback.grantedScopes),
|
grantedScopes: primaryShop.grantedScopes,
|
||||||
sellerId: normalizeString(source.sellerId, fallback.sellerId),
|
sellerId: primaryShop.sellerId,
|
||||||
provider: normalizeString(source.provider, fallback.provider),
|
provider: normalizeString(source.provider, fallback.provider),
|
||||||
platform: normalizeString(source.platform, fallback.platform),
|
platform: normalizeString(source.platform, fallback.platform),
|
||||||
shopId: normalizeString(source.shopId, fallback.shopId),
|
shopId: normalizeString(source.shopId, primaryShop.shopId || fallback.shopId),
|
||||||
shopName: normalizeString(source.shopName, fallback.shopName),
|
shopName: normalizeString(source.shopName, primaryShop.shopName || fallback.shopName),
|
||||||
version: normalizeString(source.version, fallback.version),
|
version: normalizeString(source.version, fallback.version),
|
||||||
lastRefreshedAt: normalizeNullableIso(source.lastRefreshedAt),
|
shops,
|
||||||
lastRefreshError: normalizeString(source.lastRefreshError, ''),
|
lastRefreshedAt: primaryShop.lastRefreshedAt || normalizeNullableIso(source.lastRefreshedAt),
|
||||||
|
lastRefreshError: primaryShop.lastRefreshError || normalizeString(source.lastRefreshError, ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +173,8 @@ function createDefaultKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConf
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
baseUrl: DEFAULT_CALLBACK_BASE_URL,
|
baseUrl: DEFAULT_CALLBACK_BASE_URL,
|
||||||
authBaseUrl: DEFAULT_AUTH_BASE_URL,
|
authBaseUrl: DEFAULT_AUTH_BASE_URL,
|
||||||
redirectUri: '',
|
redirectUri: DEFAULT_REDIRECT_URI,
|
||||||
scopes: '',
|
scopes: DEFAULT_SCOPES,
|
||||||
authState: '',
|
authState: '',
|
||||||
appKey: String(runtime.appKey || '').trim(),
|
appKey: String(runtime.appKey || '').trim(),
|
||||||
appSecret: String(runtime.appSecret || '').trim(),
|
appSecret: String(runtime.appSecret || '').trim(),
|
||||||
@@ -123,6 +192,117 @@ function createDefaultKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConf
|
|||||||
shopId: String(runtime.shopId || 'kuaishou-industry').trim() || 'kuaishou-industry',
|
shopId: String(runtime.shopId || 'kuaishou-industry').trim() || 'kuaishou-industry',
|
||||||
shopName: String(runtime.shopName || '快手行业电子凭证').trim() || '快手行业电子凭证',
|
shopName: String(runtime.shopName || '快手行业电子凭证').trim() || '快手行业电子凭证',
|
||||||
version: String(runtime.version || '1').trim() || '1',
|
version: String(runtime.version || '1').trim() || '1',
|
||||||
|
shops: [],
|
||||||
|
lastRefreshedAt: '',
|
||||||
|
lastRefreshError: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKuaishouIndustryShopList(source: JsonObject): KuaishouIndustryShopConfig[] {
|
||||||
|
const rawShops = Array.isArray(source.shops)
|
||||||
|
? source.shops
|
||||||
|
: buildLegacySingleShopList(source)
|
||||||
|
|
||||||
|
const deduped = new Map<string, KuaishouIndustryShopConfig>()
|
||||||
|
let unnamedIndex = 0
|
||||||
|
for (const item of rawShops) {
|
||||||
|
const shop = normalizeKuaishouIndustryShopConfig(item)
|
||||||
|
if (!shop) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = shop.sellerId || shop.shopId || `__legacy_${unnamedIndex++}`
|
||||||
|
|
||||||
|
deduped.set(key, shop)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...deduped.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKuaishouIndustryShopConfig(rawValue: unknown): KuaishouIndustryShopConfig | null {
|
||||||
|
if (!isPlainObject(rawValue)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const sellerId = String(rawValue.sellerId || rawValue.shopId || rawValue.userId || '').trim()
|
||||||
|
const shopId = String(rawValue.shopId || sellerId).trim()
|
||||||
|
const shopName = String(rawValue.shopName || rawValue.kshopName || rawValue.userName || '').trim()
|
||||||
|
const customShopName = String(rawValue.customShopName || '').trim()
|
||||||
|
const accessToken = normalizeString(rawValue.accessToken, '')
|
||||||
|
const refreshToken = normalizeString(rawValue.refreshToken, '')
|
||||||
|
const openId = normalizeString(rawValue.openId, '')
|
||||||
|
|
||||||
|
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: typeof rawValue.enabled === 'boolean' ? rawValue.enabled : true,
|
||||||
|
sellerId: sellerId || shopId,
|
||||||
|
shopId: shopId || sellerId,
|
||||||
|
shopName,
|
||||||
|
customShopName,
|
||||||
|
authState: normalizeString(rawValue.authState, ''),
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
accessTokenExpiresAt: normalizeNullableIso(rawValue.accessTokenExpiresAt),
|
||||||
|
refreshTokenExpiresAt: normalizeNullableIso(rawValue.refreshTokenExpiresAt),
|
||||||
|
openId,
|
||||||
|
grantedScopes: normalizeScopeText(normalizeString(rawValue.grantedScopes, '')),
|
||||||
|
lastRefreshedAt: normalizeNullableIso(rawValue.lastRefreshedAt),
|
||||||
|
lastRefreshError: normalizeString(rawValue.lastRefreshError, ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLegacySingleShopList(source: JsonObject): JsonObject[] {
|
||||||
|
const legacyShopId = String(source.shopId || '').trim()
|
||||||
|
const sellerId = String(
|
||||||
|
source.sellerId || (legacyShopId && legacyShopId !== 'kuaishou-industry' ? legacyShopId : ''),
|
||||||
|
).trim()
|
||||||
|
const accessToken = String(source.accessToken || '').trim()
|
||||||
|
const refreshToken = String(source.refreshToken || '').trim()
|
||||||
|
const openId = String(source.openId || '').trim()
|
||||||
|
const shopName = String(source.shopName || '').trim()
|
||||||
|
const customShopName = String(source.customShopName || '').trim()
|
||||||
|
|
||||||
|
if (!sellerId && !accessToken && !refreshToken && !openId && !shopName && !customShopName) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
enabled: source.enabled !== false,
|
||||||
|
sellerId,
|
||||||
|
shopId: sellerId,
|
||||||
|
shopName,
|
||||||
|
customShopName,
|
||||||
|
authState: source.authState,
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
accessTokenExpiresAt: source.accessTokenExpiresAt,
|
||||||
|
refreshTokenExpiresAt: source.refreshTokenExpiresAt,
|
||||||
|
openId,
|
||||||
|
grantedScopes: source.grantedScopes,
|
||||||
|
lastRefreshedAt: source.lastRefreshedAt,
|
||||||
|
lastRefreshError: source.lastRefreshError,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyKuaishouIndustryShopConfig(): KuaishouIndustryShopConfig {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
sellerId: '',
|
||||||
|
shopId: '',
|
||||||
|
shopName: '',
|
||||||
|
customShopName: '',
|
||||||
|
authState: '',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
accessTokenExpiresAt: '',
|
||||||
|
refreshTokenExpiresAt: '',
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
lastRefreshedAt: '',
|
lastRefreshedAt: '',
|
||||||
lastRefreshError: '',
|
lastRefreshError: '',
|
||||||
}
|
}
|
||||||
@@ -133,6 +313,23 @@ function normalizeString(value: unknown, fallback: string): string {
|
|||||||
return text || fallback
|
return text || fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRedirectUri(value: unknown, fallback: string): string {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text || text === LEGACY_DEFAULT_REDIRECT_URI) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeScopeText(value: unknown): string {
|
||||||
|
return String(value || '')
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeUrlLike(value: unknown, fallback: string): string {
|
function normalizeUrlLike(value: unknown, fallback: string): string {
|
||||||
return String(value || fallback).trim().replace(/\/+$/, '') || fallback
|
return String(value || fallback).trim().replace(/\/+$/, '') || fallback
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import test from 'node:test'
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ensureKuaishouIndustryAccessToken,
|
||||||
shouldRefreshAccessToken,
|
shouldRefreshAccessToken,
|
||||||
} from './token-service.js'
|
} from './token-service.js'
|
||||||
import type {
|
import type {
|
||||||
|
KuaishouIndustryShopConfig,
|
||||||
KuaishouIndustrySourceConfig,
|
KuaishouIndustrySourceConfig,
|
||||||
} from './source-config-service.js'
|
} from './source-config-service.js'
|
||||||
|
|
||||||
@@ -37,6 +39,39 @@ test('shouldRefreshAccessToken keeps valid tokens without a forced refresh', ()
|
|||||||
})), false)
|
})), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ensureKuaishouIndustryAccessToken requires sellerId when multiple shops are enabled', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
() => ensureKuaishouIndustryAccessToken({
|
||||||
|
config: createConfig({
|
||||||
|
shops: [
|
||||||
|
createShop({ sellerId: 'seller-a', accessToken: 'token-a' }),
|
||||||
|
createShop({ sellerId: 'seller-b', accessToken: 'token-b' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: '存在多个快手行业电子凭证店铺授权,请指定 sellerId',
|
||||||
|
errorCode: 'kuaishou_industry_missing_seller_id',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ensureKuaishouIndustryAccessToken selects the token by sellerId', async () => {
|
||||||
|
const result = await ensureKuaishouIndustryAccessToken({
|
||||||
|
sellerId: 'seller-b',
|
||||||
|
config: createConfig({
|
||||||
|
shops: [
|
||||||
|
createShop({ sellerId: 'seller-a', accessToken: 'token-a' }),
|
||||||
|
createShop({ sellerId: 'seller-b', accessToken: 'token-b' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.accessToken, 'token-b')
|
||||||
|
assert.equal(result.refreshed, false)
|
||||||
|
assert.equal(result.shop.sellerId, 'seller-b')
|
||||||
|
})
|
||||||
|
|
||||||
function createConfig(
|
function createConfig(
|
||||||
patch: Partial<KuaishouIndustrySourceConfig> = {},
|
patch: Partial<KuaishouIndustrySourceConfig> = {},
|
||||||
): KuaishouIndustrySourceConfig {
|
): KuaishouIndustrySourceConfig {
|
||||||
@@ -63,6 +98,29 @@ function createConfig(
|
|||||||
shopId: 'kuaishou-industry',
|
shopId: 'kuaishou-industry',
|
||||||
shopName: '快手行业电子凭证',
|
shopName: '快手行业电子凭证',
|
||||||
version: '1',
|
version: '1',
|
||||||
|
shops: [],
|
||||||
|
lastRefreshedAt: '',
|
||||||
|
lastRefreshError: '',
|
||||||
|
...patch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createShop(
|
||||||
|
patch: Partial<KuaishouIndustryShopConfig> = {},
|
||||||
|
): KuaishouIndustryShopConfig {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
sellerId: 'seller-a',
|
||||||
|
shopId: 'seller-a',
|
||||||
|
shopName: '测试店铺',
|
||||||
|
customShopName: '自定义测试店铺',
|
||||||
|
authState: '',
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh-token',
|
||||||
|
accessTokenExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
|
||||||
|
refreshTokenExpiresAt: '',
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
lastRefreshedAt: '',
|
lastRefreshedAt: '',
|
||||||
lastRefreshError: '',
|
lastRefreshError: '',
|
||||||
...patch,
|
...patch,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||||
import {
|
import {
|
||||||
|
findKuaishouIndustryShopConfig,
|
||||||
getKuaishouIndustrySourceConfig,
|
getKuaishouIndustrySourceConfig,
|
||||||
|
listKuaishouIndustryShopConfigs,
|
||||||
patchKuaishouIndustrySourceConfig,
|
patchKuaishouIndustrySourceConfig,
|
||||||
|
patchKuaishouIndustryShopConfig,
|
||||||
|
type KuaishouIndustryShopConfig,
|
||||||
type KuaishouIndustrySourceConfig,
|
type KuaishouIndustrySourceConfig,
|
||||||
} from './source-config-service.js'
|
} from './source-config-service.js'
|
||||||
|
|
||||||
@@ -16,39 +20,51 @@ export type KuaishouIndustryAccessTokenResult = {
|
|||||||
accessToken: string
|
accessToken: string
|
||||||
refreshed: boolean
|
refreshed: boolean
|
||||||
config: KuaishouIndustrySourceConfig
|
config: KuaishouIndustrySourceConfig
|
||||||
|
shop: KuaishouIndustryShopConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureKuaishouIndustryAccessToken(
|
export async function ensureKuaishouIndustryAccessToken(
|
||||||
options: {
|
options: {
|
||||||
forceRefresh?: boolean
|
forceRefresh?: boolean
|
||||||
config?: KuaishouIndustrySourceConfig
|
config?: KuaishouIndustrySourceConfig
|
||||||
|
sellerId?: string
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<KuaishouIndustryAccessTokenResult> {
|
): Promise<KuaishouIndustryAccessTokenResult> {
|
||||||
const config = options.config || getKuaishouIndustrySourceConfig()
|
const config = options.config || getKuaishouIndustrySourceConfig()
|
||||||
const accessToken = String(config.accessToken || '').trim()
|
const shop = resolveTokenShop(config, options.sellerId)
|
||||||
const refreshToken = String(config.refreshToken || '').trim()
|
const accessToken = String(shop.accessToken || '').trim()
|
||||||
|
const refreshToken = String(shop.refreshToken || '').trim()
|
||||||
|
|
||||||
if (!options.forceRefresh && accessToken && !shouldRefreshAccessToken(config)) {
|
if (!options.forceRefresh && accessToken && !shouldRefreshAccessToken(shop)) {
|
||||||
return { accessToken, refreshed: false, config }
|
return { accessToken, refreshed: false, config, shop }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return { accessToken, refreshed: false, config }
|
return { accessToken, refreshed: false, config, shop }
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshKuaishouIndustryAccessToken(config)
|
const sellerId = shop.sellerId || options.sellerId
|
||||||
|
return refreshKuaishouIndustryAccessToken({
|
||||||
|
config,
|
||||||
|
...(sellerId ? { sellerId } : {}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshKuaishouIndustryAccessToken(
|
export async function refreshKuaishouIndustryAccessToken(
|
||||||
config: KuaishouIndustrySourceConfig = getKuaishouIndustrySourceConfig(),
|
options: {
|
||||||
|
config?: KuaishouIndustrySourceConfig
|
||||||
|
sellerId?: string
|
||||||
|
} = {},
|
||||||
): Promise<KuaishouIndustryAccessTokenResult> {
|
): Promise<KuaishouIndustryAccessTokenResult> {
|
||||||
assertRefreshConfig(config)
|
const config = options.config || getKuaishouIndustrySourceConfig()
|
||||||
|
const shop = resolveTokenShop(config, options.sellerId)
|
||||||
|
assertRefreshConfig(config, shop)
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.set('app_id', config.appKey)
|
params.set('app_id', config.appKey)
|
||||||
params.set('app_secret', config.appSecret)
|
params.set('app_secret', config.appSecret)
|
||||||
params.set('grant_type', 'refresh_token')
|
params.set('grant_type', 'refresh_token')
|
||||||
params.set('refresh_token', config.refreshToken)
|
params.set('refresh_token', shop.refreshToken)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
@@ -60,22 +76,27 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
failureMessage: '快手 accessToken 刷新失败',
|
failureMessage: '快手 accessToken 刷新失败',
|
||||||
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
||||||
})
|
})
|
||||||
const saved = saveTokenPayload(tokenPayload, config)
|
const saved = saveTokenPayload(tokenPayload, config, shop)
|
||||||
|
|
||||||
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
accessTokenExpiresAt: saved.accessTokenExpiresAt || '',
|
sellerId: saved.shop.sellerId || '',
|
||||||
refreshTokenExpiresAt: saved.refreshTokenExpiresAt || '',
|
accessTokenExpiresAt: saved.shop.accessTokenExpiresAt || '',
|
||||||
hasRefreshToken: Boolean(saved.refreshToken),
|
refreshTokenExpiresAt: saved.shop.refreshTokenExpiresAt || '',
|
||||||
|
hasRefreshToken: Boolean(saved.shop.refreshToken),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: saved.accessToken,
|
accessToken: saved.shop.accessToken,
|
||||||
refreshed: true,
|
refreshed: true,
|
||||||
config: saved,
|
config: saved.config,
|
||||||
|
shop: saved.shop,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
patchKuaishouIndustryShopConfig(shop.sellerId, {
|
||||||
|
lastRefreshError: message,
|
||||||
|
})
|
||||||
patchKuaishouIndustrySourceConfig({
|
patchKuaishouIndustrySourceConfig({
|
||||||
lastRefreshError: message,
|
lastRefreshError: message,
|
||||||
})
|
})
|
||||||
@@ -86,8 +107,14 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
|
|
||||||
export async function exchangeKuaishouIndustryAuthorizationCode(
|
export async function exchangeKuaishouIndustryAuthorizationCode(
|
||||||
code: string,
|
code: string,
|
||||||
config: KuaishouIndustrySourceConfig = getKuaishouIndustrySourceConfig(),
|
options: {
|
||||||
|
config?: KuaishouIndustrySourceConfig
|
||||||
|
sellerId?: string
|
||||||
|
shopName?: string
|
||||||
|
customShopName?: string
|
||||||
|
} = {},
|
||||||
): Promise<KuaishouIndustryAccessTokenResult> {
|
): Promise<KuaishouIndustryAccessTokenResult> {
|
||||||
|
const config = options.config || getKuaishouIndustrySourceConfig()
|
||||||
assertAuthorizationCodeConfig(config, code)
|
assertAuthorizationCodeConfig(config, code)
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -106,19 +133,27 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
|||||||
failureMessage: '快手授权码换取 accessToken 失败',
|
failureMessage: '快手授权码换取 accessToken 失败',
|
||||||
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
||||||
})
|
})
|
||||||
const saved = saveTokenPayload(tokenPayload, config)
|
const saved = saveTokenPayload(tokenPayload, config, {
|
||||||
|
...createEmptyShopConfig(),
|
||||||
|
sellerId: String(options.sellerId || '').trim(),
|
||||||
|
shopId: String(options.sellerId || '').trim(),
|
||||||
|
shopName: String(options.shopName || '').trim(),
|
||||||
|
customShopName: String(options.customShopName || '').trim(),
|
||||||
|
})
|
||||||
|
|
||||||
logInfo('[kuaishou-industry/token]', '授权码换取 accessToken 成功', {
|
logInfo('[kuaishou-industry/token]', '授权码换取 accessToken 成功', {
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
accessTokenExpiresAt: saved.accessTokenExpiresAt || '',
|
sellerId: saved.shop.sellerId || '',
|
||||||
refreshTokenExpiresAt: saved.refreshTokenExpiresAt || '',
|
accessTokenExpiresAt: saved.shop.accessTokenExpiresAt || '',
|
||||||
hasRefreshToken: Boolean(saved.refreshToken),
|
refreshTokenExpiresAt: saved.shop.refreshTokenExpiresAt || '',
|
||||||
|
hasRefreshToken: Boolean(saved.shop.refreshToken),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: saved.accessToken,
|
accessToken: saved.shop.accessToken,
|
||||||
refreshed: true,
|
refreshed: true,
|
||||||
config: saved,
|
config: saved.config,
|
||||||
|
shop: saved.shop,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
@@ -130,7 +165,7 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldRefreshAccessToken(config: KuaishouIndustrySourceConfig): boolean {
|
export function shouldRefreshAccessToken(config: Pick<KuaishouIndustryShopConfig, 'accessToken' | 'accessTokenExpiresAt'>): boolean {
|
||||||
if (!String(config.accessToken || '').trim()) {
|
if (!String(config.accessToken || '').trim()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -143,7 +178,67 @@ export function shouldRefreshAccessToken(config: KuaishouIndustrySourceConfig):
|
|||||||
return expiresAt <= Date.now() + ACCESS_TOKEN_REFRESH_MARGIN_MS
|
return expiresAt <= Date.now() + ACCESS_TOKEN_REFRESH_MARGIN_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertRefreshConfig(config: KuaishouIndustrySourceConfig) {
|
function resolveTokenShop(
|
||||||
|
config: KuaishouIndustrySourceConfig,
|
||||||
|
sellerId: unknown,
|
||||||
|
): KuaishouIndustryShopConfig {
|
||||||
|
const normalizedSellerId = String(sellerId || '').trim()
|
||||||
|
if (normalizedSellerId) {
|
||||||
|
const matched = findKuaishouIndustryShopConfig(normalizedSellerId, config)
|
||||||
|
if (!matched) {
|
||||||
|
throw createHttpError(`未找到 sellerId=${normalizedSellerId} 的快手行业电子凭证授权`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_shop_not_configured',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched.enabled === false) {
|
||||||
|
throw createHttpError(`sellerId=${normalizedSellerId} 的快手行业电子凭证授权已停用`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_shop_disabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledShops = listKuaishouIndustryShopConfigs(config)
|
||||||
|
.filter((shop) => shop.enabled !== false)
|
||||||
|
|
||||||
|
if (enabledShops.length === 1) {
|
||||||
|
return enabledShops[0] as KuaishouIndustryShopConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledShops.length > 1) {
|
||||||
|
throw createHttpError('存在多个快手行业电子凭证店铺授权,请指定 sellerId', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_seller_id',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return createEmptyShopConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyShopConfig(): KuaishouIndustryShopConfig {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
sellerId: '',
|
||||||
|
shopId: '',
|
||||||
|
shopName: '',
|
||||||
|
customShopName: '',
|
||||||
|
authState: '',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
accessTokenExpiresAt: '',
|
||||||
|
refreshTokenExpiresAt: '',
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
|
lastRefreshedAt: '',
|
||||||
|
lastRefreshError: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertRefreshConfig(config: KuaishouIndustrySourceConfig, shop: KuaishouIndustryShopConfig) {
|
||||||
if (!config.baseUrl) {
|
if (!config.baseUrl) {
|
||||||
throw createHttpError('快手开放平台 API 地址未配置', {
|
throw createHttpError('快手开放平台 API 地址未配置', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -158,7 +253,7 @@ function assertRefreshConfig(config: KuaishouIndustrySourceConfig) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.refreshToken) {
|
if (!shop.refreshToken) {
|
||||||
throw createHttpError('refreshToken 未配置,无法刷新 accessToken', {
|
throw createHttpError('refreshToken 未配置,无法刷新 accessToken', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'kuaishou_industry_missing_refresh_token',
|
errorCode: 'kuaishou_industry_missing_refresh_token',
|
||||||
@@ -234,21 +329,37 @@ async function requestKuaishouIndustryToken({
|
|||||||
function saveTokenPayload(
|
function saveTokenPayload(
|
||||||
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
||||||
config: KuaishouIndustrySourceConfig,
|
config: KuaishouIndustrySourceConfig,
|
||||||
|
shop: KuaishouIndustryShopConfig,
|
||||||
) {
|
) {
|
||||||
const nextRefreshToken = tokenPayload.refreshToken || config.refreshToken
|
const nextSellerId = tokenPayload.sellerId || shop.sellerId
|
||||||
|
if (!nextSellerId) {
|
||||||
|
throw createHttpError('授权结果缺少 sellerId,请在店铺授权中填写 sellerId 后重试', {
|
||||||
|
statusCode: 502,
|
||||||
|
errorCode: 'kuaishou_industry_missing_token_seller_id',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return patchKuaishouIndustrySourceConfig({
|
const nextRefreshToken = tokenPayload.refreshToken || shop.refreshToken
|
||||||
|
const savedConfig = patchKuaishouIndustryShopConfig(nextSellerId, {
|
||||||
|
...shop,
|
||||||
|
enabled: shop.enabled !== false,
|
||||||
|
sellerId: nextSellerId,
|
||||||
|
shopId: shop.shopId || nextSellerId,
|
||||||
accessToken: tokenPayload.accessToken,
|
accessToken: tokenPayload.accessToken,
|
||||||
refreshToken: nextRefreshToken,
|
refreshToken: nextRefreshToken,
|
||||||
accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt,
|
accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt,
|
||||||
refreshTokenExpiresAt: tokenPayload.refreshTokenExpiresAt
|
refreshTokenExpiresAt: tokenPayload.refreshTokenExpiresAt
|
||||||
|| (tokenPayload.refreshToken ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString() : config.refreshTokenExpiresAt),
|
|| (tokenPayload.refreshToken ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString() : shop.refreshTokenExpiresAt),
|
||||||
sellerId: tokenPayload.sellerId || config.sellerId,
|
openId: tokenPayload.openId || shop.openId,
|
||||||
openId: tokenPayload.openId || config.openId,
|
grantedScopes: tokenPayload.grantedScopes || shop.grantedScopes,
|
||||||
grantedScopes: tokenPayload.grantedScopes || config.grantedScopes,
|
|
||||||
lastRefreshedAt: new Date().toISOString(),
|
lastRefreshedAt: new Date().toISOString(),
|
||||||
lastRefreshError: '',
|
lastRefreshError: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: savedConfig,
|
||||||
|
shop: resolveTokenShop(savedConfig, nextSellerId),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTokenResponse(
|
function normalizeTokenResponse(
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export async function consumeKuaishouIndustryVoucher(
|
|||||||
? { success: true as const }
|
? { success: true as const }
|
||||||
: await consumeCallback({
|
: await consumeCallback({
|
||||||
oid: voucher.oid,
|
oid: voucher.oid,
|
||||||
|
sellerId: String(voucher.seller_id || '').trim(),
|
||||||
etickets: [{
|
etickets: [{
|
||||||
id: voucher.voucher_code,
|
id: voucher.voucher_code,
|
||||||
num: 1,
|
num: 1,
|
||||||
|
|||||||
@@ -12,6 +12,23 @@ export type AdminKuaishouEticketSourceConfigInput = {
|
|||||||
shops?: AdminKuaishouEticketShopConfigWriteItemInput[]
|
shops?: AdminKuaishouEticketShopConfigWriteItemInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminKuaishouIndustryShopConfigInput = {
|
||||||
|
enabled?: boolean
|
||||||
|
sellerId?: string
|
||||||
|
shopId?: string
|
||||||
|
shopName?: string
|
||||||
|
customShopName?: string
|
||||||
|
authState?: string
|
||||||
|
accessToken?: string
|
||||||
|
refreshToken?: string
|
||||||
|
accessTokenExpiresAt?: string
|
||||||
|
refreshTokenExpiresAt?: string
|
||||||
|
openId?: string
|
||||||
|
grantedScopes?: string
|
||||||
|
lastRefreshedAt?: string
|
||||||
|
lastRefreshError?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AdminKuaishouIndustrySourceConfigInput = {
|
export type AdminKuaishouIndustrySourceConfigInput = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
@@ -35,10 +52,14 @@ export type AdminKuaishouIndustrySourceConfigInput = {
|
|||||||
shopId?: string
|
shopId?: string
|
||||||
shopName?: string
|
shopName?: string
|
||||||
version?: string
|
version?: string
|
||||||
|
shops?: AdminKuaishouIndustryShopConfigInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminKuaishouIndustryAuthorizationCodeInput = {
|
export type AdminKuaishouIndustryAuthorizationCodeInput = {
|
||||||
code?: string
|
code?: string
|
||||||
|
sellerId?: string
|
||||||
|
shopName?: string
|
||||||
|
customShopName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminNotificationBarkRecipientInput = {
|
export type AdminNotificationBarkRecipientInput = {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export type TaskUpdatePatch = {
|
|||||||
export type KuaishouIndustryVoucherUpsertInput = {
|
export type KuaishouIndustryVoucherUpsertInput = {
|
||||||
oid: string
|
oid: string
|
||||||
unitIndex: number
|
unitIndex: number
|
||||||
|
sellerId?: string
|
||||||
token?: string
|
token?: string
|
||||||
orderId?: number | string | null
|
orderId?: number | string | null
|
||||||
taskId?: number | string | null
|
taskId?: number | string | null
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export type KuaishouIndustryVoucherRow = {
|
|||||||
order_id: number | null
|
order_id: number | null
|
||||||
task_id: number | null
|
task_id: number | null
|
||||||
unit_index: number
|
unit_index: number
|
||||||
|
seller_id: string
|
||||||
token: string
|
token: string
|
||||||
status: string
|
status: string
|
||||||
valid_start_time: number | string
|
valid_start_time: number | string
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import type {
|
|||||||
AdminKuaishouEticketShopInfoResult,
|
AdminKuaishouEticketShopInfoResult,
|
||||||
AdminKuaishouEticketSourceConfig,
|
AdminKuaishouEticketSourceConfig,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
@@ -1070,12 +1071,45 @@ function KuaishouIndustryPanel({
|
|||||||
config: AdminKuaishouIndustryConfigResponse
|
config: AdminKuaishouIndustryConfigResponse
|
||||||
onChange: (config: AdminKuaishouIndustryConfigResponse) => void
|
onChange: (config: AdminKuaishouIndustryConfigResponse) => void
|
||||||
}) {
|
}) {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [actionLoading, setActionLoading] = useState('')
|
||||||
const [exchanging, setExchanging] = useState(false)
|
const [authorizationCodes, setAuthorizationCodes] = useState<Record<string, string>>({})
|
||||||
const [authorizationCode, setAuthorizationCode] = useState('')
|
|
||||||
const source = config.source
|
const source = config.source
|
||||||
const authorizationUrl = buildIndustryAuthorizationUrl(source)
|
const shops = source.shops || []
|
||||||
|
const enabledShopCount = shops.filter((shop) => shop.enabled !== false).length
|
||||||
|
const validTokenCount = shops.filter((shop) => shop.accessTokenStatus === 'valid').length
|
||||||
|
const problemTokenCount = shops.filter((shop) =>
|
||||||
|
['expired', 'expiring', 'missing'].includes(shop.accessTokenStatus),
|
||||||
|
).length
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const code = String(searchParams.get('code') || '').trim()
|
||||||
|
if (!code || shops.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = String(searchParams.get('state') || '').trim()
|
||||||
|
const matchedIndex = state
|
||||||
|
? shops.findIndex((shop) =>
|
||||||
|
[shop.authState, shop.sellerId, shop.shopId].some((value) => value === state),
|
||||||
|
)
|
||||||
|
: -1
|
||||||
|
const enabledIndex = shops.findIndex((shop) => shop.enabled !== false)
|
||||||
|
const targetIndex = matchedIndex >= 0 ? matchedIndex : Math.max(enabledIndex, 0)
|
||||||
|
const actionKey = buildIndustryShopActionKey(targetIndex)
|
||||||
|
|
||||||
|
setAuthorizationCodes((current) => {
|
||||||
|
if (current[actionKey] || Object.values(current).some((value) => value.trim() === code)) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[actionKey]: code,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [searchParams, shops])
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -1090,44 +1124,63 @@ function KuaishouIndustryPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshToken() {
|
async function refreshToken(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
||||||
setRefreshing(true)
|
const sellerId = shop.sellerId.trim()
|
||||||
|
if (!sellerId) {
|
||||||
|
showError('请先填写 sellerId')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setActionLoading(`refresh-${index}`)
|
||||||
try {
|
try {
|
||||||
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
||||||
onChange(saved.data)
|
onChange(saved.data)
|
||||||
const response = await refreshAdminKuaishouIndustryAccessToken()
|
const response = await refreshAdminKuaishouIndustryAccessToken({ sellerId })
|
||||||
onChange(response.data)
|
onChange(response.data)
|
||||||
showSuccess('accessToken 已刷新')
|
showSuccess(`${resolveIndustryShopDisplayName(shop)} 的 accessToken 已刷新`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : '刷新 accessToken 失败')
|
showError(error instanceof Error ? error.message : '刷新 accessToken 失败')
|
||||||
} finally {
|
} finally {
|
||||||
setRefreshing(false)
|
setActionLoading('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exchangeAuthorizationCode() {
|
async function exchangeAuthorizationCode(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
||||||
const code = authorizationCode.trim()
|
const sellerId = shop.sellerId.trim()
|
||||||
|
if (!sellerId) {
|
||||||
|
showError('请先填写 sellerId')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionKey = buildIndustryShopActionKey(index)
|
||||||
|
const code = String(authorizationCodes[actionKey] || '').trim()
|
||||||
if (!code) {
|
if (!code) {
|
||||||
showError('请输入授权 code')
|
showError('请输入授权 code')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setExchanging(true)
|
setActionLoading(`exchange-${index}`)
|
||||||
try {
|
try {
|
||||||
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
||||||
onChange(saved.data)
|
onChange(saved.data)
|
||||||
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({ code })
|
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({
|
||||||
|
code,
|
||||||
|
sellerId,
|
||||||
|
shopName: shop.shopName,
|
||||||
|
customShopName: shop.customShopName,
|
||||||
|
})
|
||||||
onChange(response.data)
|
onChange(response.data)
|
||||||
setAuthorizationCode('')
|
setAuthorizationCodes((current) => ({ ...current, [actionKey]: '' }))
|
||||||
showSuccess('授权 token 已保存')
|
showSuccess(`${resolveIndustryShopDisplayName(shop)} 的授权 token 已保存`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : '授权 code 换 token 失败')
|
showError(error instanceof Error ? error.message : '授权 code 换 token 失败')
|
||||||
} finally {
|
} finally {
|
||||||
setExchanging(false)
|
setActionLoading('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyAuthorizationUrl() {
|
async function copyAuthorizationUrl(shop: AdminKuaishouIndustryShopConfig) {
|
||||||
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, shop)
|
||||||
if (!authorizationUrl) {
|
if (!authorizationUrl) {
|
||||||
showError('授权链接未生成')
|
showError('授权链接未生成')
|
||||||
return
|
return
|
||||||
@@ -1141,7 +1194,8 @@ function KuaishouIndustryPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAuthorizationUrl() {
|
function openAuthorizationUrl(shop: AdminKuaishouIndustryShopConfig) {
|
||||||
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, shop)
|
||||||
if (!authorizationUrl) {
|
if (!authorizationUrl) {
|
||||||
showError('授权链接未生成')
|
showError('授权链接未生成')
|
||||||
return
|
return
|
||||||
@@ -1160,9 +1214,42 @@ function KuaishouIndustryPanel({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateShop(index: number, patch: Partial<AdminKuaishouIndustryShopConfig>) {
|
||||||
|
updateSource({
|
||||||
|
shops: shops.map((shop, shopIndex) =>
|
||||||
|
shopIndex === index ? { ...shop, ...patch } : shop,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateShopSellerId(index: number, sellerId: string) {
|
||||||
|
const shop = shops[index]
|
||||||
|
if (!shop) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShop(index, {
|
||||||
|
sellerId,
|
||||||
|
shopId: !shop.shopId || shop.shopId === shop.sellerId ? sellerId : shop.shopId,
|
||||||
|
authState: !shop.authState || shop.authState === shop.sellerId ? sellerId : shop.authState,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addShop() {
|
||||||
|
updateSource({
|
||||||
|
shops: [...shops, createEmptyIndustryShopConfig()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteShop(index: number) {
|
||||||
|
updateSource({
|
||||||
|
shops: shops.filter((_, shopIndex) => shopIndex !== index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function renderSecretInput(
|
function renderSecretInput(
|
||||||
label: string,
|
label: string,
|
||||||
field: 'appSecret' | 'signSecret' | 'messageSecret' | 'accessToken' | 'refreshToken',
|
field: 'appSecret' | 'signSecret' | 'messageSecret',
|
||||||
masked: string,
|
masked: string,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -1177,7 +1264,123 @@ function KuaishouIndustryPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessTokenStatus = resolveIndustryAccessTokenStatus(source)
|
const shopColumns: TableColumnsType<AdminKuaishouIndustryShopConfig> = [
|
||||||
|
{
|
||||||
|
title: '启用',
|
||||||
|
width: 76,
|
||||||
|
fixed: 'left',
|
||||||
|
render: (_, row, index) => (
|
||||||
|
<Switch
|
||||||
|
checked={row.enabled !== false}
|
||||||
|
onChange={(enabled) => updateShop(index, { enabled })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '店铺',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row, index) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Input
|
||||||
|
value={row.sellerId}
|
||||||
|
placeholder="sellerId"
|
||||||
|
onChange={(event) => updateShopSellerId(index, event.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={row.shopName}
|
||||||
|
placeholder="官方店铺名称"
|
||||||
|
onChange={(event) => updateShop(index, { shopName: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={row.customShopName}
|
||||||
|
placeholder="自定义店铺名"
|
||||||
|
onChange={(event) => updateShop(index, { customShopName: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Token 状态',
|
||||||
|
width: 150,
|
||||||
|
render: (_, row) => {
|
||||||
|
const status = resolveIndustryAccessTokenStatus(row)
|
||||||
|
return (
|
||||||
|
<div className="status-stack">
|
||||||
|
<Tag color={status.color}>{status.label}</Tag>
|
||||||
|
{row.accessTokenMasked ? <Typography.Text type="secondary">{row.accessTokenMasked}</Typography.Text> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '过期时间',
|
||||||
|
minWidth: 220,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<span>{row.accessTokenExpiresAt ? formatAdminDateTime(row.accessTokenExpiresAt) : '-'}</span>
|
||||||
|
<span className="muted">{formatIndustryTokenCountdown(row.accessTokenExpiresInSeconds)}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权操作',
|
||||||
|
width: 420,
|
||||||
|
render: (_, row, index) => {
|
||||||
|
const actionKey = buildIndustryShopActionKey(index)
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" className="full-width" size={8}>
|
||||||
|
<Space.Compact className="full-width">
|
||||||
|
<Input
|
||||||
|
value={authorizationCodes[actionKey] || ''}
|
||||||
|
placeholder="授权 code"
|
||||||
|
onChange={(event) =>
|
||||||
|
setAuthorizationCodes((current) => ({
|
||||||
|
...current,
|
||||||
|
[actionKey]: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onPressEnter={() => exchangeAuthorizationCode(row, index)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={actionLoading === `exchange-${index}`}
|
||||||
|
disabled={saving || Boolean(actionLoading && actionLoading !== `exchange-${index}`)}
|
||||||
|
onClick={() => exchangeAuthorizationCode(row, index)}
|
||||||
|
>
|
||||||
|
换取
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
<Space wrap>
|
||||||
|
<Button size="small" icon={<CopyOutlined />} onClick={() => copyAuthorizationUrl(row)}>
|
||||||
|
复制授权
|
||||||
|
</Button>
|
||||||
|
<Button size="small" icon={<LinkOutlined />} onClick={() => openAuthorizationUrl(row)}>
|
||||||
|
打开授权
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={actionLoading === `refresh-${index}`}
|
||||||
|
disabled={saving || Boolean(actionLoading && actionLoading !== `refresh-${index}`)}
|
||||||
|
onClick={() => refreshToken(row, index)}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
disabled={saving || Boolean(actionLoading)}
|
||||||
|
onClick={() => deleteShop(index)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="platform-panel-stack">
|
<section className="platform-panel-stack">
|
||||||
@@ -1192,45 +1395,37 @@ function KuaishouIndustryPanel({
|
|||||||
|
|
||||||
<div className="metric-grid four">
|
<div className="metric-grid four">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="回调状态"
|
label="接入状态"
|
||||||
value={source.enabled ? '已启用' : '停用'}
|
value={source.enabled ? '已启用' : '停用'}
|
||||||
detail={source.enabled ? '配置启用' : '配置停用'}
|
detail={source.enabled ? '配置启用' : '配置停用'}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="accessToken"
|
label="店铺授权"
|
||||||
value={accessTokenStatus.label}
|
value={`${enabledShopCount}/${shops.length}`}
|
||||||
detail={formatIndustryTokenCountdown(source.accessTokenExpiresInSeconds)}
|
detail="启用店铺 / 全部店铺"
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="过期时间"
|
label="有效 token"
|
||||||
value={source.accessTokenExpiresAt ? formatAdminDateTime(source.accessTokenExpiresAt) : '-'}
|
value={String(validTokenCount)}
|
||||||
detail={source.lastRefreshedAt ? `刷新:${formatAdminDateTime(source.lastRefreshedAt)}` : '尚未刷新'}
|
detail={problemTokenCount > 0 ? `${problemTokenCount} 个需处理` : '全部正常'}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="refreshToken"
|
label="授权范围"
|
||||||
value={source.hasRefreshToken ? '已配置' : '未配置'}
|
value={source.scopes || '-'}
|
||||||
detail={source.refreshTokenExpiresAt ? `到期:${formatAdminDateTime(source.refreshTokenExpiresAt)}` : source.refreshTokenMasked || '-'}
|
detail={source.redirectUri || '-'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title="快手行业电子凭证"
|
title="应用参数"
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
|
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
|
||||||
<Button
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
loading={refreshing}
|
|
||||||
disabled={saving || exchanging}
|
|
||||||
onClick={refreshToken}
|
|
||||||
>
|
|
||||||
保存并刷新 token
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
disabled={refreshing || exchanging}
|
disabled={Boolean(actionLoading)}
|
||||||
onClick={saveConfig}
|
onClick={saveConfig}
|
||||||
>
|
>
|
||||||
保存配置
|
保存配置
|
||||||
@@ -1238,12 +1433,6 @@ function KuaishouIndustryPanel({
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Space wrap size={[8, 8]} className="platform-section-gap">
|
|
||||||
<Tag color={accessTokenStatus.color}>{accessTokenStatus.label}</Tag>
|
|
||||||
{source.accessTokenMasked ? <Tag>{source.accessTokenMasked}</Tag> : null}
|
|
||||||
{source.refreshTokenMasked ? <Tag>{source.refreshTokenMasked}</Tag> : null}
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<div className="platform-form-grid">
|
<div className="platform-form-grid">
|
||||||
<FieldSwitch
|
<FieldSwitch
|
||||||
label="启用配置"
|
label="启用配置"
|
||||||
@@ -1265,27 +1454,6 @@ function KuaishouIndustryPanel({
|
|||||||
value={source.appKey}
|
value={source.appKey}
|
||||||
onChange={(appKey) => updateSource({ appKey })}
|
onChange={(appKey) => updateSource({ appKey })}
|
||||||
/>
|
/>
|
||||||
{renderSecretInput('appSecret', 'appSecret', source.appSecretMasked)}
|
|
||||||
{renderSecretInput('signSecret', 'signSecret', source.signSecretMasked)}
|
|
||||||
{renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
size="small"
|
|
||||||
title="授权"
|
|
||||||
className="platform-section-gap"
|
|
||||||
extra={
|
|
||||||
<Space wrap>
|
|
||||||
<Button icon={<CopyOutlined />} disabled={!authorizationUrl} onClick={copyAuthorizationUrl}>
|
|
||||||
复制链接
|
|
||||||
</Button>
|
|
||||||
<Button icon={<LinkOutlined />} disabled={!authorizationUrl} onClick={openAuthorizationUrl}>
|
|
||||||
打开授权
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="platform-form-grid">
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label="回调地址"
|
label="回调地址"
|
||||||
value={source.redirectUri}
|
value={source.redirectUri}
|
||||||
@@ -1297,73 +1465,128 @@ function KuaishouIndustryPanel({
|
|||||||
onChange={(scopes) => updateSource({ scopes })}
|
onChange={(scopes) => updateSource({ scopes })}
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label="state"
|
label="全局 state 兜底"
|
||||||
value={source.authState}
|
value={source.authState}
|
||||||
onChange={(authState) => updateSource({ authState })}
|
onChange={(authState) => updateSource({ authState })}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<Typography.Text type="secondary">授权链接</Typography.Text>
|
|
||||||
<Input readOnly value={authorizationUrl} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography.Text type="secondary">openId</Typography.Text>
|
|
||||||
<Input
|
|
||||||
value={source.openId}
|
|
||||||
onChange={(event) => updateSource({ openId: event.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography.Text type="secondary">已授权 scope</Typography.Text>
|
|
||||||
<Input
|
|
||||||
value={source.grantedScopes}
|
|
||||||
onChange={(event) => updateSource({ grantedScopes: event.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Space.Compact className="full-width platform-section-gap">
|
|
||||||
<Input
|
|
||||||
value={authorizationCode}
|
|
||||||
placeholder="code"
|
|
||||||
onChange={(event) => setAuthorizationCode(event.target.value)}
|
|
||||||
onPressEnter={exchangeAuthorizationCode}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={exchanging}
|
|
||||||
disabled={saving || refreshing}
|
|
||||||
onClick={exchangeAuthorizationCode}
|
|
||||||
>
|
|
||||||
换取 token
|
|
||||||
</Button>
|
|
||||||
</Space.Compact>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card size="small" title="Token" className="platform-section-gap">
|
|
||||||
<div className="platform-form-grid">
|
|
||||||
{renderSecretInput('accessToken', 'accessToken', source.accessTokenMasked)}
|
|
||||||
{renderSecretInput('refreshToken', 'refreshToken', source.refreshTokenMasked)}
|
|
||||||
<LabeledInput
|
|
||||||
label="accessToken 过期时间"
|
|
||||||
value={source.accessTokenExpiresAt}
|
|
||||||
onChange={(accessTokenExpiresAt) => updateSource({ accessTokenExpiresAt })}
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
label="refreshToken 过期时间"
|
|
||||||
value={source.refreshTokenExpiresAt}
|
|
||||||
onChange={(refreshTokenExpiresAt) => updateSource({ refreshTokenExpiresAt })}
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
label="sellerId"
|
|
||||||
value={source.sellerId}
|
|
||||||
onChange={(sellerId) => updateSource({ sellerId })}
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label="版本"
|
label="版本"
|
||||||
value={source.version}
|
value={source.version}
|
||||||
onChange={(version) => updateSource({ version })}
|
onChange={(version) => updateSource({ version })}
|
||||||
/>
|
/>
|
||||||
|
{renderSecretInput('appSecret', 'appSecret', source.appSecretMasked)}
|
||||||
|
{renderSecretInput('signSecret', 'signSecret', source.signSecretMasked)}
|
||||||
|
{renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="店铺授权"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={addShop}>
|
||||||
|
新增店铺
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
loading={saving}
|
||||||
|
disabled={Boolean(actionLoading)}
|
||||||
|
onClick={saveConfig}
|
||||||
|
>
|
||||||
|
保存店铺
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shops.length === 0 ? (
|
||||||
|
<Empty description="暂无店铺授权" />
|
||||||
|
) : (
|
||||||
|
<Table<AdminKuaishouIndustryShopConfig>
|
||||||
|
rowKey={(row, index) => buildIndustryShopRowKey(row, index || 0)}
|
||||||
|
columns={shopColumns}
|
||||||
|
dataSource={shops}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 1120 }}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: (row, index) => {
|
||||||
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, row)
|
||||||
|
return (
|
||||||
|
<div className="platform-form-grid">
|
||||||
|
<LabeledInput
|
||||||
|
label="shopId"
|
||||||
|
value={row.shopId}
|
||||||
|
onChange={(shopId) => updateShop(index, { shopId })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="官方店铺名称"
|
||||||
|
value={row.shopName}
|
||||||
|
onChange={(shopName) => updateShop(index, { shopName })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="自定义店铺名"
|
||||||
|
value={row.customShopName}
|
||||||
|
onChange={(customShopName) => updateShop(index, { customShopName })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="state"
|
||||||
|
value={row.authState}
|
||||||
|
onChange={(authState) => updateShop(index, { authState })}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">授权链接</Typography.Text>
|
||||||
|
<Input readOnly value={authorizationUrl} />
|
||||||
|
</div>
|
||||||
|
<LabeledInput
|
||||||
|
label="openId"
|
||||||
|
value={row.openId}
|
||||||
|
onChange={(openId) => updateShop(index, { openId })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="已授权 scope"
|
||||||
|
value={row.grantedScopes}
|
||||||
|
onChange={(grantedScopes) => updateShop(index, { grantedScopes })}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">accessToken</Typography.Text>
|
||||||
|
<Input.Password
|
||||||
|
value={row.accessToken}
|
||||||
|
placeholder={row.accessTokenMasked || '留空保持原值'}
|
||||||
|
onChange={(event) => updateShop(index, { accessToken: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">refreshToken</Typography.Text>
|
||||||
|
<Input.Password
|
||||||
|
value={row.refreshToken}
|
||||||
|
placeholder={row.refreshTokenMasked || '留空保持原值'}
|
||||||
|
onChange={(event) => updateShop(index, { refreshToken: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LabeledInput
|
||||||
|
label="accessToken 过期时间"
|
||||||
|
value={row.accessTokenExpiresAt}
|
||||||
|
onChange={(accessTokenExpiresAt) => updateShop(index, { accessTokenExpiresAt })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="refreshToken 过期时间"
|
||||||
|
value={row.refreshTokenExpiresAt}
|
||||||
|
onChange={(refreshTokenExpiresAt) => updateShop(index, { refreshTokenExpiresAt })}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">最近错误</Typography.Text>
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
value={row.lastRefreshError}
|
||||||
|
onChange={(event) => updateShop(index, { lastRefreshError: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@@ -1815,7 +2038,9 @@ function CloudtentaclesPlatformPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveIndustryAccessTokenStatus(source: AdminKuaishouIndustrySourceConfig) {
|
function resolveIndustryAccessTokenStatus(
|
||||||
|
source: Pick<AdminKuaishouIndustrySourceConfig, 'accessTokenStatus'>,
|
||||||
|
) {
|
||||||
switch (source.accessTokenStatus) {
|
switch (source.accessTokenStatus) {
|
||||||
case 'valid':
|
case 'valid':
|
||||||
return { label: '有效', color: 'green' }
|
return { label: '有效', color: 'green' }
|
||||||
@@ -1852,9 +2077,12 @@ function formatIndustryTokenCountdown(value: number | null) {
|
|||||||
return `${minutes} 分钟后过期`
|
return `${minutes} 分钟后过期`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig) {
|
function buildIndustryAuthorizationUrl(
|
||||||
|
source: AdminKuaishouIndustrySourceConfig,
|
||||||
|
shop?: AdminKuaishouIndustryShopConfig,
|
||||||
|
) {
|
||||||
if (!source.authBaseUrl || !source.appKey || !source.redirectUri || !source.scopes) {
|
if (!source.authBaseUrl || !source.appKey || !source.redirectUri || !source.scopes) {
|
||||||
return source.authorizationUrl || ''
|
return shop?.authorizationUrl || source.authorizationUrl || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1863,16 +2091,55 @@ function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig
|
|||||||
url.searchParams.set('redirect_uri', source.redirectUri)
|
url.searchParams.set('redirect_uri', source.redirectUri)
|
||||||
url.searchParams.set('scope', normalizeIndustryScopeText(source.scopes))
|
url.searchParams.set('scope', normalizeIndustryScopeText(source.scopes))
|
||||||
url.searchParams.set('response_type', 'code')
|
url.searchParams.set('response_type', 'code')
|
||||||
if (source.authState) {
|
const state = String(shop?.authState || source.authState || shop?.sellerId || '').trim()
|
||||||
url.searchParams.set('state', source.authState)
|
if (state) {
|
||||||
|
url.searchParams.set('state', state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
} catch {
|
} catch {
|
||||||
return source.authorizationUrl || ''
|
return shop?.authorizationUrl || source.authorizationUrl || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEmptyIndustryShopConfig(): AdminKuaishouIndustryShopConfig {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
sellerId: '',
|
||||||
|
shopId: '',
|
||||||
|
shopName: '',
|
||||||
|
customShopName: '',
|
||||||
|
authState: '',
|
||||||
|
authorizationUrl: '',
|
||||||
|
accessToken: '',
|
||||||
|
accessTokenMasked: '',
|
||||||
|
hasAccessToken: false,
|
||||||
|
refreshToken: '',
|
||||||
|
refreshTokenMasked: '',
|
||||||
|
hasRefreshToken: false,
|
||||||
|
accessTokenExpiresAt: '',
|
||||||
|
refreshTokenExpiresAt: '',
|
||||||
|
accessTokenStatus: 'missing',
|
||||||
|
accessTokenExpiresInSeconds: null,
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
|
lastRefreshedAt: '',
|
||||||
|
lastRefreshError: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndustryShopRowKey(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
||||||
|
return shop.sellerId || shop.shopId || `shop-${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndustryShopActionKey(index: number) {
|
||||||
|
return `shop-${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIndustryShopDisplayName(shop: AdminKuaishouIndustryShopConfig) {
|
||||||
|
return shop.customShopName || shop.shopName || shop.sellerId || '未命名店铺'
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeIndustryScopeText(value: string) {
|
function normalizeIndustryScopeText(value: string) {
|
||||||
return value
|
return value
|
||||||
.split(/[,\s]+/)
|
.split(/[,\s]+/)
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export function saveAdminKuaishouIndustrySourceConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshAdminKuaishouIndustryAccessToken() {
|
export function refreshAdminKuaishouIndustryAccessToken(
|
||||||
|
payload: { sellerId?: string } = {},
|
||||||
|
) {
|
||||||
return apiPost<AdminKuaishouIndustryRefreshTokenResponse>(
|
return apiPost<AdminKuaishouIndustryRefreshTokenResponse>(
|
||||||
'/api/v1/admin/platform-config/kuaishou-industry-source/refresh-token',
|
'/api/v1/admin/platform-config/kuaishou-industry-source/refresh-token',
|
||||||
{},
|
payload,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type {
|
|||||||
AdminKuaishouEticketDetailResult,
|
AdminKuaishouEticketDetailResult,
|
||||||
AdminKuaishouEticketConsumeResult,
|
AdminKuaishouEticketConsumeResult,
|
||||||
AdminKuaishouIndustryAccessTokenStatus,
|
AdminKuaishouIndustryAccessTokenStatus,
|
||||||
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryRefreshTokenResponse,
|
AdminKuaishouIndustryRefreshTokenResponse,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type {
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
AdminKuaishouIndustryAccessTokenStatus,
|
AdminKuaishouIndustryAccessTokenStatus,
|
||||||
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryRefreshTokenResponse,
|
AdminKuaishouIndustryRefreshTokenResponse,
|
||||||
|
|||||||
@@ -5,6 +5,30 @@ export type AdminKuaishouIndustryAccessTokenStatus =
|
|||||||
| 'expiring'
|
| 'expiring'
|
||||||
| 'valid'
|
| 'valid'
|
||||||
|
|
||||||
|
export interface AdminKuaishouIndustryShopConfig {
|
||||||
|
enabled: boolean
|
||||||
|
sellerId: string
|
||||||
|
shopId: string
|
||||||
|
shopName: string
|
||||||
|
customShopName: string
|
||||||
|
authState: string
|
||||||
|
authorizationUrl: string
|
||||||
|
accessToken: string
|
||||||
|
accessTokenMasked: string
|
||||||
|
hasAccessToken: boolean
|
||||||
|
refreshToken: string
|
||||||
|
refreshTokenMasked: string
|
||||||
|
hasRefreshToken: boolean
|
||||||
|
accessTokenExpiresAt: string
|
||||||
|
refreshTokenExpiresAt: string
|
||||||
|
accessTokenStatus: AdminKuaishouIndustryAccessTokenStatus
|
||||||
|
accessTokenExpiresInSeconds: number | null
|
||||||
|
openId: string
|
||||||
|
grantedScopes: string
|
||||||
|
lastRefreshedAt: string
|
||||||
|
lastRefreshError: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminKuaishouIndustrySourceConfig {
|
export interface AdminKuaishouIndustrySourceConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
@@ -41,6 +65,7 @@ export interface AdminKuaishouIndustrySourceConfig {
|
|||||||
shopId: string
|
shopId: string
|
||||||
shopName: string
|
shopName: string
|
||||||
version: string
|
version: string
|
||||||
|
shops: AdminKuaishouIndustryShopConfig[]
|
||||||
lastRefreshedAt: string
|
lastRefreshedAt: string
|
||||||
lastRefreshError: string
|
lastRefreshError: string
|
||||||
}
|
}
|
||||||
@@ -52,10 +77,14 @@ export interface AdminKuaishouIndustryConfigResponse {
|
|||||||
|
|
||||||
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
||||||
refreshed: boolean
|
refreshed: boolean
|
||||||
|
shop?: AdminKuaishouIndustryShopConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminKuaishouIndustryAuthorizationCodePayload {
|
export interface AdminKuaishouIndustryAuthorizationCodePayload {
|
||||||
code: string
|
code: string
|
||||||
|
sellerId?: string
|
||||||
|
shopName?: string
|
||||||
|
customShopName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminKuaishouIndustryExchangeCodeResponse = AdminKuaishouIndustryRefreshTokenResponse
|
export type AdminKuaishouIndustryExchangeCodeResponse = AdminKuaishouIndustryRefreshTokenResponse
|
||||||
|
|||||||
Reference in New Issue
Block a user