diff --git a/apps/frontend/src/composables/admin/platform-shops/types.ts b/apps/frontend/src/composables/admin/platform-shops/types.ts
index 2f4fbf27..1121f505 100644
--- a/apps/frontend/src/composables/admin/platform-shops/types.ts
+++ b/apps/frontend/src/composables/admin/platform-shops/types.ts
@@ -30,3 +30,16 @@ export type EditableNotificationRecipient = {
deviceKey: string
enabled: boolean
}
+
+export type EditableScheduledJob = {
+ id: string
+ type: string
+ enabled: boolean
+ intervalSeconds: number
+ intervalSecondsAmount: number
+ intervalSecondsUnit: number
+ cooldownSeconds: number
+ cooldownSecondsAmount: number
+ cooldownSecondsUnit: number
+ assetThreshold: number
+}
diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminNotificationPlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminNotificationPlatform.ts
index a049aa90..b6fa566f 100644
--- a/apps/frontend/src/composables/admin/platform-shops/useAdminNotificationPlatform.ts
+++ b/apps/frontend/src/composables/admin/platform-shops/useAdminNotificationPlatform.ts
@@ -2,15 +2,20 @@ import { computed, ref } from 'vue'
import { showError, showSuccess } from '@/lib/feedback'
import {
+ runAdminScheduledJob,
saveAdminNotificationConfig,
+ saveAdminScheduledJobsConfig,
testAdminNotification,
} from '@/services/admin'
import type {
AdminNotificationConfig,
AdminNotificationTestResult,
+ AdminScheduledJobRuntimeState,
+ AdminScheduledJobsConfig,
+ AdminScheduledJobsResponse,
} from '@/types/admin'
-import type { EditableNotificationRecipient } from './types'
+import type { EditableNotificationRecipient, EditableScheduledJob } from './types'
export function useAdminNotificationPlatform() {
const notificationFilePath = ref('')
@@ -27,6 +32,14 @@ export function useAdminNotificationPlatform() {
const notificationTesting = ref(false)
const notificationResultError = ref('')
const notificationTestResult = ref
(null)
+ const scheduledJobsFilePath = ref('')
+ const scheduledJobsForm = ref({
+ enabled: true,
+ })
+ const scheduledJobs = ref([])
+ const scheduledJobRuntime = ref([])
+ const scheduledJobsSaving = ref(false)
+ const scheduledJobRunningId = ref('')
const notificationStats = computed(() => {
const enabledRecipients = notificationRecipients.value.filter((item) => item.enabled && item.deviceKey.trim())
@@ -37,6 +50,7 @@ export function useAdminNotificationPlatform() {
barkReady: notificationForm.value.enabled && notificationForm.value.barkEnabled && enabledRecipients.length > 0,
lastSuccessCount: notificationTestResult.value?.successCount || 0,
lastFailedCount: notificationTestResult.value?.failedCount || 0,
+ scheduledJobEnabledCount: scheduledJobs.value.filter((item) => item.enabled).length,
}
})
@@ -56,6 +70,24 @@ export function useAdminNotificationPlatform() {
}))
}
+ function hydrateScheduledJobsConfig(data: AdminScheduledJobsResponse) {
+ scheduledJobsFilePath.value = data.filePath
+ scheduledJobsForm.value.enabled = data.source.enabled !== false
+ scheduledJobs.value = data.source.jobs.map((item) => ({
+ id: item.id,
+ type: item.type,
+ enabled: item.enabled === true,
+ intervalSeconds: Number(item.intervalSeconds || 300),
+ intervalSecondsAmount: resolveTimeValue(Number(item.intervalSeconds || 300)).amount,
+ intervalSecondsUnit: resolveTimeValue(Number(item.intervalSeconds || 300)).unit,
+ cooldownSeconds: Number(item.cooldownSeconds || 1800),
+ cooldownSecondsAmount: resolveTimeValue(Number(item.cooldownSeconds || 1800)).amount,
+ cooldownSecondsUnit: resolveTimeValue(Number(item.cooldownSeconds || 1800)).unit,
+ assetThreshold: Number(item.config?.assetThreshold || 500),
+ }))
+ scheduledJobRuntime.value = data.runtime || []
+ }
+
function addNotificationRecipient() {
notificationRecipients.value.unshift({
id: crypto.randomUUID(),
@@ -88,6 +120,22 @@ export function useAdminNotificationPlatform() {
}
}
+ function buildScheduledJobsConfigPayload(): AdminScheduledJobsConfig {
+ return {
+ enabled: scheduledJobsForm.value.enabled,
+ jobs: scheduledJobs.value.map((item) => ({
+ id: item.id,
+ type: item.type,
+ enabled: item.enabled,
+ intervalSeconds: Number(item.intervalSeconds || 300),
+ cooldownSeconds: Number(item.cooldownSeconds || 1800),
+ config: {
+ assetThreshold: Number(item.assetThreshold || 500),
+ },
+ })),
+ }
+ }
+
async function handleNotificationSaveConfig() {
notificationSaving.value = true
notificationResultError.value = ''
@@ -134,6 +182,58 @@ export function useAdminNotificationPlatform() {
}
}
+ async function handleScheduledJobsSaveConfig() {
+ scheduledJobsSaving.value = true
+ notificationResultError.value = ''
+
+ try {
+ const response = await saveAdminScheduledJobsConfig(buildScheduledJobsConfigPayload())
+ hydrateScheduledJobsConfig(response.data)
+ showSuccess('监控任务配置已保存')
+ } catch (error) {
+ notificationResultError.value = error instanceof Error ? error.message : '监控任务配置保存失败'
+ showError(notificationResultError.value)
+ } finally {
+ scheduledJobsSaving.value = false
+ }
+ }
+
+ async function handleScheduledJobRunNow(jobId: string) {
+ scheduledJobRunningId.value = jobId
+ notificationResultError.value = ''
+
+ try {
+ const saved = await saveAdminScheduledJobsConfig(buildScheduledJobsConfigPayload())
+ hydrateScheduledJobsConfig(saved.data)
+ const response = await runAdminScheduledJob(jobId)
+ scheduledJobRuntime.value = response.data.runtime || []
+ const message = String(response.data.result?.message || '监控任务已执行')
+ showSuccess(message)
+ } catch (error) {
+ notificationResultError.value = error instanceof Error ? error.message : '监控任务执行失败'
+ showError(notificationResultError.value)
+ } finally {
+ scheduledJobRunningId.value = ''
+ }
+ }
+
+ function getScheduledJobRuntime(jobId: string) {
+ return scheduledJobRuntime.value.find((item) => item.id === jobId) || null
+ }
+
+ function resolveTimeValue(seconds: number) {
+ const normalized = Math.max(0, Number(seconds || 0))
+ if (normalized >= 3600 && normalized % 3600 === 0) {
+ return { amount: normalized / 3600, unit: 3600 }
+ }
+
+ if (normalized >= 60 && normalized % 60 === 0) {
+ return { amount: normalized / 60, unit: 60 }
+ }
+
+ return { amount: normalized || 1, unit: 1 }
+ }
+
return {
notificationFilePath,
notificationForm,
@@ -142,11 +242,21 @@ export function useAdminNotificationPlatform() {
notificationTesting,
notificationResultError,
notificationTestResult,
+ scheduledJobsFilePath,
+ scheduledJobsForm,
+ scheduledJobs,
+ scheduledJobRuntime,
+ scheduledJobsSaving,
+ scheduledJobRunningId,
notificationStats,
hydrateNotificationConfig,
+ hydrateScheduledJobsConfig,
addNotificationRecipient,
removeNotificationRecipient,
handleNotificationSaveConfig,
handleNotificationTest,
+ handleScheduledJobsSaveConfig,
+ handleScheduledJobRunNow,
+ getScheduledJobRuntime,
}
}
diff --git a/apps/frontend/src/services/admin/platform-config.ts b/apps/frontend/src/services/admin/platform-config.ts
index fef5c2c9..bdbf0881 100644
--- a/apps/frontend/src/services/admin/platform-config.ts
+++ b/apps/frontend/src/services/admin/platform-config.ts
@@ -15,6 +15,8 @@ import type {
AdminNinetyoneOrderListResult,
AdminNotificationConfig,
AdminNotificationTestResult,
+ AdminScheduledJobsConfig,
+ AdminScheduledJobsResponse,
AdminKuaishouCloudFulfillmentConfig,
AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult,
@@ -65,6 +67,24 @@ export function testAdminNotification(payload: {
return apiPost('/api/v1/admin/platform-config/notifications/test', payload)
}
+export function fetchAdminScheduledJobsConfig() {
+ return apiGet('/api/v1/admin/platform-config/scheduled-jobs')
+}
+
+export function saveAdminScheduledJobsConfig(payload: AdminScheduledJobsConfig) {
+ return apiPost(
+ '/api/v1/admin/platform-config/scheduled-jobs',
+ payload as unknown as Record,
+ )
+}
+
+export function runAdminScheduledJob(jobId: string) {
+ return apiPost<{
+ result: Record
+ runtime: AdminScheduledJobsResponse['runtime']
+ }>(`/api/v1/admin/platform-config/scheduled-jobs/${jobId}/run`, {})
+}
+
export function fetchAdminNinetyoneOrders(params: {
page?: number
pageSize?: number
diff --git a/apps/frontend/src/styles/admin-platform-shops.css b/apps/frontend/src/styles/admin-platform-shops.css
index 6bdfc8c5..7b33714d 100644
--- a/apps/frontend/src/styles/admin-platform-shops.css
+++ b/apps/frontend/src/styles/admin-platform-shops.css
@@ -505,6 +505,17 @@
font-size: 13px;
}
+.admin-platform-shops .time-input-row {
+ display: grid;
+ grid-template-columns: minmax(96px, 1fr) 96px;
+ gap: 8px;
+ align-items: center;
+}
+
+.admin-platform-shops .time-unit-select {
+ min-width: 96px;
+}
+
.admin-platform-shops .empty-block,
.admin-platform-shops .empty-inline {
color: #64748b;
diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts
index e8dbf29c..d1f6a307 100644
--- a/apps/frontend/src/types/admin.ts
+++ b/apps/frontend/src/types/admin.ts
@@ -202,6 +202,42 @@ export interface AdminNotificationTestResult {
results: AdminNotificationSendResult[]
}
+export interface AdminScheduledJobItem {
+ id: string
+ type: string
+ enabled: boolean
+ intervalSeconds: number
+ cooldownSeconds: number
+ config: {
+ assetThreshold: number
+ }
+}
+
+export interface AdminScheduledJobsConfig {
+ enabled: boolean
+ jobs: AdminScheduledJobItem[]
+}
+
+export interface AdminScheduledJobRuntimeState {
+ id: string
+ enabled: boolean
+ running: boolean
+ lastRunAt: string
+ lastFinishedAt: string
+ nextRunAt: string
+ lastStatus: string
+ lastMessage: string
+ lastAsset: number | null
+ lastThreshold: number | null
+ lastManual: boolean
+}
+
+export interface AdminScheduledJobsResponse {
+ filePath: string
+ source: AdminScheduledJobsConfig
+ runtime: AdminScheduledJobRuntimeState[]
+}
+
export interface AdminNinetyoneOrderItem {
orderId: number
orderNo: string
diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue
index f7c991e0..0e5d7a54 100644
--- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue
+++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue
@@ -17,6 +17,7 @@ import {
fetchAdminCloudtentaclesSourceConfig,
fetchAdminKuaishouEticketSourceConfig,
fetchAdminNotificationConfig,
+ fetchAdminScheduledJobsConfig,
} from '@/services/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import '@/styles/admin-platform-shops.css'
@@ -66,12 +67,22 @@ const {
notificationTesting,
notificationResultError,
notificationTestResult,
+ scheduledJobsFilePath,
+ scheduledJobsForm,
+ scheduledJobs,
+ scheduledJobRuntime,
+ scheduledJobsSaving,
+ scheduledJobRunningId,
notificationStats,
hydrateNotificationConfig,
+ hydrateScheduledJobsConfig,
addNotificationRecipient,
removeNotificationRecipient,
handleNotificationSaveConfig,
handleNotificationTest,
+ handleScheduledJobsSaveConfig,
+ handleScheduledJobRunNow,
+ getScheduledJobRuntime,
} = useAdminNotificationPlatform()
const {
@@ -151,15 +162,23 @@ async function loadConfigs() {
errorMessage.value = ''
try {
- const [agisoResponse, notificationResponse, kuaishouEticketResponse, cloudtentaclesResponse] = await Promise.all([
+ const [
+ agisoResponse,
+ notificationResponse,
+ scheduledJobsResponse,
+ kuaishouEticketResponse,
+ cloudtentaclesResponse,
+ ] = await Promise.all([
fetchAdminAgisoShopConfigs(),
fetchAdminNotificationConfig(),
+ fetchAdminScheduledJobsConfig(),
fetchAdminKuaishouEticketSourceConfig(),
fetchAdminCloudtentaclesSourceConfig(),
])
hydrateAgisoConfig(agisoResponse.data)
hydrateNotificationConfig(notificationResponse.data)
+ hydrateScheduledJobsConfig(scheduledJobsResponse.data)
hydrateKuaishouEticketConfig(kuaishouEticketResponse.data)
hydrateCloudtentaclesConfig(cloudtentaclesResponse.data)
await loadNinetyoneOrders(1)
@@ -368,11 +387,20 @@ onMounted(loadConfigs)
:notification-testing="notificationTesting"
:notification-result-error="notificationResultError"
:notification-test-result="notificationTestResult"
+ :scheduled-jobs-file-path="scheduledJobsFilePath"
+ :scheduled-jobs-form="scheduledJobsForm"
+ :scheduled-jobs="scheduledJobs"
+ :scheduled-job-runtime="scheduledJobRuntime"
+ :scheduled-jobs-saving="scheduledJobsSaving"
+ :scheduled-job-running-id="scheduledJobRunningId"
:notification-stats="notificationStats"
:add-notification-recipient="addNotificationRecipient"
:remove-notification-recipient="removeNotificationRecipient"
:handle-notification-save-config="handleNotificationSaveConfig"
:handle-notification-test="handleNotificationTest"
+ :handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
+ :handle-scheduled-job-run-now="handleScheduledJobRunNow"
+ :get-scheduled-job-runtime="getScheduledJobRuntime"
/>