diff --git a/apps/backend/src/services/admin/platform-config/notification-service.ts b/apps/backend/src/services/admin/platform-config/notification-service.ts index cf4d0e0b..ff8b6181 100644 --- a/apps/backend/src/services/admin/platform-config/notification-service.ts +++ b/apps/backend/src/services/admin/platform-config/notification-service.ts @@ -170,21 +170,63 @@ function mapScheduledJobCloudtentaclesAccounts( cloudtentaclesAccountMap: Map, ) { const accounts = Array.isArray(rawAccounts) ? rawAccounts : [] - return accounts.map((item) => { - const source = isPlainObject(item) ? item : {} - const sourceKey = String(source.sourceKey || source.key || '').trim() - const option = cloudtentaclesAccountMap.get(sourceKey) || {} + const configuredMap = new Map( + accounts + .map((item) => { + const source = isPlainObject(item) ? item : {} + const sourceKey = String(source.sourceKey || source.key || '').trim() + if (!sourceKey) { + return null + } + const option = cloudtentaclesAccountMap.get(sourceKey) || {} + return [ + sourceKey, + { + sourceKey, + label: String(source.label || option.label || sourceKey).trim(), + enabled: source.enabled !== false, + assetThreshold: normalizeNonNegativeNumber( + source.assetThreshold ?? source.threshold, + defaultAssetThreshold, + ), + }, + ] as const + }) + .filter(Boolean) as Array, + ) + + // 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。 + const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => { + const sourceKey = String(option.sourceKey || '').trim() + const configured = configuredMap.get(sourceKey) return { sourceKey, - label: String(source.label || option.label || sourceKey).trim(), - enabled: source.enabled !== false, + label: String(configured?.label || option.label || sourceKey).trim(), + enabled: configured + ? configured.enabled !== false + : option.enabled !== false, assetThreshold: normalizeNonNegativeNumber( - source.assetThreshold ?? source.threshold, + configured?.assetThreshold, defaultAssetThreshold, ), } }).filter((item) => item.sourceKey) + + const mergedKeys = new Set(merged.map((item) => item.sourceKey)) + for (const [sourceKey, configured] of configuredMap.entries()) { + if (mergedKeys.has(sourceKey)) { + continue + } + merged.push(configured) + } + + return merged } function normalizeNonNegativeNumber(value: unknown, fallback: number) { diff --git a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts index 2d79311d..3f6efbe8 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts @@ -191,23 +191,64 @@ function resolveCloudtentaclesHealthAccounts(job: JsonObject) { const rawAccounts = Array.isArray(config.accounts) ? config.accounts : [] const defaultThreshold = normalizeNonNegativeInteger(config.assetThreshold, 500) const sourceConfig = listCloudtentaclesSources() + const sources = Array.isArray(sourceConfig.sources) ? sourceConfig.sources : [] const sourceMap = new Map( - (Array.isArray(sourceConfig.sources) ? sourceConfig.sources : []) - .map((source) => [String(source.key || '').trim(), source]), + sources + .map((source) => [String(source.key || '').trim(), source] as const) + .filter(([sourceKey]) => Boolean(sourceKey)), ) - const normalizedAccounts = rawAccounts - .map((item) => normalizeCloudtentaclesHealthAccount(item, defaultThreshold, sourceMap)) + const configuredMap = new Map( + rawAccounts + .map((item) => normalizeCloudtentaclesHealthAccount(item, defaultThreshold, sourceMap)) + .filter(Boolean) + .map((item) => [String(item!.sourceKey || '').trim(), item!] as const) + .filter(([sourceKey]) => Boolean(sourceKey)), + ) + + // 以平台配置中的全部账号为准,合并任务里已保存的阈值/开关;新增账号自动纳入监控。 + const mergedAccounts = sources + .map((source) => { + const sourceKey = String(source.key || '').trim() + if (!sourceKey) { + return null + } + + const configured = configuredMap.get(sourceKey) + return { + sourceKey, + label: String( + configured?.label || source.label || source.username || sourceKey, + ).trim() || sourceKey, + enabled: configured + ? configured.enabled !== false + : source.enabled !== false, + assetThreshold: normalizeNonNegativeInteger( + configured?.assetThreshold, + defaultThreshold, + ), + } + }) .filter(Boolean) as JsonObject[] - if (normalizedAccounts.length > 0) { - return dedupeAccounts(normalizedAccounts) + // 保留已配置但源账号已删除的项,便于报 source_missing。 + for (const [sourceKey, configured] of configuredMap.entries()) { + if (sourceMap.has(sourceKey)) { + continue + } + mergedAccounts.push(configured) } + if (mergedAccounts.length > 0) { + return dedupeAccounts(mergedAccounts) + } + + // 兼容旧配置:没有任何平台账号时,仍检查历史 default。 + const fallback = configuredMap.get('default') return [{ sourceKey: 'default', - label: sourceMap.get('default')?.label || '默认账号', - enabled: true, - assetThreshold: defaultThreshold, + label: String(fallback?.label || '默认账号').trim() || '默认账号', + enabled: fallback ? fallback.enabled !== false : true, + assetThreshold: normalizeNonNegativeInteger(fallback?.assetThreshold, defaultThreshold), }] } @@ -249,7 +290,7 @@ function dedupeAccounts(accounts: JsonObject[]) { function buildCloudtentaclesHealthSummary(results: CloudtentaclesHealthAccountResult[]) { if (results.length === 0) { - return '未配置 cloudtentacles 监控账号' + return '未配置 kuaishou-lewan 监控账号' } const checkedResults = results.filter((item) => !item.skipped) diff --git a/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx b/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx index aafc630c..6e9cf09f 100644 --- a/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx +++ b/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx @@ -191,7 +191,7 @@ export default function AdminCloudtentaclesRecordsPage() { setSourceKey(nextSourceKey) return nextSourceKey } catch (error) { - setErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 账号失败') + setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 账号失败') return '' } finally { setSourceLoading(false) @@ -213,7 +213,7 @@ export default function AdminCloudtentaclesRecordsPage() { const orderKeyword = (options.platformOrderId ?? platformOrderId).trim() if (!nextSourceKey && !orderKeyword) { - showError('请先选择 cloudtentacles 账号') + showError('请先选择 kuaishou-lewan 账号') return } @@ -291,7 +291,7 @@ export default function AdminCloudtentaclesRecordsPage() {
共 {total} 条记录} /> @@ -299,7 +299,7 @@ export default function AdminCloudtentaclesRecordsPage() { title="查询条件" extra={ - 输入订单号时会先关联本地任务,再匹配 cloudtentacles 发货记录。 + 输入订单号时会先关联本地任务,再匹配 kuaishou-lewan 发货记录。 } > @@ -307,7 +307,7 @@ export default function AdminCloudtentaclesRecordsPage() { setMatchInput(event.target.value)} /> } + extra={} >
onChange({ ...job, cooldownSeconds })} /> - onChange({ ...job, config: { ...job.config, assetThreshold } }) - } + onChange={(assetThreshold) => updateDefaultThreshold(Number(assetThreshold ?? 500))} />
{runtime ? ( ) : null} + + + className="platform-section-gap" + rowKey="sourceKey" + size="small" + pagination={false} + columns={accountColumns} + dataSource={accounts} + locale={{ emptyText: '暂无 kuaishou-lewan 账号,请先在平台配置登录。' }} + /> ) } +function mergeScheduledJobAccounts( + configuredAccounts: AdminScheduledJobCloudtentaclesAccount[], + monitorAccounts: AdminCloudtentaclesMonitorAccount[], + defaultAssetThreshold: number, +) { + const configuredMap = new Map( + configuredAccounts + .map((item) => [String(item.sourceKey || '').trim(), item] as const) + .filter(([sourceKey]) => Boolean(sourceKey)), + ) + const merged = monitorAccounts + .filter((item) => String(item.sourceKey || '').trim()) + .map((item) => { + const sourceKey = String(item.sourceKey || '').trim() + const configured = configuredMap.get(sourceKey) + return { + sourceKey, + label: String(configured?.label || item.label || sourceKey).trim(), + enabled: configured ? configured.enabled !== false : item.enabled !== false, + assetThreshold: Number( + configured?.assetThreshold ?? defaultAssetThreshold, + ), + } + }) + const mergedKeys = new Set(merged.map((item) => item.sourceKey)) + + for (const configured of configuredAccounts) { + const sourceKey = String(configured.sourceKey || '').trim() + if (!sourceKey || mergedKeys.has(sourceKey)) { + continue + } + merged.push({ + sourceKey, + label: String(configured.label || sourceKey).trim(), + enabled: configured.enabled !== false, + assetThreshold: Number(configured.assetThreshold ?? defaultAssetThreshold), + }) + } + + return merged +} + +function formatScheduledJobTitle(job: AdminScheduledJobItem) { + if (job.type === 'cloudtentacles_health' || job.id === 'cloudtentacles-health') { + return 'kuaishou-lewan 健康检查' + } + return job.id || job.type || '定时任务' +} + +function formatMonitorAccountMeta( + account: AdminScheduledJobCloudtentaclesAccount, + source: AdminCloudtentaclesMonitorAccount | undefined, +) { + if (!source) { + return '账号配置不存在' + } + + return [ + source.username ? `登录名 ${source.username}` : '', + source.phoneMasked ? `手机 ${source.phoneMasked}` : '', + source.hasToken ? 'Token 已保存' : '未登录', + ] + .filter(Boolean) + .join(' · ') || account.sourceKey +} + +function getAccountRuntime( + runtime: AdminScheduledJobRuntimeState | null, + sourceKey: string, +): AdminScheduledJobAccountRuntime | null { + if (!runtime?.lastAccounts?.length) { + return null + } + return runtime.lastAccounts.find((item) => item.sourceKey === sourceKey) || null +} + +function formatHealthStatus(status: string) { + const normalized = String(status || '').trim() + if (normalized === 'ok' || normalized === 'success') return '正常' + if (normalized === 'asset_low') return '余额预警' + if (normalized === 'auth_missing') return '未登录' + if (normalized === 'source_missing') return '账号缺失' + if (normalized === 'disabled') return '已停用' + if (normalized === 'skipped') return '已跳过' + if (normalized === 'running') return '执行中' + if (normalized === 'failed') return '检查失败' + if (normalized === 'pending') return '待执行' + return normalized || '待执行' +} + +function resolveHealthStatusColor(status: string) { + const normalized = String(status || '').trim() + if (normalized === 'ok' || normalized === 'success') return 'green' + if (normalized === 'asset_low' || normalized === 'running') return 'orange' + if (['auth_missing', 'source_missing', 'failed'].includes(normalized)) return 'red' + return 'default' +} + function KuaishouIndustryPanel({ config, onChange, @@ -1529,7 +1789,14 @@ function CloudtentaclesPlatformPanel({ const [selectedSourceKey, setSelectedSourceKey] = useState(config.sources[0]?.key || '') const [smsCode, setSmsCode] = useState('') const [debugResult, setDebugResult] = useState(null) - const selectedSource = config.sources.find((source) => source.key === selectedSourceKey) || config.sources[0] || null + + useEffect(() => { + if (!config.sources.some((source) => source.key === selectedSourceKey)) { + setSelectedSourceKey(config.sources[0]?.key || '') + } + }, [config.sources, selectedSourceKey]) + + const selectedSource = config.sources.find((source) => source.key === selectedSourceKey) || null const selectedSession = selectedSource ? config.sessions[selectedSource.key] : null async function saveConfig() { @@ -1540,9 +1807,13 @@ function CloudtentaclesPlatformPanel({ sources: config.sources, }) onChange(response.data) - showSuccess('cloudtentacles 账号配置已保存') + const nextKey = selectedSourceKey && response.data.sources.some((item) => item.key === selectedSourceKey) + ? selectedSourceKey + : response.data.sources[0]?.key || '' + setSelectedSourceKey(nextKey) + showSuccess('kuaishou-lewan 账号配置已保存') } catch (error) { - showError(error instanceof Error ? error.message : '保存 cloudtentacles 账号失败') + showError(error instanceof Error ? error.message : '保存 kuaishou-lewan 账号失败') } finally { setSaving(false) } @@ -1554,24 +1825,35 @@ function CloudtentaclesPlatformPanel({ await deleteAdminCloudtentaclesSource(sourceKey) const response = await fetchAdminCloudtentaclesSourceConfig() onChange(response.data) - setSelectedSourceKey(response.data.sources[0]?.key || '') - showSuccess('cloudtentacles 账号已删除') + const nextKey = selectedSourceKey === sourceKey + ? response.data.sources[0]?.key || '' + : selectedSourceKey + setSelectedSourceKey( + response.data.sources.some((item) => item.key === nextKey) + ? nextKey + : response.data.sources[0]?.key || '', + ) + showSuccess('kuaishou-lewan 账号已删除') } catch (error) { - showError(error instanceof Error ? error.message : '删除 cloudtentacles 账号失败') + showError(error instanceof Error ? error.message : '删除 kuaishou-lewan 账号失败') } finally { setDebugLoading('') } } async function runDebug(action: string, handler: () => Promise) { + if (!selectedSource) { + showError('请先选择一个账号') + return + } setDebugLoading(action) setDebugResult(null) try { const result = await handler() setDebugResult(result) - showSuccess('cloudtentacles 调试完成') + showSuccess(`账号 ${formatCloudtentaclesSourceLabel(selectedSource)} 调试完成`) } catch (error) { - showError(error instanceof Error ? error.message : 'cloudtentacles 调试失败') + showError(error instanceof Error ? error.message : 'kuaishou-lewan 调试失败') } finally { setDebugLoading('') } @@ -1587,6 +1869,9 @@ function CloudtentaclesPlatformPanel({ source.key === sourceKey ? { ...source, ...patch } : source, ), }) + if (patch.key && patch.key !== sourceKey && selectedSourceKey === sourceKey) { + setSelectedSourceKey(String(patch.key)) + } } function addSource() { @@ -1613,7 +1898,7 @@ function CloudtentaclesPlatformPanel({ return (
{config.filePath || '默认配置'} @@ -1624,44 +1909,114 @@ function CloudtentaclesPlatformPanel({ } > {config.sources.length === 0 ? ( - + ) : ( - - {config.sources.map((source) => { - const session = config.sessions[source.key] - const active = source.key === selectedSourceKey - return ( - - updateSource(source.key, { enabled })} /> - - - {session?.hasToken ? '已登录' : '未登录'} +
+ + + {config.sources.map((source) => { + const session = config.sessions[source.key] + const active = source.key === selectedSourceKey + return ( + } - > + + ) + })} + + + + } + loading={debugLoading === `delete-${selectedSource.key}`} + onClick={() => deleteSource(selectedSource.key)} + > + 删除 + + ) : null + } + > + {!selectedSource ? ( + + ) : ( + <>
- updateSource(source.key, { key: value })} /> - updateSource(source.key, { label: value })} /> - updateSource(source.key, { baseUrl: value })} /> - updateSource(source.key, { username: value })} /> - updateSource(source.key, { password: value })} /> - updateSource(source.key, { phone: value })} /> - updateSource(source.key, { deviceId: value })} /> - updateSource(source.key, { deviceType })} /> + updateSource(selectedSource.key, { enabled })} + /> + updateSource(selectedSource.key, { key: value })} + /> + updateSource(selectedSource.key, { label: value })} + /> + updateSource(selectedSource.key, { baseUrl: value })} + /> + updateSource(selectedSource.key, { username: value })} + /> + updateSource(selectedSource.key, { password: value })} + /> + updateSource(selectedSource.key, { phone: value })} + /> + updateSource(selectedSource.key, { deviceId: value })} + /> + updateSource(selectedSource.key, { deviceType })} + />
-
- ) - })} - + + + )} + +
)}
@@ -1673,8 +2028,12 @@ function CloudtentaclesPlatformPanel({ - 查资产 + 查余额