mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: Provider 签到任务时间可配置
- 前端添加签到时间选择器(HH:MM 格式) - 后端支持动态更新调度器执行时间 - 新增 reschedule_cron_job 方法支持运行时重调度
This commit is contained in:
@@ -487,8 +487,17 @@
|
|||||||
title="定时任务"
|
title="定时任务"
|
||||||
description="配置系统后台定时任务"
|
description="配置系统后台定时任务"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-6">
|
<template #actions>
|
||||||
<div class="flex items-center gap-2">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
:disabled="checkinTimeLoading || !hasCheckinTimeChanged"
|
||||||
|
@click="handleCheckinTimeSave"
|
||||||
|
>
|
||||||
|
{{ checkinTimeLoading ? '保存中...' : '保存' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="enable-provider-checkin"
|
id="enable-provider-checkin"
|
||||||
:model-value="systemConfig.enable_provider_checkin"
|
:model-value="systemConfig.enable_provider_checkin"
|
||||||
@@ -497,15 +506,63 @@
|
|||||||
<div>
|
<div>
|
||||||
<Label
|
<Label
|
||||||
for="enable-provider-checkin"
|
for="enable-provider-checkin"
|
||||||
class="text-sm cursor-pointer"
|
class="cursor-pointer"
|
||||||
>
|
>
|
||||||
启用 Provider 自动签到
|
启用 Provider 自动签到
|
||||||
</Label>
|
</Label>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
每天凌晨 1:05 执行
|
自动执行已配置 Provider 的签到任务
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="systemConfig.enable_provider_checkin">
|
||||||
|
<Label class="block text-sm font-medium">
|
||||||
|
执行时间
|
||||||
|
</Label>
|
||||||
|
<div class="mt-1 flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
v-model:open="checkinHourSelectOpen"
|
||||||
|
:model-value="checkinHour"
|
||||||
|
@update:model-value="(val: string) => updateCheckinTime(val, checkinMinute)"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-20">
|
||||||
|
<SelectValue placeholder="时" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="h in 24"
|
||||||
|
:key="h - 1"
|
||||||
|
:value="String(h - 1).padStart(2, '0')"
|
||||||
|
>
|
||||||
|
{{ String(h - 1).padStart(2, '0') }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span class="text-muted-foreground">:</span>
|
||||||
|
<Select
|
||||||
|
v-model:open="checkinMinuteSelectOpen"
|
||||||
|
:model-value="checkinMinute"
|
||||||
|
@update:model-value="(val: string) => updateCheckinTime(checkinHour, val)"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-20">
|
||||||
|
<SelectValue placeholder="分" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="m in 60"
|
||||||
|
:key="m - 1"
|
||||||
|
:value="String(m - 1).padStart(2, '0')"
|
||||||
|
>
|
||||||
|
{{ String(m - 1).padStart(2, '0') }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
每天定时执行(24小时制)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
|
|
||||||
@@ -924,6 +981,7 @@ interface SystemConfig {
|
|||||||
audit_log_retention_days: number
|
audit_log_retention_days: number
|
||||||
// 定时任务
|
// 定时任务
|
||||||
enable_provider_checkin: boolean
|
enable_provider_checkin: boolean
|
||||||
|
provider_checkin_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const basicConfigLoading = ref(false)
|
const basicConfigLoading = ref(false)
|
||||||
@@ -980,6 +1038,7 @@ const systemConfig = ref<SystemConfig>({
|
|||||||
audit_log_retention_days: 30,
|
audit_log_retention_days: 30,
|
||||||
// 定时任务
|
// 定时任务
|
||||||
enable_provider_checkin: true,
|
enable_provider_checkin: true,
|
||||||
|
provider_checkin_time: '01:05',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 原始配置值(用于检测变动)
|
// 原始配置值(用于检测变动)
|
||||||
@@ -1087,6 +1146,7 @@ async function loadSystemConfig() {
|
|||||||
'audit_log_retention_days',
|
'audit_log_retention_days',
|
||||||
// 定时任务
|
// 定时任务
|
||||||
'enable_provider_checkin',
|
'enable_provider_checkin',
|
||||||
|
'provider_checkin_time',
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const key of configs) {
|
for (const key of configs) {
|
||||||
@@ -1101,6 +1161,8 @@ async function loadSystemConfig() {
|
|||||||
}
|
}
|
||||||
// 保存原始值用于变动检测
|
// 保存原始值用于变动检测
|
||||||
originalConfig.value = JSON.parse(JSON.stringify(systemConfig.value))
|
originalConfig.value = JSON.parse(JSON.stringify(systemConfig.value))
|
||||||
|
// 初始化签到时间的原始值(用于回滚)
|
||||||
|
previousCheckinTime.value = systemConfig.value.provider_checkin_time
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error('加载系统配置失败')
|
error('加载系统配置失败')
|
||||||
log.error('加载系统配置失败:', err)
|
log.error('加载系统配置失败:', err)
|
||||||
@@ -1243,6 +1305,61 @@ async function handleProviderCheckinToggle(enabled: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 签到时间相关
|
||||||
|
const previousCheckinTime = ref('')
|
||||||
|
const checkinTimeLoading = ref(false)
|
||||||
|
const checkinHourSelectOpen = ref(false)
|
||||||
|
const checkinMinuteSelectOpen = ref(false)
|
||||||
|
|
||||||
|
// 解析签到时间的小时和分钟
|
||||||
|
const checkinHour = computed(() => {
|
||||||
|
const time = systemConfig.value.provider_checkin_time
|
||||||
|
if (!time || !time.includes(':')) return '01'
|
||||||
|
return time.split(':')[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkinMinute = computed(() => {
|
||||||
|
const time = systemConfig.value.provider_checkin_time
|
||||||
|
if (!time || !time.includes(':')) return '05'
|
||||||
|
return time.split(':')[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新签到时间
|
||||||
|
function updateCheckinTime(hour: string, minute: string) {
|
||||||
|
systemConfig.value.provider_checkin_time = `${hour}:${minute}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测签到时间是否有变化
|
||||||
|
const hasCheckinTimeChanged = computed(() => {
|
||||||
|
return systemConfig.value.provider_checkin_time !== previousCheckinTime.value
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleCheckinTimeSave() {
|
||||||
|
const newTime = systemConfig.value.provider_checkin_time
|
||||||
|
|
||||||
|
// 验证时间格式
|
||||||
|
if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) {
|
||||||
|
error('请输入有效的时间格式 (HH:MM)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkinTimeLoading.value = true
|
||||||
|
try {
|
||||||
|
await adminApi.updateSystemConfig(
|
||||||
|
'provider_checkin_time',
|
||||||
|
newTime,
|
||||||
|
'Provider 自动签到执行时间(HH:MM 格式)'
|
||||||
|
)
|
||||||
|
previousCheckinTime.value = newTime
|
||||||
|
success(`签到时间已设置为 ${newTime}`)
|
||||||
|
} catch (err) {
|
||||||
|
error('保存签到时间失败')
|
||||||
|
log.error('保存签到时间失败:', err)
|
||||||
|
} finally {
|
||||||
|
checkinTimeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveCleanupConfig() {
|
async function saveCleanupConfig() {
|
||||||
cleanupConfigLoading.value = true
|
cleanupConfigLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -614,6 +614,16 @@ class AdminSetSystemConfigAdapter(AdminApiAdapter):
|
|||||||
payload.get("description"),
|
payload.get("description"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 如果更新的是签到任务时间,动态更新调度器
|
||||||
|
if self.key == "provider_checkin_time" and value:
|
||||||
|
try:
|
||||||
|
from src.services.system.maintenance_scheduler import get_maintenance_scheduler
|
||||||
|
|
||||||
|
scheduler = get_maintenance_scheduler()
|
||||||
|
scheduler.update_checkin_time(value)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"更新签到任务时间失败: {e}")
|
||||||
|
|
||||||
# 返回时不暴露加密后的值
|
# 返回时不暴露加密后的值
|
||||||
display_value = "********" if self.key in self.ENCRYPTED_KEYS else config.value
|
display_value = "********" if self.key in self.ENCRYPTED_KEYS else config.value
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ class SystemConfigService:
|
|||||||
},
|
},
|
||||||
"enable_provider_checkin": {
|
"enable_provider_checkin": {
|
||||||
"value": True,
|
"value": True,
|
||||||
"description": "是否启用 Provider 自动签到任务,每天凌晨 1:05 执行",
|
"description": "是否启用 Provider 自动签到任务",
|
||||||
|
},
|
||||||
|
"provider_checkin_time": {
|
||||||
|
"value": "01:05",
|
||||||
|
"description": "Provider 自动签到执行时间(HH:MM 格式,24小时制)",
|
||||||
},
|
},
|
||||||
"provider_priority_mode": {
|
"provider_priority_mode": {
|
||||||
"value": "provider",
|
"value": "provider",
|
||||||
|
|||||||
@@ -36,11 +36,82 @@ from src.utils.compression import compress_json
|
|||||||
class MaintenanceScheduler:
|
class MaintenanceScheduler:
|
||||||
"""系统维护任务调度器"""
|
"""系统维护任务调度器"""
|
||||||
|
|
||||||
|
# 签到任务的 job_id
|
||||||
|
CHECKIN_JOB_ID = "provider_checkin"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.running = False
|
self.running = False
|
||||||
self._interval_tasks = []
|
self._interval_tasks = []
|
||||||
self._stats_aggregation_lock = asyncio.Lock()
|
self._stats_aggregation_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def _get_checkin_time(self) -> tuple[int, int]:
|
||||||
|
"""获取签到任务的执行时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(hour, minute) 元组
|
||||||
|
"""
|
||||||
|
db = create_session()
|
||||||
|
try:
|
||||||
|
time_str = SystemConfigService.get_config(db, "provider_checkin_time", "01:05")
|
||||||
|
return self._parse_time_string(time_str)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_time_string(time_str: str) -> tuple[int, int]:
|
||||||
|
"""解析时间字符串为 (hour, minute) 元组
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_str: HH:MM 格式的时间字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(hour, minute) 元组,解析失败返回默认值 (1, 5)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not time_str or ":" not in time_str:
|
||||||
|
return (1, 5)
|
||||||
|
parts = time_str.split(":")
|
||||||
|
hour = int(parts[0])
|
||||||
|
minute = int(parts[1])
|
||||||
|
# 验证范围
|
||||||
|
if 0 <= hour <= 23 and 0 <= minute <= 59:
|
||||||
|
return (hour, minute)
|
||||||
|
return (1, 5)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return (1, 5)
|
||||||
|
|
||||||
|
def update_checkin_time(self, time_str: str) -> bool:
|
||||||
|
"""更新签到任务的执行时间
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_str: HH:MM 格式的时间字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功更新
|
||||||
|
"""
|
||||||
|
hour, minute = self._parse_time_string(time_str)
|
||||||
|
|
||||||
|
scheduler = get_scheduler()
|
||||||
|
success = scheduler.reschedule_cron_job(
|
||||||
|
self.CHECKIN_JOB_ID,
|
||||||
|
hour=hour,
|
||||||
|
minute=minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"Provider 签到任务时间已更新为: {hour:02d}:{minute:02d}")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def get_checkin_job_info(self) -> dict | None:
|
||||||
|
"""获取签到任务的信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
任务信息字典
|
||||||
|
"""
|
||||||
|
scheduler = get_scheduler()
|
||||||
|
return scheduler.get_job_info(self.CHECKIN_JOB_ID)
|
||||||
|
|
||||||
async def start(self) -> Any:
|
async def start(self) -> Any:
|
||||||
"""启动调度器"""
|
"""启动调度器"""
|
||||||
if self.running:
|
if self.running:
|
||||||
@@ -112,12 +183,13 @@ class MaintenanceScheduler:
|
|||||||
name="Gemini文件映射清理",
|
name="Gemini文件映射清理",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Provider 签到任务 - 凌晨 1:05 执行
|
# Provider 签到任务 - 根据配置时间执行
|
||||||
|
checkin_hour, checkin_minute = self._get_checkin_time()
|
||||||
scheduler.add_cron_job(
|
scheduler.add_cron_job(
|
||||||
self._scheduled_provider_checkin,
|
self._scheduled_provider_checkin,
|
||||||
hour=1,
|
hour=checkin_hour,
|
||||||
minute=5, # 在统计聚合任务(1:00)之后执行
|
minute=checkin_minute,
|
||||||
job_id="provider_checkin",
|
job_id=self.CHECKIN_JOB_ID,
|
||||||
name="Provider签到",
|
name="Provider签到",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,66 @@ class TaskScheduler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"移除定时任务失败 {job_id}: {e}")
|
logger.warning(f"移除定时任务失败 {job_id}: {e}")
|
||||||
|
|
||||||
|
def reschedule_cron_job(
|
||||||
|
self,
|
||||||
|
job_id: str,
|
||||||
|
hour: int,
|
||||||
|
minute: int = 0,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
重新调度 cron 定时任务的执行时间
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 任务ID
|
||||||
|
hour: 新的执行时间(小时),使用业务时区
|
||||||
|
minute: 新的执行时间(分钟)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功重新调度
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job = self.scheduler.get_job(job_id)
|
||||||
|
if not job:
|
||||||
|
logger.warning(f"任务不存在: {job_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
trigger = CronTrigger(hour=hour, minute=minute, timezone=APP_TIMEZONE)
|
||||||
|
self.scheduler.reschedule_job(job_id, trigger=trigger)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"已重新调度定时任务: {job.name}, "
|
||||||
|
f"新执行时间: {hour:02d}:{minute:02d} ({APP_TIMEZONE})"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"重新调度任务失败 {job_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_job_info(self, job_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
获取任务信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 任务ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
任务信息字典,包含 name, next_run_time 等
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job = self.scheduler.get_job(job_id)
|
||||||
|
if not job:
|
||||||
|
return None
|
||||||
|
|
||||||
|
next_run = job.next_run_time
|
||||||
|
return {
|
||||||
|
"id": job.id,
|
||||||
|
"name": job.name,
|
||||||
|
"next_run_time": next_run.isoformat() if next_run else None,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取任务信息失败 {job_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""调度器是否在运行"""
|
"""调度器是否在运行"""
|
||||||
|
|||||||
Reference in New Issue
Block a user