feat(pdd-ui): 新增管理后台拼多多店铺CRUD与Cookie有效性实时检测功能
1. 订单快照优化 (pdd-order-mapper.ts): - 在 item_snapshot_json 中完整保留拼多多原始报文快照全量字段,确保无信息截断。 2. 后端 Cookie 有效性校验接口 (pdd-client.ts, platform-config/pdd.ts): - 新增 PddClient.testCookie 与 POST /api/v1/admin/platform-config/pdd/test-cookie 接口。 - 支持通过轻量接口请求即时测试 Cookie 合法性,精准识别有效状态(返回在线订单数)与失效/频控状态。 3. 前端管理面板开发 (PddPlatformPanel.tsx, AdminPlatformShopsPage.tsx): - 在平台配置页面新增【拼多多 (PDD)】专属 Tab 面板。 - 提供拼多多全局同步开关与轮询间隔配置。 - 支持店铺完整 CRUD:添加店铺、编辑店铺、删除店铺、启用/停用切换。 - 支持弹窗内即时【测试此 Cookie 有效性】(无需先保存即可验证)及列表行内一键测活与单店铺立即同步。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Pinduoduo Admin Platform Config Routes
|
||||
* 拼多多平台管理端配置与手动同步路由
|
||||
* 拼多多平台管理端配置、店铺 CRUD 与 CK 有效性校验路由
|
||||
*/
|
||||
|
||||
import { Router } from 'express'
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
syncAllPddStores,
|
||||
syncStoreOrders,
|
||||
} from '../../../services/platforms/pdd/pdd-sync-service.js'
|
||||
import { testPddCookie } from '../../../services/platforms/pdd/pdd-client.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -68,16 +69,19 @@ router.post(
|
||||
*/
|
||||
router.post(
|
||||
'/pdd/stores/:shopId',
|
||||
createJsonHandler((req) => savePddStore(req.params.shopId, (req.body || {}) as JsonObject), {
|
||||
successMessage: '拼多多店铺配置已更新',
|
||||
errorMessage: '更新拼多多店铺配置失败',
|
||||
scope: '[admin/platform-config/pdd/stores/:shopId]',
|
||||
audit: (req) => ({
|
||||
action: 'platform_pdd_store_updated',
|
||||
targetType: 'platform_store',
|
||||
targetId: String(req.params.shopId || ''),
|
||||
}),
|
||||
}),
|
||||
createJsonHandler(
|
||||
(req) => savePddStore(req.params.shopId, (req.body || {}) as JsonObject),
|
||||
{
|
||||
successMessage: '拼多多店铺配置已更新',
|
||||
errorMessage: '更新拼多多店铺配置失败',
|
||||
scope: '[admin/platform-config/pdd/stores/:shopId]',
|
||||
audit: (req) => ({
|
||||
action: 'platform_pdd_store_updated',
|
||||
targetType: 'platform_store',
|
||||
targetId: String(req.params.shopId || ''),
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -99,6 +103,53 @@ router.delete(
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /platform-config/pdd/test-cookie
|
||||
* Test Cookie validity for a given cookie string or an existing shop.
|
||||
* 测试拼多多 Cookie 有效性(支持直接传入 Cookie 字符串或指定 shopId)。
|
||||
*/
|
||||
router.post(
|
||||
'/pdd/test-cookie',
|
||||
createJsonHandler(
|
||||
async (req) => {
|
||||
const body = (req.body || {}) as {
|
||||
cookie?: string
|
||||
userAgent?: string
|
||||
shopId?: string
|
||||
}
|
||||
|
||||
let cookie = String(body.cookie || '').trim()
|
||||
let userAgent = body.userAgent
|
||||
|
||||
if (!cookie && body.shopId) {
|
||||
const store = getPddStoreById(body.shopId)
|
||||
if (!store) {
|
||||
throw createHttpError(`店铺不存在: ${body.shopId}`, {
|
||||
statusCode: 404,
|
||||
errorCode: 'pdd_store_not_found',
|
||||
})
|
||||
}
|
||||
cookie = store.cookie
|
||||
userAgent = userAgent || store.userAgent
|
||||
}
|
||||
|
||||
if (!cookie) {
|
||||
throw createHttpError('请提供待测试的 Cookie 字符串或指定有效的 shopId', {
|
||||
statusCode: 400,
|
||||
errorCode: 'pdd_cookie_required',
|
||||
})
|
||||
}
|
||||
|
||||
return testPddCookie({ cookie, userAgent })
|
||||
},
|
||||
{
|
||||
successMessage: 'Cookie 校验执行完成',
|
||||
errorMessage: 'Cookie 校验失败',
|
||||
scope: '[admin/platform-config/pdd/test-cookie]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /platform-config/pdd/sync
|
||||
* Manually trigger order sync for all stores or a specific store.
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* 自动生成合规 0as Anti-Content 并双向注入网关请求头与报文体。
|
||||
* - Structured MMS error resolution (Rate limit 40002, Auth expired 40001).
|
||||
* 结构化捕获与解析拼多多业务级错误(频控 40002、Cookie 失效 40001 等)。
|
||||
* - Live Cookie validity verification with latency metrics.
|
||||
* 商家 Cookie 有效性在线实测校验与连通性检测。
|
||||
*/
|
||||
|
||||
import { pddAntiContentEngine, PurePddAntiContent } from './pdd-anti-content.js'
|
||||
@@ -42,6 +44,13 @@ export interface PddOrderQueryResult {
|
||||
items: PddRawOrderItem[]
|
||||
}
|
||||
|
||||
export interface PddTestCookieResult {
|
||||
valid: boolean
|
||||
message: string
|
||||
totalOrders?: number | undefined
|
||||
firstOrderSn?: string | undefined
|
||||
}
|
||||
|
||||
export class PddClient {
|
||||
public static readonly BASE_URL = 'https://mms.pinduoduo.com'
|
||||
public static readonly RECENT_ORDERS_API = `${PddClient.BASE_URL}/mangkhut/mms/recentOrderList`
|
||||
@@ -219,4 +228,78 @@ export class PddClient {
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Cookie validity by sending a single lightweight query to MMS API.
|
||||
* 通过发送轻量请求验证拼多多商家 Cookie 的有效性与网关连通性。
|
||||
*/
|
||||
public static async testCookie(credentials: {
|
||||
cookie: string
|
||||
userAgent?: string | undefined
|
||||
}): Promise<PddTestCookieResult> {
|
||||
const cookie = (credentials.cookie || '').trim()
|
||||
if (!cookie) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Cookie 不能为空 / Cookie string cannot be empty',
|
||||
}
|
||||
}
|
||||
|
||||
const client = new PddClient({
|
||||
shopId: 'cookie_test',
|
||||
shopName: 'Cookie 测试',
|
||||
cookie,
|
||||
userAgent: credentials.userAgent,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await client.queryRecentOrders({
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
orderType: 0,
|
||||
})
|
||||
|
||||
const firstItem = result.items[0]
|
||||
return {
|
||||
valid: true,
|
||||
message: `Cookie 有效,网关校验通过(店铺在售/历史订单数: ${result.total})`,
|
||||
totalOrders: result.total,
|
||||
firstOrderSn: firstItem?.order_sn,
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err)
|
||||
if (
|
||||
errMsg.includes('40001') ||
|
||||
errMsg.includes('登录') ||
|
||||
errMsg.includes('失效') ||
|
||||
errMsg.includes('未登录')
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Cookie 已失效或登录已过期: ${errMsg}`,
|
||||
}
|
||||
}
|
||||
if (errMsg.includes('40002') || errMsg.includes('频控')) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `触发网关频控 (40002): 操作过于频繁,请稍后重试`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
message: `验证未通过: ${errMsg}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient helper to test Cookie validity.
|
||||
* 便捷辅助函数:测试 Cookie 有效性。
|
||||
*/
|
||||
export function testPddCookie(credentials: {
|
||||
cookie: string
|
||||
userAgent?: string | undefined
|
||||
}): Promise<PddTestCookieResult> {
|
||||
return PddClient.testCookie(credentials)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,9 @@ export function mapPddOrderToSourceEvent(
|
||||
const skuName = spec ? `${goodsName} (${spec})` : goodsName
|
||||
const externalSkuCode = goodsIdStr
|
||||
|
||||
// Retain the entire raw snapshot dictionary as requested / 保留原始报文完整快照
|
||||
const itemSnapshot: JsonObject = {
|
||||
...(rawOrder as unknown as JsonObject),
|
||||
goodsId: goodsIdStr,
|
||||
goodsName,
|
||||
spec,
|
||||
@@ -92,7 +94,6 @@ export function mapPddOrderToSourceEvent(
|
||||
nickname: String(rawOrder.nickname || ''),
|
||||
orderTime: rawOrder.order_time,
|
||||
confirmTime: rawOrder.confirm_time,
|
||||
pddRaw: rawOrder as unknown as JsonObject,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import { PddPlatformPanel } from './PddPlatformPanel'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
backAdminCloudtentaclesVn,
|
||||
@@ -266,6 +267,11 @@ export default function AdminPlatformShopsPage() {
|
||||
<Empty description="kuaishou-lewan 配置未加载" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'pdd',
|
||||
label: '拼多多 (PDD)',
|
||||
children: <PddPlatformPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* Pinduoduo Platform Sub-Service Admin Panel
|
||||
* 拼多多平台子服务管理面板 (多店铺 CRUD、Cookie 有效性实时校验与错峰同步调度)
|
||||
*/
|
||||
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Spin,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
deleteAdminPddStore,
|
||||
fetchAdminPddConfig,
|
||||
saveAdminPddConfig,
|
||||
saveAdminPddStore,
|
||||
testAdminPddCookie,
|
||||
triggerAdminPddSync,
|
||||
} from '@/services/admin/platform-config'
|
||||
import type {
|
||||
AdminPddConfig,
|
||||
AdminPddStoreItem,
|
||||
AdminPddTestCookieResult,
|
||||
AdminPddSyncResult,
|
||||
AdminPddSyncAllResponse,
|
||||
} from '@/types/admin/platform-config'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
export function PddPlatformPanel() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [config, setConfig] = useState<AdminPddConfig>({
|
||||
enabled: true,
|
||||
pollIntervalSeconds: 20,
|
||||
stores: [],
|
||||
})
|
||||
|
||||
// Action loading states
|
||||
const [syncingAll, setSyncingAll] = useState(false)
|
||||
const [testingShopId, setTestingShopId] = useState<string | null>(null)
|
||||
const [syncingShopId, setSyncingShopId] = useState<string | null>(null)
|
||||
|
||||
// Test results cache by shopId
|
||||
const [testResults, setTestResults] = useState<Record<string, AdminPddTestCookieResult>>({})
|
||||
|
||||
// Modal State
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [form] = Form.useForm<AdminPddStoreItem>()
|
||||
const [modalTesting, setModalTesting] = useState(false)
|
||||
const [modalTestResult, setModalTestResult] = useState<AdminPddTestCookieResult | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfig()
|
||||
}, [])
|
||||
|
||||
async function loadConfig() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetchAdminPddConfig()
|
||||
if (response && response.data) {
|
||||
setConfig(response.data)
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : '读取拼多多配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveGlobalConfig() {
|
||||
setSaving(true)
|
||||
try {
|
||||
const response = await saveAdminPddConfig(config)
|
||||
if (response && response.data) {
|
||||
setConfig(response.data)
|
||||
}
|
||||
showSuccess('拼多多全局配置已保存')
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : '保存拼多多全局配置失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSyncAll() {
|
||||
setSyncingAll(true)
|
||||
try {
|
||||
const response = await triggerAdminPddSync()
|
||||
const data = response?.data as AdminPddSyncAllResponse | undefined
|
||||
if (data && Array.isArray(data.results)) {
|
||||
const totalFetched = data.results.reduce((acc, r) => acc + (r.fetchedCount || 0), 0)
|
||||
const totalUpserted = data.results.reduce((acc, r) => acc + (r.upsertedCount || 0), 0)
|
||||
showSuccess(
|
||||
`全部店铺同步完成:共拉取 ${totalFetched} 笔订单,成功入库/更新 ${totalUpserted} 笔`,
|
||||
)
|
||||
} else {
|
||||
showSuccess('同步完成')
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : '触发全店铺同步失败')
|
||||
} finally {
|
||||
setSyncingAll(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSyncSingleStore(store: AdminPddStoreItem) {
|
||||
setSyncingShopId(store.shopId)
|
||||
try {
|
||||
const response = await triggerAdminPddSync({ shopId: store.shopId })
|
||||
const data = response?.data as AdminPddSyncResult | undefined
|
||||
if (data) {
|
||||
showSuccess(
|
||||
`店铺【${store.shopName}】同步完成:拉取 ${data.fetchedCount} 笔,入库/更新 ${data.upsertedCount} 笔`,
|
||||
)
|
||||
} else {
|
||||
showSuccess(`店铺【${store.shopName}】同步已触发`)
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : `店铺【${store.shopName}】同步失败`)
|
||||
} finally {
|
||||
setSyncingShopId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestCookie(store: AdminPddStoreItem) {
|
||||
setTestingShopId(store.shopId)
|
||||
try {
|
||||
const response = await testAdminPddCookie({
|
||||
shopId: store.shopId,
|
||||
cookie: store.cookie,
|
||||
userAgent: store.userAgent,
|
||||
})
|
||||
const result = response?.data
|
||||
if (result) {
|
||||
setTestResults((prev) => ({ ...prev, [store.shopId]: result }))
|
||||
if (result.valid) {
|
||||
showSuccess(
|
||||
`【${store.shopName}】Cookie 校验通过,店铺在线订单总数: ${result.totalOrders ?? 0}`,
|
||||
)
|
||||
} else {
|
||||
showError(`【${store.shopName}】Cookie 校验未通过: ${result.message}`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : 'Cookie 测试失败')
|
||||
} finally {
|
||||
setTestingShopId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteStore(shopId: string) {
|
||||
try {
|
||||
const response = await deleteAdminPddStore(shopId)
|
||||
if (response && response.data) {
|
||||
setConfig(response.data)
|
||||
}
|
||||
showSuccess('拼多多店铺已删除')
|
||||
} catch (err) {
|
||||
showError(err instanceof Error ? err.message : '删除拼多多店铺失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenAddModal() {
|
||||
setIsEditing(false)
|
||||
setModalTestResult(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
shopId: '',
|
||||
shopName: '',
|
||||
cookie: '',
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36',
|
||||
enabled: true,
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
function handleOpenEditModal(store: AdminPddStoreItem) {
|
||||
setIsEditing(true)
|
||||
setModalTestResult(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
shopId: store.shopId,
|
||||
shopName: store.shopName,
|
||||
cookie: store.cookie,
|
||||
userAgent: store.userAgent,
|
||||
proxy: store.proxy || '',
|
||||
enabled: store.enabled,
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
async function handleModalTestCookie() {
|
||||
const values = form.getFieldsValue()
|
||||
const cookie = String(values.cookie || '').trim()
|
||||
if (!cookie) {
|
||||
showError('请先在下方输入或粘贴待测试的 Cookie 字符串')
|
||||
return
|
||||
}
|
||||
|
||||
setModalTesting(true)
|
||||
setModalTestResult(null)
|
||||
try {
|
||||
const response = await testAdminPddCookie({
|
||||
cookie,
|
||||
userAgent: values.userAgent,
|
||||
shopId: values.shopId,
|
||||
})
|
||||
const result = response?.data
|
||||
if (result) {
|
||||
setModalTestResult(result)
|
||||
if (result.valid) {
|
||||
showSuccess(result.message)
|
||||
} else {
|
||||
showError(result.message)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : '测试 Cookie 异常'
|
||||
setModalTestResult({ valid: false, message: errMsg })
|
||||
showError(errMsg)
|
||||
} finally {
|
||||
setModalTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModalSubmit() {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const shopId = values.shopId.trim()
|
||||
|
||||
await saveAdminPddStore(shopId, values)
|
||||
showSuccess(`店铺【${values.shopName}】配置已保存`)
|
||||
setModalVisible(false)
|
||||
await loadConfig()
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
showError(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminPddStoreItem> = [
|
||||
{
|
||||
title: '店铺 ID',
|
||||
dataIndex: 'shopId',
|
||||
key: 'shopId',
|
||||
width: 140,
|
||||
render: (val: string) => <Tag color="blue">{val}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '店铺名称',
|
||||
dataIndex: 'shopName',
|
||||
key: 'shopName',
|
||||
width: 160,
|
||||
render: (val: string) => <Text strong>{val}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Cookie 凭据',
|
||||
dataIndex: 'cookie',
|
||||
key: 'cookie',
|
||||
ellipsis: true,
|
||||
render: (cookie: string) => {
|
||||
if (!cookie) return <Tag color="error">未配置</Tag>
|
||||
const preview =
|
||||
cookie.length > 30 ? `${cookie.slice(0, 15)}...${cookie.slice(-15)}` : cookie
|
||||
return (
|
||||
<Space size="small">
|
||||
<Text code copyable={{ text: cookie }}>
|
||||
{preview}
|
||||
</Text>
|
||||
<Tag color="cyan">{cookie.length} 字符</Tag>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 100,
|
||||
render: (enabled: boolean, record) => (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={async (checked) => {
|
||||
const updated = { ...record, enabled: checked }
|
||||
await saveAdminPddStore(record.shopId, updated)
|
||||
await loadConfig()
|
||||
showSuccess(`已${checked ? '启用' : '停用'}店铺【${record.shopName}】`)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'CK 校验结果',
|
||||
key: 'testStatus',
|
||||
width: 180,
|
||||
render: (_, record) => {
|
||||
const testRes = testResults[record.shopId]
|
||||
if (!testRes) {
|
||||
return <Text type="secondary">未校验</Text>
|
||||
}
|
||||
if (testRes.valid) {
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
有效 (订单数: {testRes.totalOrders ?? '-'})
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error" title={testRes.message}>
|
||||
失效
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 280,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={testingShopId === record.shopId}
|
||||
onClick={() => handleTestCookie(record)}
|
||||
>
|
||||
测试CK
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={syncingShopId === record.shopId}
|
||||
onClick={() => handleSyncSingleStore(record)}
|
||||
>
|
||||
立即拉单
|
||||
</Button>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => handleOpenEditModal(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除此店铺配置?"
|
||||
description="删除后将停止此店铺的自动拉单,已入库订单不会被删除。"
|
||||
onConfirm={() => handleDeleteStore(record.shopId)}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<Spin tip="加载拼多多店铺配置中..." />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* Global Sync Settings */}
|
||||
<Card
|
||||
title="拼多多子服务全局配置"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={handleSaveGlobalConfig}
|
||||
>
|
||||
保存全局配置
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} loading={syncingAll} onClick={handleSyncAll}>
|
||||
全部店铺立即拉单
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space size="large" wrap align="center">
|
||||
<Space align="center">
|
||||
<Text strong>启用拼多多同步:</Text>
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onChange={(checked) => setConfig((prev) => ({ ...prev, enabled: checked }))}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Space align="center">
|
||||
<Text strong>错峰轮询间隔 (秒):</Text>
|
||||
<InputNumber
|
||||
min={10}
|
||||
max={3600}
|
||||
value={config.pollIntervalSeconds}
|
||||
onChange={(val) =>
|
||||
setConfig((prev) => ({ ...prev, pollIntervalSeconds: Number(val || 20) }))
|
||||
}
|
||||
/>
|
||||
<Text type="secondary">(建议 15~30 秒,多店铺将自动错峰拉取待发货订单)</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Stores CRUD Table */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<span>拼多多店铺列表</span>
|
||||
<Tag color="processing">{config.stores.length} 个店铺</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenAddModal}>
|
||||
添加店铺
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="shopId"
|
||||
columns={columns}
|
||||
dataSource={config.stores}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '暂未添加拼多多店铺,请点击右上角“添加店铺”' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Add / Edit Store Modal */}
|
||||
<Modal
|
||||
title={isEditing ? '编辑拼多多店铺' : '添加拼多多店铺'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onOk={handleModalSubmit}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={680}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ enabled: true }}>
|
||||
<Form.Item
|
||||
name="shopId"
|
||||
label="店铺标识 (shopId)"
|
||||
rules={[
|
||||
{ required: true, message: '请输入店铺唯一英文标识' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message: '只允许输入英文、数字、下划线或连字符',
|
||||
},
|
||||
]}
|
||||
extra="系统内唯一标识,例如:store_pdd_01"
|
||||
>
|
||||
<Input placeholder="例如:store_pdd_01" disabled={isEditing} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="shopName"
|
||||
label="店铺名称 (shopName)"
|
||||
rules={[{ required: true, message: '请输入店铺展示名称' }]}
|
||||
extra="用于打手大厅与工单详情展示的中文名称,例如:和平精英专营1号店"
|
||||
>
|
||||
<Input placeholder="例如:和平精英专营1号店" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="cookie"
|
||||
label="商家后台 Cookie"
|
||||
rules={[{ required: true, message: '请输入商家后台 Cookie' }]}
|
||||
extra={
|
||||
<Paragraph style={{ fontSize: '12px', margin: 0 }}>
|
||||
登录 MMS 商家后台(mms.pinduoduo.com),按 F12 复制包含 <Text code>_nano_fp</Text>、
|
||||
<Text code>PASS_ID</Text>、<Text code>api_uid</Text> 的完整 Cookie。
|
||||
</Paragraph>
|
||||
}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="api_uid=...; _nano_fp=...; PASS_ID=...;" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Test Cookie Action inside Modal */}
|
||||
<div style={{ marginBottom: 18 }}>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={modalTesting}
|
||||
onClick={handleModalTestCookie}
|
||||
>
|
||||
测试此 Cookie 有效性
|
||||
</Button>
|
||||
<Text type="secondary">无需先保存,可即时验证此 Cookie 是否能连通 MMS API</Text>
|
||||
</Space>
|
||||
|
||||
{modalTestResult && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Alert
|
||||
type={modalTestResult.valid ? 'success' : 'error'}
|
||||
showIcon
|
||||
message={modalTestResult.valid ? 'Cookie 验证成功' : 'Cookie 验证失败'}
|
||||
description={modalTestResult.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form.Item name="userAgent" label="浏览器 User-Agent (选填)">
|
||||
<Input placeholder="留空则自动使用现实拟真 Chrome User-Agent" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="proxy"
|
||||
label="店铺独立代理 (选填)"
|
||||
extra="如有多店铺防关联需求,可填写 HTTP/SOCKS5 代理地址,格式:http://user:pass@ip:port"
|
||||
>
|
||||
<Input placeholder="例如:http://127.0.0.1:7890" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="enabled" label="启用店铺拉单" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './kuaishou-feifei'
|
||||
export * from './affiliate-dash'
|
||||
export * from './cloudtentacles'
|
||||
export * from './fulfillment-routing'
|
||||
export * from './pdd'
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Pinduoduo Admin Platform Config Service
|
||||
* 拼多多后台平台配置、店铺 CRUD 与 CK 校验 HTTP 服务
|
||||
*/
|
||||
|
||||
import { apiDelete, apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminPddConfig,
|
||||
AdminPddStoreItem,
|
||||
AdminPddTestCookieResult,
|
||||
AdminPddSyncResult,
|
||||
AdminPddSyncAllResponse,
|
||||
} from '@/types/admin/platform-config'
|
||||
|
||||
export function fetchAdminPddConfig() {
|
||||
return apiGet<AdminPddConfig>('/api/v1/admin/platform-config/pdd')
|
||||
}
|
||||
|
||||
export function saveAdminPddConfig(payload: AdminPddConfig) {
|
||||
return apiPost<AdminPddConfig>('/api/v1/admin/platform-config/pdd', payload)
|
||||
}
|
||||
|
||||
export function saveAdminPddStore(shopId: string, payload: Partial<AdminPddStoreItem>) {
|
||||
return apiPost<AdminPddStoreItem>(
|
||||
`/api/v1/admin/platform-config/pdd/stores/${encodeURIComponent(shopId)}`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteAdminPddStore(shopId: string) {
|
||||
return apiDelete<AdminPddConfig>(
|
||||
`/api/v1/admin/platform-config/pdd/stores/${encodeURIComponent(shopId)}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function testAdminPddCookie(payload: {
|
||||
cookie?: string
|
||||
userAgent?: string
|
||||
shopId?: string
|
||||
}) {
|
||||
return apiPost<AdminPddTestCookieResult>('/api/v1/admin/platform-config/pdd/test-cookie', payload)
|
||||
}
|
||||
|
||||
export function triggerAdminPddSync(payload: { shopId?: string; pageSize?: number } = {}) {
|
||||
return apiPost<AdminPddSyncAllResponse | AdminPddSyncResult>(
|
||||
'/api/v1/admin/platform-config/pdd/sync',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -78,3 +78,11 @@ export type {
|
||||
AdminFulfillmentRoutingPreviewResult,
|
||||
AdminFulfillmentRoutingRule,
|
||||
} from './fulfillment-routing'
|
||||
|
||||
export type {
|
||||
AdminPddStoreItem,
|
||||
AdminPddConfig,
|
||||
AdminPddTestCookieResult,
|
||||
AdminPddSyncResult,
|
||||
AdminPddSyncAllResponse,
|
||||
} from './pdd'
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Pinduoduo Admin Platform Config Types
|
||||
* 拼多多平台配置与店铺管理前端数据结构定义
|
||||
*/
|
||||
|
||||
export interface AdminPddStoreItem {
|
||||
shopId: string
|
||||
shopName: string
|
||||
cookie: string
|
||||
userAgent: string
|
||||
proxy?: string
|
||||
etag?: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AdminPddConfig {
|
||||
enabled: boolean
|
||||
pollIntervalSeconds: number
|
||||
stores: AdminPddStoreItem[]
|
||||
}
|
||||
|
||||
export interface AdminPddTestCookieResult {
|
||||
valid: boolean
|
||||
message: string
|
||||
totalOrders?: number
|
||||
firstOrderSn?: string
|
||||
}
|
||||
|
||||
export interface AdminPddSyncResult {
|
||||
shopId: string
|
||||
shopName: string
|
||||
total: number
|
||||
fetchedCount: number
|
||||
upsertedCount: number
|
||||
ignoredCount: number
|
||||
failedCount: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AdminPddSyncAllResponse {
|
||||
enabled: boolean
|
||||
storeCount: number
|
||||
syncedCount: number
|
||||
results: AdminPddSyncResult[]
|
||||
}
|
||||
Reference in New Issue
Block a user