补充行业凭证授权换 token

This commit is contained in:
yml2213
2026-07-07 23:17:16 +08:00
parent 81bfd8b078
commit 3940e59df6
13 changed files with 491 additions and 41 deletions
@@ -1,6 +1,8 @@
import {
CheckCircleOutlined,
CopyOutlined,
DeleteOutlined,
LinkOutlined,
PlusOutlined,
ReloadOutlined,
SaveOutlined,
@@ -32,6 +34,7 @@ import PageHeader from '@/components/admin/PageHeader'
import { isFeedbackDismissed, showError, showPrompt, showSuccess } from '@/lib/feedback'
import {
deleteAdminCloudtentaclesSource,
exchangeAdminKuaishouIndustryAuthorizationCode,
failAdminNinetyoneOrder,
fetchAdminCloudtentaclesAsset,
fetchAdminCloudtentaclesSkuList,
@@ -1069,7 +1072,10 @@ function KuaishouIndustryPanel({
}) {
const [saving, setSaving] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [exchanging, setExchanging] = useState(false)
const [authorizationCode, setAuthorizationCode] = useState('')
const source = config.source
const authorizationUrl = buildIndustryAuthorizationUrl(source)
async function saveConfig() {
setSaving(true)
@@ -1099,6 +1105,51 @@ function KuaishouIndustryPanel({
}
}
async function exchangeAuthorizationCode() {
const code = authorizationCode.trim()
if (!code) {
showError('请输入授权 code')
return
}
setExchanging(true)
try {
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
onChange(saved.data)
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({ code })
onChange(response.data)
setAuthorizationCode('')
showSuccess('授权 token 已保存')
} catch (error) {
showError(error instanceof Error ? error.message : '授权 code 换 token 失败')
} finally {
setExchanging(false)
}
}
async function copyAuthorizationUrl() {
if (!authorizationUrl) {
showError('授权链接未生成')
return
}
try {
await navigator.clipboard.writeText(authorizationUrl)
showSuccess('授权链接已复制')
} catch (error) {
showError(error instanceof Error ? error.message : '复制授权链接失败')
}
}
function openAuthorizationUrl() {
if (!authorizationUrl) {
showError('授权链接未生成')
return
}
window.open(authorizationUrl, '_blank', 'noopener,noreferrer')
}
function updateSource(patch: Partial<AdminKuaishouIndustrySourceConfig>) {
onChange({
...config,
@@ -1170,7 +1221,7 @@ function KuaishouIndustryPanel({
<Button
icon={<ReloadOutlined />}
loading={refreshing}
disabled={saving}
disabled={saving || exchanging}
onClick={refreshToken}
>
token
@@ -1179,7 +1230,7 @@ function KuaishouIndustryPanel({
type="primary"
icon={<SaveOutlined />}
loading={saving}
disabled={refreshing}
disabled={refreshing || exchanging}
onClick={saveConfig}
>
@@ -1210,7 +1261,7 @@ function KuaishouIndustryPanel({
onChange={(baseUrl) => updateSource({ baseUrl })}
/>
<LabeledInput
label="授权 API"
label="授权页地址"
value={source.authBaseUrl}
onChange={(authBaseUrl) => updateSource({ authBaseUrl })}
/>
@@ -1224,6 +1275,74 @@ function KuaishouIndustryPanel({
{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
label="回调地址"
value={source.redirectUri}
onChange={(redirectUri) => updateSource({ redirectUri })}
/>
<LabeledInput
label="scope"
value={source.scopes}
onChange={(scopes) => updateSource({ scopes })}
/>
<LabeledInput
label="state"
value={source.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)}
@@ -1738,6 +1857,35 @@ function formatIndustryTokenCountdown(value: number | null) {
return `${minutes} 分钟后过期`
}
function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig) {
if (!source.authBaseUrl || !source.appKey || !source.redirectUri || !source.scopes) {
return source.authorizationUrl || ''
}
try {
const url = new URL(`${source.authBaseUrl.replace(/\/+$/, '')}/oauth/authorize`)
url.searchParams.set('app_id', source.appKey)
url.searchParams.set('redirect_uri', source.redirectUri)
url.searchParams.set('scope', normalizeIndustryScopeText(source.scopes))
url.searchParams.set('response_type', 'code')
if (source.authState) {
url.searchParams.set('state', source.authState)
}
return url.toString()
} catch {
return source.authorizationUrl || ''
}
}
function normalizeIndustryScopeText(value: string) {
return value
.split(/[,\s]+/)
.map((item) => item.trim())
.filter(Boolean)
.join(',')
}
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
return (
<Card className="metric-card">