mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge branch 'pr-140'
This commit is contained in:
@@ -490,12 +490,13 @@
|
||||
<template #actions>
|
||||
<Button
|
||||
size="sm"
|
||||
:disabled="checkinTimeLoading || !hasCheckinTimeChanged"
|
||||
@click="handleCheckinTimeSave"
|
||||
:disabled="schedulerConfigLoading || !hasSchedulerConfigChanges"
|
||||
@click="handleSchedulerConfigSave"
|
||||
>
|
||||
{{ checkinTimeLoading ? '保存中...' : '保存' }}
|
||||
{{ schedulerConfigLoading ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
@@ -564,6 +565,100 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable-user-quota-reset"
|
||||
:model-value="systemConfig.enable_user_quota_reset"
|
||||
@update:model-value="handleUserQuotaResetToggle"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
for="enable-user-quota-reset"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
启用用户配额自动重置
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
每天定时触发,按周期执行
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="systemConfig.enable_user_quota_reset"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">
|
||||
执行时间
|
||||
</Label>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<Select
|
||||
v-model:open="userQuotaResetHourSelectOpen"
|
||||
:model-value="userQuotaResetHour"
|
||||
@update:model-value="(val: string) => updateUserQuotaResetTime(val, userQuotaResetMinute)"
|
||||
>
|
||||
<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="userQuotaResetMinuteSelectOpen"
|
||||
:model-value="userQuotaResetMinute"
|
||||
@update:model-value="(val: string) => updateUserQuotaResetTime(userQuotaResetHour, 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>
|
||||
<Label class="block text-sm font-medium">
|
||||
重置周期
|
||||
</Label>
|
||||
<div class="mt-1 flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs text-muted-foreground">每</span>
|
||||
<Input
|
||||
v-model.number="systemConfig.user_quota_reset_interval_days"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="w-24"
|
||||
/>
|
||||
<span class="text-xs text-muted-foreground">天</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
滚动计算:距离上次成功执行满 N 天后才会再次执行
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- 系统版本信息 -->
|
||||
@@ -982,6 +1077,9 @@ interface SystemConfig {
|
||||
// 定时任务
|
||||
enable_provider_checkin: boolean
|
||||
provider_checkin_time: string
|
||||
enable_user_quota_reset: boolean
|
||||
user_quota_reset_time: string
|
||||
user_quota_reset_interval_days: number
|
||||
}
|
||||
|
||||
const basicConfigLoading = ref(false)
|
||||
@@ -1039,6 +1137,9 @@ const systemConfig = ref<SystemConfig>({
|
||||
// 定时任务
|
||||
enable_provider_checkin: true,
|
||||
provider_checkin_time: '01:05',
|
||||
enable_user_quota_reset: false,
|
||||
user_quota_reset_time: '05:00',
|
||||
user_quota_reset_interval_days: 1,
|
||||
})
|
||||
|
||||
// 原始配置值(用于检测变动)
|
||||
@@ -1147,6 +1248,9 @@ async function loadSystemConfig() {
|
||||
// 定时任务
|
||||
'enable_provider_checkin',
|
||||
'provider_checkin_time',
|
||||
'enable_user_quota_reset',
|
||||
'user_quota_reset_time',
|
||||
'user_quota_reset_interval_days',
|
||||
]
|
||||
|
||||
for (const key of configs) {
|
||||
@@ -1163,6 +1267,9 @@ async function loadSystemConfig() {
|
||||
originalConfig.value = JSON.parse(JSON.stringify(systemConfig.value))
|
||||
// 初始化签到时间的原始值(用于回滚)
|
||||
previousCheckinTime.value = systemConfig.value.provider_checkin_time
|
||||
// 初始化配额重置时间的原始值(用于回滚)
|
||||
previousUserQuotaResetTime.value = systemConfig.value.user_quota_reset_time
|
||||
previousUserQuotaResetIntervalDays.value = systemConfig.value.user_quota_reset_interval_days
|
||||
} catch (err) {
|
||||
error('加载系统配置失败')
|
||||
log.error('加载系统配置失败:', err)
|
||||
@@ -1305,9 +1412,8 @@ async function handleProviderCheckinToggle(enabled: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
// 签到时间相关
|
||||
const previousCheckinTime = ref('')
|
||||
const checkinTimeLoading = ref(false)
|
||||
const schedulerConfigLoading = ref(false)
|
||||
const checkinHourSelectOpen = ref(false)
|
||||
const checkinMinuteSelectOpen = ref(false)
|
||||
|
||||
@@ -1334,29 +1440,138 @@ 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
|
||||
async function handleUserQuotaResetToggle(enabled: boolean) {
|
||||
const previousValue = systemConfig.value.enable_user_quota_reset
|
||||
systemConfig.value.enable_user_quota_reset = enabled
|
||||
try {
|
||||
await adminApi.updateSystemConfig(
|
||||
'provider_checkin_time',
|
||||
newTime,
|
||||
'Provider 自动签到执行时间(HH:MM 格式)'
|
||||
'enable_user_quota_reset',
|
||||
enabled,
|
||||
'是否启用用户配额自动重置任务'
|
||||
)
|
||||
previousCheckinTime.value = newTime
|
||||
success(`签到时间已设置为 ${newTime}`)
|
||||
success(enabled ? '已启用用户配额自动重置' : '已禁用用户配额自动重置')
|
||||
} catch (err) {
|
||||
error('保存签到时间失败')
|
||||
log.error('保存签到时间失败:', err)
|
||||
error('保存配置失败')
|
||||
log.error('保存用户配额自动重置配置失败:', err)
|
||||
// 回滚状态
|
||||
systemConfig.value.enable_user_quota_reset = previousValue
|
||||
}
|
||||
}
|
||||
|
||||
// 用户配额重置时间相关
|
||||
const previousUserQuotaResetTime = ref('')
|
||||
const userQuotaResetHourSelectOpen = ref(false)
|
||||
const userQuotaResetMinuteSelectOpen = ref(false)
|
||||
const previousUserQuotaResetIntervalDays = ref(1)
|
||||
|
||||
const userQuotaResetHour = computed(() => {
|
||||
const time = systemConfig.value.user_quota_reset_time
|
||||
if (!time || !time.includes(':')) return '05'
|
||||
return time.split(':')[0]
|
||||
})
|
||||
|
||||
const userQuotaResetMinute = computed(() => {
|
||||
const time = systemConfig.value.user_quota_reset_time
|
||||
if (!time || !time.includes(':')) return '00'
|
||||
return time.split(':')[1]
|
||||
})
|
||||
|
||||
function updateUserQuotaResetTime(hour: string, minute: string) {
|
||||
systemConfig.value.user_quota_reset_time = `${hour}:${minute}`
|
||||
}
|
||||
|
||||
const hasUserQuotaResetTimeChanged = computed(() => {
|
||||
return systemConfig.value.user_quota_reset_time !== previousUserQuotaResetTime.value
|
||||
})
|
||||
|
||||
const hasUserQuotaResetIntervalChanged = computed(() => {
|
||||
return systemConfig.value.user_quota_reset_interval_days !== previousUserQuotaResetIntervalDays.value
|
||||
})
|
||||
|
||||
const hasSchedulerConfigChanges = computed(() => {
|
||||
return (
|
||||
hasCheckinTimeChanged.value ||
|
||||
hasUserQuotaResetTimeChanged.value ||
|
||||
hasUserQuotaResetIntervalChanged.value
|
||||
)
|
||||
})
|
||||
|
||||
async function handleSchedulerConfigSave() {
|
||||
const configItems: Array<{ key: string, value: any, description: string, onSuccess: () => void }> = []
|
||||
|
||||
if (hasCheckinTimeChanged.value) {
|
||||
const newTime = systemConfig.value.provider_checkin_time
|
||||
if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) {
|
||||
error('请输入有效的时间格式 (HH:MM)')
|
||||
return
|
||||
}
|
||||
configItems.push({
|
||||
key: 'provider_checkin_time',
|
||||
value: newTime,
|
||||
description: 'Provider 自动签到执行时间(HH:MM 格式)',
|
||||
onSuccess: () => {
|
||||
previousCheckinTime.value = newTime
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasUserQuotaResetTimeChanged.value) {
|
||||
const newTime = systemConfig.value.user_quota_reset_time
|
||||
if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) {
|
||||
error('请输入有效的时间格式 (HH:MM)')
|
||||
return
|
||||
}
|
||||
configItems.push({
|
||||
key: 'user_quota_reset_time',
|
||||
value: newTime,
|
||||
description: '用户配额自动重置执行时间(HH:MM 格式)',
|
||||
onSuccess: () => {
|
||||
previousUserQuotaResetTime.value = newTime
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasUserQuotaResetIntervalChanged.value) {
|
||||
let intervalDays = Number(systemConfig.value.user_quota_reset_interval_days)
|
||||
if (!Number.isFinite(intervalDays) || intervalDays < 1) intervalDays = 1
|
||||
intervalDays = Math.trunc(intervalDays)
|
||||
|
||||
systemConfig.value.user_quota_reset_interval_days = intervalDays
|
||||
|
||||
configItems.push({
|
||||
key: 'user_quota_reset_interval_days',
|
||||
value: intervalDays,
|
||||
description: '用户配额重置周期(天数),滚动计算',
|
||||
onSuccess: () => {
|
||||
previousUserQuotaResetIntervalDays.value = intervalDays
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (configItems.length === 0) return
|
||||
|
||||
schedulerConfigLoading.value = true
|
||||
const failedKeys: string[] = []
|
||||
|
||||
try {
|
||||
for (const item of configItems) {
|
||||
try {
|
||||
await adminApi.updateSystemConfig(item.key, item.value, item.description)
|
||||
item.onSuccess()
|
||||
} catch (err) {
|
||||
failedKeys.push(item.key)
|
||||
log.error(`保存定时任务配置失败: ${item.key}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
if (failedKeys.length > 0) {
|
||||
error(`部分配置保存失败: ${failedKeys.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
success('定时任务配置已保存')
|
||||
} finally {
|
||||
checkinTimeLoading.value = false
|
||||
schedulerConfigLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -624,6 +624,16 @@ class AdminSetSystemConfigAdapter(AdminApiAdapter):
|
||||
except Exception as e:
|
||||
logger.warning(f"更新签到任务时间失败: {e}")
|
||||
|
||||
# 如果更新的是用户配额重置任务时间,动态更新调度器
|
||||
if self.key == "user_quota_reset_time" and value:
|
||||
try:
|
||||
from src.services.system.maintenance_scheduler import get_maintenance_scheduler
|
||||
|
||||
scheduler = get_maintenance_scheduler()
|
||||
scheduler.update_user_quota_reset_time(value)
|
||||
except Exception as e:
|
||||
logger.warning(f"更新用户配额重置任务时间失败: {e}")
|
||||
|
||||
# 返回时不暴露加密后的值
|
||||
display_value = "********" if self.key in self.ENCRYPTED_KEYS else config.value
|
||||
|
||||
|
||||
@@ -123,6 +123,18 @@ class SystemConfigService:
|
||||
"value": "01:05",
|
||||
"description": "Provider 自动签到执行时间(HH:MM 格式,24小时制)",
|
||||
},
|
||||
"enable_user_quota_reset": {
|
||||
"value": False,
|
||||
"description": "是否启用用户配额自动重置任务(按配置时间触发,按周期执行)",
|
||||
},
|
||||
"user_quota_reset_time": {
|
||||
"value": "05:00",
|
||||
"description": "用户配额自动重置执行时间(HH:MM 格式,24小时制)",
|
||||
},
|
||||
"user_quota_reset_interval_days": {
|
||||
"value": 1,
|
||||
"description": "用户配额重置周期(天数)",
|
||||
},
|
||||
"provider_priority_mode": {
|
||||
"value": "provider",
|
||||
"description": "优先级策略:provider(提供商优先模式) 或 global_key(全局Key优先模式)",
|
||||
|
||||
@@ -38,6 +38,8 @@ class MaintenanceScheduler:
|
||||
|
||||
# 签到任务的 job_id
|
||||
CHECKIN_JOB_ID = "provider_checkin"
|
||||
# 用户配额重置任务的 job_id
|
||||
USER_QUOTA_RESET_JOB_ID = "user_quota_reset"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.running = False
|
||||
@@ -57,6 +59,19 @@ class MaintenanceScheduler:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _get_user_quota_reset_time(self) -> tuple[int, int]:
|
||||
"""获取用户配额重置任务的执行时间
|
||||
|
||||
Returns:
|
||||
(hour, minute) 元组
|
||||
"""
|
||||
db = create_session()
|
||||
try:
|
||||
time_str = SystemConfigService.get_config(db, "user_quota_reset_time", "05:00")
|
||||
return self._parse_user_quota_reset_time_string(time_str)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def _parse_time_string(time_str: str) -> tuple[int, int]:
|
||||
"""解析时间字符串为 (hour, minute) 元组
|
||||
@@ -80,6 +95,26 @@ class MaintenanceScheduler:
|
||||
except (ValueError, IndexError):
|
||||
return (1, 5)
|
||||
|
||||
@staticmethod
|
||||
def _parse_user_quota_reset_time_string(time_str: str) -> tuple[int, int]:
|
||||
"""解析用户配额重置时间字符串为 (hour, minute) 元组
|
||||
|
||||
Returns:
|
||||
(hour, minute) 元组,解析失败返回默认值 (5, 0)
|
||||
"""
|
||||
try:
|
||||
if not time_str or ":" not in time_str:
|
||||
return (5, 0)
|
||||
parts = time_str.split(":")
|
||||
hour = int(parts[0])
|
||||
minute = int(parts[1])
|
||||
# 验证范围
|
||||
if 0 <= hour <= 23 and 0 <= minute <= 59:
|
||||
return (hour, minute)
|
||||
return (5, 0)
|
||||
except (ValueError, IndexError):
|
||||
return (5, 0)
|
||||
|
||||
def update_checkin_time(self, time_str: str) -> bool:
|
||||
"""更新签到任务的执行时间
|
||||
|
||||
@@ -103,6 +138,29 @@ class MaintenanceScheduler:
|
||||
|
||||
return success
|
||||
|
||||
def update_user_quota_reset_time(self, time_str: str) -> bool:
|
||||
"""更新用户配额重置任务的执行时间
|
||||
|
||||
Args:
|
||||
time_str: HH:MM 格式的时间字符串
|
||||
|
||||
Returns:
|
||||
是否成功更新
|
||||
"""
|
||||
hour, minute = self._parse_user_quota_reset_time_string(time_str)
|
||||
|
||||
scheduler = get_scheduler()
|
||||
success = scheduler.reschedule_cron_job(
|
||||
self.USER_QUOTA_RESET_JOB_ID,
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"用户配额重置任务时间已更新为: {hour:02d}:{minute:02d}")
|
||||
|
||||
return success
|
||||
|
||||
def get_checkin_job_info(self) -> dict | None:
|
||||
"""获取签到任务的信息
|
||||
|
||||
@@ -203,6 +261,16 @@ class MaintenanceScheduler:
|
||||
name="Provider签到",
|
||||
)
|
||||
|
||||
# 用户配额重置任务 - 根据配置时间执行(按周期配置决定是否执行)
|
||||
quota_reset_hour, quota_reset_minute = self._get_user_quota_reset_time()
|
||||
scheduler.add_cron_job(
|
||||
self._scheduled_user_quota_reset,
|
||||
hour=quota_reset_hour,
|
||||
minute=quota_reset_minute,
|
||||
job_id=self.USER_QUOTA_RESET_JOB_ID,
|
||||
name="用户配额自动重置",
|
||||
)
|
||||
|
||||
# 启动时执行一次初始化任务
|
||||
asyncio.create_task(self._run_startup_tasks())
|
||||
|
||||
@@ -274,6 +342,10 @@ class MaintenanceScheduler:
|
||||
"""Provider 签到任务(定时调用)"""
|
||||
await self._perform_provider_checkin()
|
||||
|
||||
async def _scheduled_user_quota_reset(self) -> None:
|
||||
"""用户配额重置任务(定时调用)"""
|
||||
await self._perform_user_quota_reset()
|
||||
|
||||
# ========== 实际任务实现 ==========
|
||||
|
||||
async def _perform_stats_aggregation(self, backfill: bool = False) -> None:
|
||||
@@ -677,6 +749,108 @@ class MaintenanceScheduler:
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
async def _perform_user_quota_reset(self) -> None:
|
||||
"""执行用户配额自动重置任务
|
||||
|
||||
适用范围:
|
||||
- 未删除(is_deleted=false)
|
||||
- 仅对 quota_usd != NULL 的用户生效
|
||||
"""
|
||||
db = create_session()
|
||||
try:
|
||||
# 检查是否启用用户配额重置
|
||||
if not SystemConfigService.get_config(db, "enable_user_quota_reset", False):
|
||||
logger.info("用户配额自动重置已禁用,跳过任务")
|
||||
return
|
||||
|
||||
# 重置周期(天数),不限制上限
|
||||
interval_value = SystemConfigService.get_config(db, "user_quota_reset_interval_days", 1)
|
||||
try:
|
||||
interval_days = int(interval_value)
|
||||
except Exception:
|
||||
interval_days = 1
|
||||
if interval_days < 1:
|
||||
interval_days = 1
|
||||
|
||||
# 滚动计算:根据上次执行日(APP_TIMEZONE)判断是否到期
|
||||
last_reset_at = SystemConfigService.get_config(db, "user_quota_last_reset_at")
|
||||
|
||||
should_run = True
|
||||
if last_reset_at:
|
||||
last_dt: datetime | None = None
|
||||
try:
|
||||
if isinstance(last_reset_at, str):
|
||||
last_dt = datetime.fromisoformat(last_reset_at)
|
||||
except Exception:
|
||||
last_dt = None
|
||||
|
||||
if last_dt is None:
|
||||
logger.warning("user_quota_last_reset_at 格式无效,视为需要执行一次")
|
||||
else:
|
||||
if last_dt.tzinfo is None:
|
||||
last_dt = last_dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from src.services.system.scheduler import APP_TIMEZONE
|
||||
|
||||
tz = ZoneInfo(APP_TIMEZONE)
|
||||
now_local = datetime.now(tz)
|
||||
last_local_date = last_dt.astimezone(tz).date()
|
||||
days_since_reset = (now_local.date() - last_local_date).days
|
||||
|
||||
if days_since_reset < 0:
|
||||
logger.warning(
|
||||
"user_quota_last_reset_at 在未来,跳过本次用户配额自动重置"
|
||||
)
|
||||
should_run = False
|
||||
elif days_since_reset < interval_days:
|
||||
logger.info(
|
||||
f"用户配额自动重置未到周期,跳过任务({days_since_reset}/{interval_days}天)"
|
||||
)
|
||||
should_run = False
|
||||
|
||||
if not should_run:
|
||||
return
|
||||
|
||||
from src.models.database import User as DBUser
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
reset_count = (
|
||||
db.query(DBUser)
|
||||
.filter(
|
||||
DBUser.is_deleted.is_(False),
|
||||
DBUser.quota_usd.isnot(None),
|
||||
)
|
||||
.update(
|
||||
{
|
||||
DBUser.used_usd: 0.0,
|
||||
DBUser.updated_at: now_utc,
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 记录 last_reset_at(成功执行后更新,滚动计算用)
|
||||
SystemConfigService.set_config(
|
||||
db,
|
||||
"user_quota_last_reset_at",
|
||||
now_utc.isoformat(),
|
||||
"用户配额自动重置的上次执行时间(UTC,内部使用)",
|
||||
)
|
||||
|
||||
logger.info(f"用户配额自动重置完成: interval_days={interval_days}, 重置用户数={reset_count}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"用户配额自动重置任务执行失败: {e}")
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
async def _perform_cleanup(self) -> None:
|
||||
"""执行清理任务"""
|
||||
db = create_session()
|
||||
|
||||
166
tests/services/test_maintenance_scheduler_user_quota_reset.py
Normal file
166
tests/services/test_maintenance_scheduler_user_quota_reset.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.system.maintenance_scheduler import MaintenanceScheduler
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_quota_reset_disabled(monkeypatch):
|
||||
scheduler = MaintenanceScheduler()
|
||||
|
||||
mock_db = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.create_session",
|
||||
lambda: mock_db,
|
||||
)
|
||||
|
||||
def fake_get_config(cls, db, key, default=None):
|
||||
if key == "enable_user_quota_reset":
|
||||
return False
|
||||
return default
|
||||
|
||||
mock_set_config = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.get_config",
|
||||
classmethod(fake_get_config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.set_config",
|
||||
mock_set_config,
|
||||
)
|
||||
|
||||
await scheduler._perform_user_quota_reset()
|
||||
|
||||
assert not mock_db.query.called
|
||||
assert not mock_db.commit.called
|
||||
assert not mock_set_config.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_quota_reset_not_due_skips(monkeypatch):
|
||||
scheduler = MaintenanceScheduler()
|
||||
|
||||
mock_db = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.create_session",
|
||||
lambda: mock_db,
|
||||
)
|
||||
|
||||
last_reset_at = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
|
||||
|
||||
def fake_get_config(cls, db, key, default=None):
|
||||
if key == "enable_user_quota_reset":
|
||||
return True
|
||||
if key == "user_quota_reset_interval_days":
|
||||
return 2
|
||||
if key == "user_quota_last_reset_at":
|
||||
return last_reset_at
|
||||
return default
|
||||
|
||||
mock_set_config = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.get_config",
|
||||
classmethod(fake_get_config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.set_config",
|
||||
mock_set_config,
|
||||
)
|
||||
|
||||
await scheduler._perform_user_quota_reset()
|
||||
|
||||
assert not mock_db.query.called
|
||||
assert not mock_db.commit.called
|
||||
assert not mock_set_config.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_quota_reset_due_runs(monkeypatch):
|
||||
scheduler = MaintenanceScheduler()
|
||||
|
||||
mock_db = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.create_session",
|
||||
lambda: mock_db,
|
||||
)
|
||||
|
||||
last_reset_at = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
|
||||
|
||||
def fake_get_config(cls, db, key, default=None):
|
||||
if key == "enable_user_quota_reset":
|
||||
return True
|
||||
if key == "user_quota_reset_interval_days":
|
||||
return 2
|
||||
if key == "user_quota_last_reset_at":
|
||||
return last_reset_at
|
||||
return default
|
||||
|
||||
mock_set_config = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.get_config",
|
||||
classmethod(fake_get_config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.set_config",
|
||||
mock_set_config,
|
||||
)
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_filter = MagicMock()
|
||||
mock_filter.update.return_value = 7
|
||||
mock_query.filter.return_value = mock_filter
|
||||
mock_db.query.return_value = mock_query
|
||||
|
||||
await scheduler._perform_user_quota_reset()
|
||||
|
||||
mock_db.query.assert_called_once()
|
||||
mock_filter.update.assert_called_once()
|
||||
_, update_kwargs = mock_filter.update.call_args
|
||||
assert update_kwargs["synchronize_session"] is False
|
||||
mock_db.commit.assert_called_once()
|
||||
mock_set_config.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_quota_reset_invalid_interval_defaults_to_1(monkeypatch):
|
||||
scheduler = MaintenanceScheduler()
|
||||
|
||||
mock_db = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.create_session",
|
||||
lambda: mock_db,
|
||||
)
|
||||
|
||||
def fake_get_config(cls, db, key, default=None):
|
||||
if key == "enable_user_quota_reset":
|
||||
return True
|
||||
if key == "user_quota_reset_interval_days":
|
||||
return "abc"
|
||||
if key == "user_quota_last_reset_at":
|
||||
return None
|
||||
return default
|
||||
|
||||
mock_set_config = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.get_config",
|
||||
classmethod(fake_get_config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.system.maintenance_scheduler.SystemConfigService.set_config",
|
||||
mock_set_config,
|
||||
)
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_filter = MagicMock()
|
||||
mock_filter.update.return_value = 1
|
||||
mock_query.filter.return_value = mock_filter
|
||||
mock_db.query.return_value = mock_query
|
||||
|
||||
await scheduler._perform_user_quota_reset()
|
||||
|
||||
mock_db.commit.assert_called_once()
|
||||
mock_set_config.assert_called_once()
|
||||
Reference in New Issue
Block a user