补充行业凭证授权换 token
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryExchangeCodeResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
} from '@/types/admin'
|
||||
@@ -26,3 +28,12 @@ export function refreshAdminKuaishouIndustryAccessToken() {
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function exchangeAdminKuaishouIndustryAuthorizationCode(
|
||||
payload: AdminKuaishouIndustryAuthorizationCodePayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryExchangeCodeResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-industry-source/exchange-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export type {
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||
AdminKuaishouIndustryExchangeCodeResponse,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
|
||||
@@ -35,6 +35,8 @@ export type {
|
||||
AdminKuaishouIndustrySourceConfig,
|
||||
AdminKuaishouIndustryConfigResponse,
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||
AdminKuaishouIndustryExchangeCodeResponse,
|
||||
} from './kuaishou-industry'
|
||||
|
||||
export type {
|
||||
|
||||
@@ -10,6 +10,10 @@ export interface AdminKuaishouIndustrySourceConfig {
|
||||
sendCallbackEnabled: boolean
|
||||
baseUrl: string
|
||||
authBaseUrl: string
|
||||
redirectUri: string
|
||||
scopes: string
|
||||
authState: string
|
||||
authorizationUrl: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
appSecretMasked: string
|
||||
@@ -30,6 +34,8 @@ export interface AdminKuaishouIndustrySourceConfig {
|
||||
refreshTokenExpiresAt: string
|
||||
accessTokenStatus: AdminKuaishouIndustryAccessTokenStatus
|
||||
accessTokenExpiresInSeconds: number | null
|
||||
openId: string
|
||||
grantedScopes: string
|
||||
sellerId: string
|
||||
provider: string
|
||||
platform: string
|
||||
@@ -48,3 +54,9 @@ export interface AdminKuaishouIndustryConfigResponse {
|
||||
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
||||
refreshed: boolean
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryAuthorizationCodePayload {
|
||||
code: string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryExchangeCodeResponse = AdminKuaishouIndustryRefreshTokenResponse
|
||||
|
||||
Reference in New Issue
Block a user