# 远程数据自动调度功能设计方案 ## 一、需求概述 ### 1.1 背景 当前系统需要每周一手动点击"远程下载并处理"来跑上周数据(周一到周日,7天数据)。由于服务器时间不准确,无法使用定时任务在固定时间运行。需要一个基于**数据就绪状态**的自动调度机制。 ### 1.2 核心需求 1. **文件时间过滤**:处理时只处理最近7天的文件 2. **自动就绪检测**:每小时检查远程目录,判断上周7天数据是否齐全 3. **自动触发处理**:数据就绪后自动下载并处理 4. **状态标识管理**:使用标识文件避免重复触发 --- ## 二、文件时间解析规则 ### 2.1 文件名格式 | 格式 | 示例 | 数据日期 | |------|------|----------| | `XXX_YYYYMMDDHHMM_YYYYMMDDHHMM` | `CapacityReportData2.6_202605110000_202605120000.zip` | 覆盖起止时间范围,结束零点按右开区间处理 → 2026-05-11 | | `XXX_YYYYMMDDHHMM` | `CapacityReportData2.6_202605110000.zip` | 视为单日文件 → 2026-05-11 | ### 2.2 解析逻辑 复用项目已有的正则 `_ZIP_DATE_RE = re.compile(r"(? FileDateRange | None: """从文件名提取数据覆盖范围。""" values = _ZIP_DATE_RE.findall(filename) if not values: return None start = parse_timestamp(values[0]) end = parse_timestamp(values[1]) if len(values) > 1 else start + timedelta(days=1) return FileDateRange(start=start.date(), end_exclusive=normalize_end(end)) ``` --- ## 三、自动调度机制 ### 3.1 工作流程 ``` 每小时定时器触发 │ ▼ ┌─────────────────────┐ │ 检查就绪标识文件 │ │ (ready.flag) │ └─────────────────────┘ │ ├── 标识存在 ──► 跳过检测,直接触发处理 │ ▼ ┌─────────────────────┐ │ 连接 FTP/SFTP │ │ 遍历远程目录 │ └─────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ 对每个 expected_directory: │ │ 1. 列出所有 ZIP 文件名 │ │ 2. 从文件名提取数据日期 │ │ 3. 检查是否覆盖上周7天(周一~周日)│ └─────────────────────────────────────┘ │ ├── 未就绪 ──► 记录日志,等待下次检查 │ ▼ ┌─────────────────────┐ │ 写入就绪标识文件 │ │ (ready.flag) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ 触发远程下载处理 │ │ (复用现有流程) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ 处理成功后 │ │ 1. 删除远程源文件 │ │ 2. 删除就绪标识 │ └─────────────────────┘ │ ▼ 继续每小时检查 ``` ### 3.2 目标日期范围(严格按自然周) **永远是上周一到上周日**(自然周,7天)。无论今天是周几,程序都会自动计算正确的上周一和上周日。 ```python def get_target_week_range(week_offset: int = 0) -> tuple[date, date]: """获取目标周的周一到周日。 week_offset=0 表示上周,-1 表示上上周 """ today = date.today() this_monday = today - timedelta(days=today.weekday()) target_monday = this_monday - timedelta(days=7 * (1 - week_offset)) target_sunday = target_monday + timedelta(days=6) return target_monday, target_sunday ``` 示例(假设 week_offset=0): | 今天 | 本周一 | 上周一 | 上周日 | 检查范围 | |------|--------|--------|--------|----------| | 周一 5/19 | 5/19 | 5/12 | 5/18 | 5/12~5/18 | | 周三 5/21 | 5/19 | 5/12 | 5/18 | 5/12~5/18 | | 周日 5/25 | 5/19 | 5/12 | 5/18 | 5/12~5/18 | **不会**把非自然周的7天范围(如上周五到本周四)当作目标。`week_offset` 参数仅用于补跑场景:如果上周处理失败,可设为 `-1` 检查上上周的周一到周日。 ### 3.3 就绪判断 一个目录的"就绪"条件:该目录下所有 ZIP 文件提取出的数据日期集合,**完全覆盖**目标周(上周一~上周日)的全部 7 天。 **关键**: 1. 不依赖系统日期,而是从文件名中提取日期 2. 文件名带两个时间戳时按覆盖范围判断;只有一个时间戳时视为单日文件 3. 例如文件 `CapacityReportData2.6_202605120000_202605130000.zip` → 覆盖 2026-05-12 4. 如果某个日期没有对应的 ZIP 文件,该目录判定为未就绪 RJ 目录会额外按最新文件自动判断粒度:最新文件为单日时要求 7 个目标日都齐全;最新文件为多日/周文件时要求存在一个 ZIP 覆盖完整目标自然周。 ### 3.4 标识文件 - **位置**:`cache/auto_scheduler/ready.flag` - **格式**:JSON ```json { "ready_at": "2026-05-19T10:00:00", "week_start": "2026-05-12", "week_end": "2026-05-18", "directories": { "4G/FDD": {"found_days": 7, "required_days": 7}, "4G/900": {"found_days": 7, "required_days": 7}, "5G/2.6": {"found_days": 7, "required_days": 7}, "5G/700": {"found_days": 7, "required_days": 7} } } ``` --- ## 四、配置项设计 ### 4.1 新增配置(`Configure.json` → `RemoteData` 节点下) ```json { "RemoteData": { "enabled": true, "protocol": "sftp", "host": "127.0.0.1", "port": 2022, "user": "nixevol", "remote_dir": "/CapacityReportData", "passive": true, "timeout": 30, "auto_delete_source": true, "passwd": "242520", "auto_scheduler": { "enabled": false, "check_interval_hours": 1, "expected_directories": [ "4G/FDD", "4G/900", "5G/2.6", "5G/700" ], "week_offset": 0 } } } ``` ### 4.2 字段说明 | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `enabled` | bool | `false` | 是否启用自动调度 | | `check_interval_hours` | int | `1` | 检查间隔(小时),最小1 | | `expected_directories` | list[str] | `[]` | 需要检测就绪的子目录路径列表(相对于 `remote_dir`)。为空时按实际 ZIP 所在目录逐个检测 | | `week_offset` | int | `0` | 周偏移,`0` = 检查上周,`-1` = 检查上上周 | ### 4.3 约束规则 1. **自动调度开启时,`auto_delete_source` 强制为 `true`**(前端灰显、后端校验) 2. `expected_directories` 为空时按远程 ZIP 实际所在目录逐个检测 3. `check_interval_hours` 最小值为 1 4. 配置了 `expected_directories` 时,某个预期目录可访问但完全没有 ZIP 文件,视为该目录已停推并跳过;所有目录都为空时不触发自动处理 --- ## 五、代码改动计划 ### 5.1 后端 #### `app/config.py` - `RemoteDataConfig` 新增 4 个字段 + `AutoSchedulerConfig` 子配置类 - 更新 `from_dict()`、`to_dict()`、`to_file_dict()` 序列化逻辑 - 新增约束校验:`auto_scheduler.enabled = true` 时强制 `auto_delete_source = true` #### `app/services/remote_download.py` - 新增 `list_remote_zip_files(directory: str | None) -> list[RemoteFileInfo]`:递归列出指定远程目录下所有 ZIP 文件路径、相对路径、所在目录和大小(只扫描,不下载) - 远程下载会先按每个实际目录筛选最近 7 天 ZIP;自动调度触发时按 ready flag 中的目标日期精确下载,避免数据就绪后远程目录又出现新文件时误下载非目标周数据。 #### `app/services/auto_scheduler.py`(**新建**) - `AutoScheduler` 类 - `start()` / `stop()`:启动/停止后台定时线程 - `check_and_run()`:检查 → 就绪 → 触发处理 - `_is_ready()`:检查所有目录就绪状态 - `_mark_ready()` / `_clear_ready()`:标识文件管理 - `_trigger_processing()`:复用 `remote.py` 中的 `_run_remote_processing` 逻辑 - `get_status()` → dict:返回调度器当前状态 #### `app/main.py` - 应用启动时初始化 `AutoScheduler` 并 start - 应用关闭时 stop #### `app/api/routers/remote.py` - 新增 `GET /api/remote/scheduler/status`:查询调度状态 - 新增 `POST /api/remote/scheduler/trigger`:手动触发一次检测 - 修改 `POST /api/remote/start`:自动调度期间禁止手动触发(避免冲突) ### 5.2 前端 #### `frontend/src/components/SettingsPanel.vue` 在"远程数据源"配置区域新增"自动调度"折叠卡片: - 启用开关 - 检查间隔(小时)输入框 - 预期目录列表(支持增删) - 周偏移选择(默认上周) - 状态面板:下次检查时间、上次检查结果、就绪状态、各目录检测结果 --- ## 六、日志与监控 ### 6.1 日志示例 ``` [10:00:00] 自动调度:开始检查远程目录就绪状态 [10:00:01] 自动调度:连接 SFTP 127.0.0.1:2022 [10:00:02] 自动调度:检查 4G/FDD → 找到 5/7 天文件(缺少 2026-05-13, 2026-05-14) [10:00:03] 自动调度:检查 4G/900 → 找到 7/7 天文件 ✓ [10:00:04] 自动调度:检查 5G/2.6 → 找到 7/7 天文件 ✓ [10:00:05] 自动调度:检查 5G/700 → 找到 6/7 天文件(缺少 2026-05-18) [10:00:06] 自动调度:数据未就绪(2/4 目录满足),等待下次检查 ... [11:00:00] 自动调度:开始检查远程目录就绪状态 [11:00:05] 自动调度:所有目录数据就绪 (4/4),写入标识文件 [11:00:06] 自动调度:触发远程下载处理任务 [11:05:00] 自动调度:任务处理成功 [11:05:01] 自动调度:远程源文件已清理 [11:05:02] 自动调度:已清除就绪标识,继续监控 ``` ### 6.2 状态接口响应 ```json GET /api/remote/scheduler/status { "enabled": true, "running": true, "next_check_at": "2026-05-19T12:00:00", "last_check_at": "2026-05-19T11:00:00", "last_result": "triggered", "failure_count": 0, "task_running": false, "ready_flag": { "exists": false }, "target_week": { "start": "2026-05-12", "end": "2026-05-18" }, "directory_status": { "4G/FDD": {"found_days": ["2026-05-12"], "found_count": 7, "required_count": 7, "ready": true}, "4G/900": {"found_days": ["2026-05-12"], "found_count": 7, "required_count": 7, "ready": true}, "5G/2.6": {"found_days": ["2026-05-12"], "found_count": 7, "required_count": 7, "ready": true}, "5G/700": {"found_days": ["2026-05-12"], "found_count": 7, "required_count": 7, "ready": true} } } ``` --- ## 七、异常处理 | 场景 | 处理方式 | |------|----------| | SFTP/FTP 连接失败 | 记录错误日志,等下次周期重试 | | 连续失败 ≥3 次 | 在前端调度状态面板显示红色失败状态 | | 处理失败 | **保留就绪标识**,下次检查时直接触发处理(不重新检测) | | 远程目录不存在 | 跳过该目录,记录警告,其他目录正常检测 | | 预期目录内无 ZIP 文件 | 视为该目录已停推并跳过,不阻塞其他目录;如果所有目录都为空,则继续等待,不触发处理 | | 手动触发远程处理 | 自动调度运行中禁止手动触发,反之亦然(互斥锁复用 `global_task_lock`) | --- ## 八、测试要点 1. **文件名解析** - `XXX_202605110000_202605120000.zip` → 2026-05-11 ✓ - `XXX_202605110000.zip` → 2026-05-11 ✓ - `no_date_here.zip` → None(跳过) - `XXX_20260511000012345.zip` → 14位匹配 → 2026-05-11 ✓ 2. **就绪检测** - 所有目录都覆盖7天 → 就绪 - 某目录缺少1天 → 未就绪 - `expected_directories` 为空 → 只检测根目录 - 子目录不存在 → 跳过并记录警告 3. **调度流程** - 检测 → 写标识 → 触发处理 → 成功 → 删标识 → 循环 - 处理失败 → 保留标识 → 下次直接触发 4. **配置联动** - 开启自动调度 → `auto_delete_source` 自动变为 true - 修改配置 → 调度器热重载 5. **互斥** - 自动调度运行中 → 手动触发被拒绝 - 手动任务运行中 → 自动调度跳过本轮 --- ## 九、实施步骤与工时估算 | 步骤 | 内容 | 预估工时 | |------|------|----------| | 1 | `app/config.py`:扩展 `RemoteDataConfig`,新增 `AutoSchedulerConfig` | 20min | | 2 | `app/services/remote_download.py`:新增 `list_remote_zip_names()` | 30min | | 3 | `app/services/auto_scheduler.py`:新建调度器核心逻辑 | 2h | | 4 | `app/api/routers/remote.py`:新增状态查询和手动触发接口 | 30min | | 5 | `app/main.py`:集成调度器启动/停止 | 10min | | 6 | `frontend/src/components/SettingsPanel.vue`:自动调度配置 UI | 1.5h | | 7 | `frontend/src/api/client.ts`:新增 API 调用方法 | 10min | | 8 | 联调测试 | 1h | | **合计** | | **约6h** | --- ## 十、注意事项 1. **不改动现有处理流程**:自动调度只是在现有"远程下载并处理"之上加了一层检测和触发机制 2. **标识文件是运行时数据**:`cache/auto_scheduler/ready.flag` 不应提交到版本库 3. **`week_offset` 用途**:如果某周处理失败需要下周补跑,可将 `week_offset` 设为 `-1` 来检查上上周的数据 4. **配置热重载**:修改自动调度配置后,调度器应在下一次检查周期使用新配置(不需要重启服务) 5. **前端 `auto_delete_source` 联动**:当自动调度开启时,前端应将"处理成功后删除源文件"开关灰显为强制开启状态