增加行业凭证后台 token 配置
This commit is contained in:
@@ -38,17 +38,20 @@ import {
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
fetchAdminKuaishouEticketSourceConfig,
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
fetchAdminKuaishouIndustrySourceConfig,
|
||||
fetchAdminNinetyoneOrders,
|
||||
fetchAdminNotificationConfig,
|
||||
fetchAdminScheduledJobsConfig,
|
||||
matchAdminKuaishouFeifeiProduct,
|
||||
queryAdminKuaishouEticketDetail,
|
||||
queryAdminKuaishouEticketShopInfo,
|
||||
refreshAdminKuaishouIndustryAccessToken,
|
||||
retryAdminNinetyoneOrder,
|
||||
runAdminScheduledJob,
|
||||
saveAdminCloudtentaclesSourceConfig,
|
||||
saveAdminKuaishouEticketSourceConfig,
|
||||
saveAdminKuaishouFeifeiConfig,
|
||||
saveAdminKuaishouIndustrySourceConfig,
|
||||
saveAdminNotificationConfig,
|
||||
saveAdminScheduledJobsConfig,
|
||||
sendAdminCloudtentaclesSmsCode,
|
||||
@@ -64,6 +67,8 @@ import type {
|
||||
AdminKuaishouEticketShopConfigItem,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
@@ -83,6 +88,7 @@ type PlatformTab =
|
||||
| 'ninetyone'
|
||||
| 'notifications'
|
||||
| 'kuaishouEticket'
|
||||
| 'kuaishouIndustry'
|
||||
| 'kuaishouFeifei'
|
||||
| 'cloudtentacles'
|
||||
|
||||
@@ -110,6 +116,7 @@ const platformTabs: PlatformTab[] = [
|
||||
'ninetyone',
|
||||
'notifications',
|
||||
'kuaishouEticket',
|
||||
'kuaishouIndustry',
|
||||
'kuaishouFeifei',
|
||||
'cloudtentacles',
|
||||
]
|
||||
@@ -125,6 +132,7 @@ export default function AdminPlatformShopsPage() {
|
||||
filePath: string
|
||||
source: AdminKuaishouEticketSourceConfig
|
||||
} | null>(null)
|
||||
const [industryConfig, setIndustryConfig] = useState<AdminKuaishouIndustryConfigResponse | null>(null)
|
||||
const [feifeiConfig, setFeifeiConfig] = useState<AdminKuaishouFeifeiConfigResponse | null>(null)
|
||||
const [cloudtentaclesConfig, setCloudtentaclesConfig] =
|
||||
useState<AdminCloudtentaclesSourceConfigResponse | null>(null)
|
||||
@@ -148,6 +156,7 @@ export default function AdminPlatformShopsPage() {
|
||||
notificationResponse,
|
||||
scheduledJobsResponse,
|
||||
eticketResponse,
|
||||
industryResponse,
|
||||
feifeiResponse,
|
||||
cloudtentaclesResponse,
|
||||
ninetyoneResponse,
|
||||
@@ -155,6 +164,7 @@ export default function AdminPlatformShopsPage() {
|
||||
fetchAdminNotificationConfig(),
|
||||
fetchAdminScheduledJobsConfig(),
|
||||
fetchAdminKuaishouEticketSourceConfig(),
|
||||
fetchAdminKuaishouIndustrySourceConfig(),
|
||||
fetchAdminKuaishouFeifeiConfig(),
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
fetchAdminNinetyoneOrders({ page: 1, pageSize: 20 }),
|
||||
@@ -163,6 +173,7 @@ export default function AdminPlatformShopsPage() {
|
||||
setNotificationConfig(notificationResponse.data)
|
||||
setScheduledJobs(scheduledJobsResponse.data)
|
||||
setEticketConfig(eticketResponse.data)
|
||||
setIndustryConfig(industryResponse.data)
|
||||
setFeifeiConfig(feifeiResponse.data)
|
||||
setCloudtentaclesConfig(cloudtentaclesResponse.data)
|
||||
setNinetyoneData(ninetyoneResponse.data)
|
||||
@@ -243,6 +254,18 @@ export default function AdminPlatformShopsPage() {
|
||||
<Empty description="快手小店配置未加载" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'kuaishouIndustry',
|
||||
label: '行业电子凭证',
|
||||
children: industryConfig ? (
|
||||
<KuaishouIndustryPanel
|
||||
config={industryConfig}
|
||||
onChange={setIndustryConfig}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="快手行业电子凭证配置未加载" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'kuaishouFeifei',
|
||||
label: 'kuaishou-feifei',
|
||||
@@ -1037,6 +1060,201 @@ function KuaishouEticketPanel({
|
||||
)
|
||||
}
|
||||
|
||||
function KuaishouIndustryPanel({
|
||||
config,
|
||||
onChange,
|
||||
}: {
|
||||
config: AdminKuaishouIndustryConfigResponse
|
||||
onChange: (config: AdminKuaishouIndustryConfigResponse) => void
|
||||
}) {
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const source = config.source
|
||||
|
||||
async function saveConfig() {
|
||||
setSaving(true)
|
||||
try {
|
||||
const response = await saveAdminKuaishouIndustrySourceConfig(source)
|
||||
onChange(response.data)
|
||||
showSuccess('快手行业电子凭证配置已保存')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '保存快手行业电子凭证配置失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
||||
onChange(saved.data)
|
||||
const response = await refreshAdminKuaishouIndustryAccessToken()
|
||||
onChange(response.data)
|
||||
showSuccess('accessToken 已刷新')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '刷新 accessToken 失败')
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateSource(patch: Partial<AdminKuaishouIndustrySourceConfig>) {
|
||||
onChange({
|
||||
...config,
|
||||
source: {
|
||||
...source,
|
||||
...patch,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function renderSecretInput(
|
||||
label: string,
|
||||
field: 'appSecret' | 'signSecret' | 'messageSecret' | 'accessToken' | 'refreshToken',
|
||||
masked: string,
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text type="secondary">{label}</Typography.Text>
|
||||
<Input.Password
|
||||
value={source[field]}
|
||||
placeholder={masked || '未配置'}
|
||||
onChange={(event) => updateSource({ [field]: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const accessTokenStatus = resolveIndustryAccessTokenStatus(source)
|
||||
|
||||
return (
|
||||
<section className="platform-panel-stack">
|
||||
{source.lastRefreshError ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="最近一次刷新失败"
|
||||
description={source.lastRefreshError}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="metric-grid four">
|
||||
<MetricCard
|
||||
label="回调状态"
|
||||
value={source.enabled && source.sendCallbackEnabled ? '已启用' : '停用'}
|
||||
detail={source.enabled ? '配置启用' : '配置停用'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="accessToken"
|
||||
value={accessTokenStatus.label}
|
||||
detail={formatIndustryTokenCountdown(source.accessTokenExpiresInSeconds)}
|
||||
/>
|
||||
<MetricCard
|
||||
label="过期时间"
|
||||
value={source.accessTokenExpiresAt ? formatAdminDateTime(source.accessTokenExpiresAt) : '-'}
|
||||
detail={source.lastRefreshedAt ? `刷新:${formatAdminDateTime(source.lastRefreshedAt)}` : '尚未刷新'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="refreshToken"
|
||||
value={source.hasRefreshToken ? '已配置' : '未配置'}
|
||||
detail={source.refreshTokenExpiresAt ? `到期:${formatAdminDateTime(source.refreshTokenExpiresAt)}` : source.refreshTokenMasked || '-'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="快手行业电子凭证"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={refreshing}
|
||||
disabled={saving}
|
||||
onClick={refreshToken}
|
||||
>
|
||||
保存并刷新 token
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
disabled={refreshing}
|
||||
onClick={saveConfig}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</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">
|
||||
<FieldSwitch
|
||||
label="启用配置"
|
||||
checked={source.enabled}
|
||||
onChange={(enabled) => updateSource({ enabled })}
|
||||
/>
|
||||
<FieldSwitch
|
||||
label="通知快手回调"
|
||||
checked={source.sendCallbackEnabled}
|
||||
onChange={(sendCallbackEnabled) => updateSource({ sendCallbackEnabled })}
|
||||
/>
|
||||
<LabeledInput
|
||||
label="开放平台 API"
|
||||
value={source.baseUrl}
|
||||
onChange={(baseUrl) => updateSource({ baseUrl })}
|
||||
/>
|
||||
<LabeledInput
|
||||
label="授权 API"
|
||||
value={source.authBaseUrl}
|
||||
onChange={(authBaseUrl) => updateSource({ authBaseUrl })}
|
||||
/>
|
||||
<LabeledInput
|
||||
label="appKey"
|
||||
value={source.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="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
|
||||
label="版本"
|
||||
value={source.version}
|
||||
onChange={(version) => updateSource({ version })}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function KuaishouFeifeiPlatformPanel({
|
||||
config,
|
||||
onChange,
|
||||
@@ -1483,6 +1701,43 @@ function CloudtentaclesPlatformPanel({
|
||||
)
|
||||
}
|
||||
|
||||
function resolveIndustryAccessTokenStatus(source: AdminKuaishouIndustrySourceConfig) {
|
||||
switch (source.accessTokenStatus) {
|
||||
case 'valid':
|
||||
return { label: '有效', color: 'green' }
|
||||
case 'expiring':
|
||||
return { label: '即将过期', color: 'orange' }
|
||||
case 'expired':
|
||||
return { label: '已过期', color: 'red' }
|
||||
case 'unknown':
|
||||
return { label: '未知', color: 'blue' }
|
||||
case 'missing':
|
||||
default:
|
||||
return { label: '未配置', color: 'default' }
|
||||
}
|
||||
}
|
||||
|
||||
function formatIndustryTokenCountdown(value: number | null) {
|
||||
if (value === null || value === undefined) {
|
||||
return '未记录过期时间'
|
||||
}
|
||||
|
||||
if (value <= 0) {
|
||||
return '已过期'
|
||||
}
|
||||
|
||||
const days = Math.floor(value / 86_400)
|
||||
const hours = Math.floor((value % 86_400) / 3_600)
|
||||
const minutes = Math.floor((value % 3_600) / 60)
|
||||
if (days > 0) {
|
||||
return `${days} 天 ${hours} 小时后过期`
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours} 小时 ${minutes} 分钟后过期`
|
||||
}
|
||||
return `${minutes} 分钟后过期`
|
||||
}
|
||||
|
||||
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
|
||||
return (
|
||||
<Card className="metric-card">
|
||||
|
||||
@@ -2,5 +2,6 @@ export * from './notifications'
|
||||
export * from './scheduled-jobs'
|
||||
export * from './ninetyone'
|
||||
export * from './kuaishou-eticket'
|
||||
export * from './kuaishou-industry'
|
||||
export * from './kuaishou-feifei'
|
||||
export * from './cloudtentacles'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouIndustrySourceConfig() {
|
||||
return apiGet<AdminKuaishouIndustryConfigResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-industry-source',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminKuaishouIndustrySourceConfig(
|
||||
payload: AdminKuaishouIndustrySourceConfig,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryConfigResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-industry-source',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function refreshAdminKuaishouIndustryAccessToken() {
|
||||
return apiPost<AdminKuaishouIndustryRefreshTokenResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-industry-source/refresh-token',
|
||||
{},
|
||||
)
|
||||
}
|
||||
@@ -50,6 +50,10 @@ export type {
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouIndustryAccessTokenStatus,
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
|
||||
@@ -30,6 +30,13 @@ export type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from './kuaishou-eticket'
|
||||
|
||||
export type {
|
||||
AdminKuaishouIndustryAccessTokenStatus,
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
} from './kuaishou-industry'
|
||||
|
||||
export type {
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export type AdminKuaishouIndustryAccessTokenStatus =
|
||||
| 'missing'
|
||||
| 'unknown'
|
||||
| 'expired'
|
||||
| 'expiring'
|
||||
| 'valid'
|
||||
|
||||
export interface AdminKuaishouIndustrySourceConfig {
|
||||
enabled: boolean
|
||||
sendCallbackEnabled: boolean
|
||||
baseUrl: string
|
||||
authBaseUrl: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
appSecretMasked: string
|
||||
hasAppSecret: boolean
|
||||
signSecret: string
|
||||
signSecretMasked: string
|
||||
hasSignSecret: boolean
|
||||
messageSecret: string
|
||||
messageSecretMasked: string
|
||||
hasMessageSecret: boolean
|
||||
accessToken: string
|
||||
accessTokenMasked: string
|
||||
hasAccessToken: boolean
|
||||
refreshToken: string
|
||||
refreshTokenMasked: string
|
||||
hasRefreshToken: boolean
|
||||
accessTokenExpiresAt: string
|
||||
refreshTokenExpiresAt: string
|
||||
accessTokenStatus: AdminKuaishouIndustryAccessTokenStatus
|
||||
accessTokenExpiresInSeconds: number | null
|
||||
sellerId: string
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
version: string
|
||||
lastRefreshedAt: string
|
||||
lastRefreshError: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryConfigResponse {
|
||||
filePath: string
|
||||
source: AdminKuaishouIndustrySourceConfig
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
||||
refreshed: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user