增加行业凭证后台 token 配置

This commit is contained in:
yml2213
2026-07-07 23:00:24 +08:00
parent f6cbd7993f
commit 81bfd8b078
24 changed files with 1315 additions and 32 deletions
@@ -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">