Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a576b04cf | ||
| 486313044c | |||
| 84e907b93e | |||
| 5fd05fcd3c | |||
| f04478fd8f | |||
| f5ba5624aa | |||
| 071a3f05f3 | |||
| bb3f25a643 | |||
| 53eef800b4 | |||
| 1b688c1603 | |||
| 0cbc587bf3 | |||
| 9b19015156 | |||
| 929c4b836f |
@@ -5,5 +5,12 @@ CONFLUENCE_CONTENT_ID=155764524
|
|||||||
|
|
||||||
# 飞书表格配置
|
# 飞书表格配置
|
||||||
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
||||||
FEISHU_TOKEN=your-feishu-api-token
|
|
||||||
FEISHU_SPREADSHEET_TOKEN=EgNPssi2ghZ7BLtGiTxcIBUmnVh
|
FEISHU_SPREADSHEET_TOKEN=EgNPssi2ghZ7BLtGiTxcIBUmnVh
|
||||||
|
|
||||||
|
# 飞书应用凭证(推荐方式,自动获取tenant_access_token)
|
||||||
|
# 创建飞书自建应用后获取app_id和app_secret
|
||||||
|
FEISHU_APP_ID=your-feishu-app-id
|
||||||
|
FEISHU_APP_SECRET=your-feishu-app-secret
|
||||||
|
|
||||||
|
# 备选:手动配置token(不推荐,token会过期)
|
||||||
|
# FEISHU_TOKEN=your-feishu-api-token
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,6 +23,8 @@ debug/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
plans/
|
||||||
@@ -68,6 +68,7 @@ OrbitIn/
|
|||||||
- `get_ships_with_monthly_teu(year_month)` - 获取当月每艘船的作业量
|
- `get_ships_with_monthly_teu(year_month)` - 获取当月每艘船的作业量
|
||||||
- `insert_unaccounted(year_month, teu, note)` - 添加未统计数据
|
- `insert_unaccounted(year_month, teu, note)` - 添加未统计数据
|
||||||
- `get_unaccounted(year_month)` - 获取未统计数据
|
- `get_unaccounted(year_month)` - 获取未统计数据
|
||||||
|
- `delete_unaccounted(year_month)` - 去除未统计数据(对称功能)
|
||||||
|
|
||||||
### DailyReportGenerator (src/report.py:15)
|
### DailyReportGenerator (src/report.py:15)
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ OrbitIn/
|
|||||||
|
|
||||||
- tkinter 图形界面
|
- tkinter 图形界面
|
||||||
- 支持获取数据、生成日报、添加未统计数据
|
- 支持获取数据、生成日报、添加未统计数据
|
||||||
|
- 支持去除多余统计数据(对称功能)
|
||||||
- 日报内容可复制
|
- 日报内容可复制
|
||||||
|
|
||||||
### FeishuScheduleManager (src/feishu.py:150)
|
### FeishuScheduleManager (src/feishu.py:150)
|
||||||
@@ -118,6 +120,9 @@ python3 main.py parse-test
|
|||||||
# 添加未统计数据
|
# 添加未统计数据
|
||||||
python3 main.py --unaccounted 118 --month 2025-12
|
python3 main.py --unaccounted 118 --month 2025-12
|
||||||
|
|
||||||
|
# 去除未统计数据
|
||||||
|
python3 main.py --remove-unaccounted --month 2025-12
|
||||||
|
|
||||||
# GUI界面
|
# GUI界面
|
||||||
python3 src/gui.py
|
python3 src/gui.py
|
||||||
```
|
```
|
||||||
|
|||||||
323
README.md
323
README.md
@@ -1,17 +1,44 @@
|
|||||||
# OrbitIn - 码头作业日志管理系统
|
# OrbitIn
|
||||||
|
|
||||||
从 Confluence API 获取交接班日志,提取作业数据并生成统计报表。
|
码头作业日志管理工具。从 Confluence API 获取交接班日志,提取作业数据并生成统计报表。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 从 Confluence 获取交接班日志 HTML
|
- **数据获取**:从 Confluence API 获取交接班日志 HTML
|
||||||
- 提取保留布局的文本内容
|
- **文本提取**:提取保留布局的文本内容,支持表格格式化
|
||||||
- SQLite3 数据库存储
|
- **数据库存储**:SQLite3 数据库存储,支持二次靠泊记录合并
|
||||||
- 生成日报和月度统计
|
- **报表生成**:生成日报和月度统计,包含完成率计算
|
||||||
- 支持未统计数据手动录入
|
- **数据调整**:支持手动添加/剔除统计数据,月底数据自动转移
|
||||||
- 支持二次靠泊记录合并
|
- **智能调整**:月底最后一天自动询问剔除12点后数据,月初自动添加上月数据
|
||||||
- GUI 图形界面(可选)
|
- **GUI界面**:tkinter 图形界面,支持一键操作
|
||||||
- 飞书排班表集成(自动获取班次人员)
|
- **飞书集成**:自动获取排班人员信息,支持应用凭证自动刷新token
|
||||||
|
- **月份页面映射**:支持配置各月份的Confluence页面ID,解决每月页面ID变化问题
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install requests beautifulsoup4 python-dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env 文件,填入配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GUI 方式
|
||||||
|
python3 src/gui.py
|
||||||
|
|
||||||
|
# CLI 方式
|
||||||
|
python3 main.py fetch-save # 获取并保存数据
|
||||||
|
python3 main.py report 2025-12-28 # 生成日报
|
||||||
|
```
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -19,12 +46,8 @@
|
|||||||
OrbitIn/
|
OrbitIn/
|
||||||
├── main.py # CLI 入口
|
├── main.py # CLI 入口
|
||||||
├── README.md # 项目说明
|
├── README.md # 项目说明
|
||||||
├── AGENTS.md # AI助手开发文档
|
|
||||||
├── .env # 环境配置(敏感信息)
|
├── .env # 环境配置(敏感信息)
|
||||||
├── .env.example # 环境配置示例
|
├── .env.example # 环境配置示例
|
||||||
├── layout_output.txt # 缓存的布局文本
|
|
||||||
├── debug/ # 调试输出目录
|
|
||||||
│ └── layout_output_*.txt # 带时间戳的调试文件
|
|
||||||
├── data/ # 数据目录
|
├── data/ # 数据目录
|
||||||
│ ├── daily_logs.db # SQLite3 数据库
|
│ ├── daily_logs.db # SQLite3 数据库
|
||||||
│ └── schedule_cache.json # 排班数据缓存
|
│ └── schedule_cache.json # 排班数据缓存
|
||||||
@@ -45,174 +68,130 @@ OrbitIn/
|
|||||||
│ ├── parser.py # HTML 内容解析器
|
│ ├── parser.py # HTML 内容解析器
|
||||||
│ ├── text.py # HTML 文本提取器
|
│ ├── text.py # HTML 文本提取器
|
||||||
│ ├── log_parser.py # 日志解析器
|
│ ├── log_parser.py # 日志解析器
|
||||||
│ ├── manager.py # 内容管理器
|
│ └── manager.py # 内容管理器
|
||||||
│ └── __init__.py # 模块导出
|
|
||||||
└── feishu/ # 飞书 API 模块
|
└── feishu/ # 飞书 API 模块
|
||||||
├── client.py # 飞书 API 客户端
|
├── client.py # 飞书 API 客户端
|
||||||
├── parser.py # 排班数据解析器
|
├── parser.py # 排班数据解析器
|
||||||
├── manager.py # 飞书排班管理器
|
└── manager.py # 飞书排班管理器
|
||||||
└── __init__.py # 模块导出
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 核心模块
|
||||||
|
|
||||||
### 安装依赖
|
### ConfluenceClient (src/confluence/client.py)
|
||||||
|
|
||||||
```bash
|
- `fetch_content(content_id, expand)` - 获取页面内容
|
||||||
pip install requests beautifulsoup4 python-dotenv
|
- `get_html(content_id)` - 获取 HTML 字符串
|
||||||
```
|
|
||||||
|
|
||||||
### 配置
|
### HTMLTextExtractor (src/confluence/text.py)
|
||||||
|
|
||||||
|
- `extract(html)` - 从 HTML 提取保留布局的文本
|
||||||
|
- 使用 `html.parser`(非 lxml)
|
||||||
|
- 移除带 `ac:name` 属性的 Confluence 宏元素
|
||||||
|
- 表格格式化使用 `ljust()` 列对齐
|
||||||
|
|
||||||
|
### HandoverLogParser (src/confluence/log_parser.py)
|
||||||
|
|
||||||
|
- `parse(text)` - 解析日志文本,返回 `ShipLog` 列表
|
||||||
|
- 自动合并同日期同班次同船名的记录(二次靠泊)
|
||||||
|
- `ShipLog` 数据类:date, shift, ship_name, teu, efficiency, vehicles
|
||||||
|
|
||||||
|
### DailyLogsDatabase (src/database/daily_logs.py)
|
||||||
|
|
||||||
|
- `insert(log)` - 插入单条记录(存在则跳过)
|
||||||
|
- `insert_many(logs)` - 批量插入
|
||||||
|
- `query_by_date(date)` - 按日期查询
|
||||||
|
- `query_by_ship(ship_name)` - 按船名查询
|
||||||
|
- `query_all(limit)` - 查询所有
|
||||||
|
- `get_stats()` - 获取统计信息
|
||||||
|
- `get_ships_with_monthly_teu(year_month)` - 获取当月每艘船的作业量
|
||||||
|
- `insert_cross_month_exclusion()` - 跨月数据调整
|
||||||
|
- `insert_confluence_page()` - 保存月份页面ID映射
|
||||||
|
- `get_confluence_page_for_date()` - 获取指定日期对应的页面ID
|
||||||
|
|
||||||
|
### DailyReportGenerator (src/report.py)
|
||||||
|
|
||||||
|
- `generate_report(date)` - 生成日报
|
||||||
|
- `print_report(date)` - 打印日报
|
||||||
|
- `get_shift_personnel(date)` - 获取班次人员(从飞书排班表获取)
|
||||||
|
|
||||||
|
### OrbitInGUI (src/gui.py)
|
||||||
|
|
||||||
|
- tkinter 图形界面
|
||||||
|
- 支持获取数据、生成日报
|
||||||
|
- 支持手动剔除次月多统计的船
|
||||||
|
- 日报内容可复制
|
||||||
|
|
||||||
|
### FeishuScheduleManager (src/feishu/manager.py)
|
||||||
|
|
||||||
|
- `get_schedule_for_date(date)` - 获取指定日期的排班信息
|
||||||
|
- `get_schedule_for_today()` - 获取今天的排班信息
|
||||||
|
- `refresh_all_schedules(days)` - 批量刷新排班信息
|
||||||
|
|
||||||
|
## 文本格式约定
|
||||||
|
|
||||||
|
- 列表前缀:`•` 用于 `ul`,数字+点用于 `ol`
|
||||||
|
- 粗体使用 `**text**`,斜体使用 `*text*`
|
||||||
|
- 水平线使用 `─` (U+2500) 字符
|
||||||
|
- 链接渲染为 `text (url)`
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
在 `.env` 文件中配置:
|
在 `.env` 文件中配置:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env
|
|
||||||
# Confluence 配置
|
# Confluence 配置
|
||||||
CONFLUENCE_BASE_URL=https://your-confluence.atlassian.net/rest/api
|
CONFLUENCE_BASE_URL=https://confluence.westwell-lab.com/rest/api
|
||||||
CONFLUENCE_TOKEN=your-api-token
|
CONFLUENCE_TOKEN=your-api-token
|
||||||
CONFLUENCE_CONTENT_ID=155764524
|
CONFLUENCE_CONTENT_ID=155764524
|
||||||
|
|
||||||
# 飞书表格配置(用于获取排班人员信息)
|
# 飞书表格配置
|
||||||
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
||||||
FEISHU_TOKEN=your-feishu-api-token
|
FEISHU_TOKEN=your-feishu-api-token
|
||||||
FEISHU_SPREADSHEET_TOKEN=EgNPssi2ghZ7BLtGiTxcIBUmnVh
|
FEISHU_SPREADSHEET_TOKEN=EgNPssi2ghZ7BLtGiTxcIBUmnVh
|
||||||
|
FEISHU_APP_ID=your-feishu-app-id
|
||||||
# 数据库配置
|
FEISHU_APP_SECRET=your-feishu-app-secret
|
||||||
DATABASE_PATH=data/daily_logs.db
|
|
||||||
|
|
||||||
# 业务配置
|
# 业务配置
|
||||||
DAILY_TARGET_TEU=300 # 每日目标TEU数量,用于计算完成率
|
DAILY_TARGET_TEU=300
|
||||||
DUTY_PHONE=13107662315 # 值班电话,显示在日报中
|
DUTY_PHONE=13107662315
|
||||||
SEPARATOR_CHAR=─ # 分隔线字符,用于格式化输出
|
|
||||||
SEPARATOR_LENGTH=50 # 分隔线长度
|
|
||||||
SCHEDULE_REFRESH_DAYS=30 # 排班数据刷新间隔(天)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
参考 `.env.example` 文件创建 `.env` 文件。
|
## 命令
|
||||||
|
|
||||||
### 使用方法
|
|
||||||
|
|
||||||
#### 命令行方式
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 默认:获取、提取、解析并保存到数据库
|
# 默认:获取、提取、解析并保存到数据库
|
||||||
python3 main.py fetch-save
|
python3 main.py
|
||||||
|
|
||||||
# 仅获取HTML并提取文本(保存到debug目录)
|
# 仅获取HTML并提取文本
|
||||||
python3 main.py fetch
|
python3 main.py fetch
|
||||||
|
|
||||||
# 获取并保存带时间戳的debug文件
|
# 生成日报
|
||||||
python3 main.py fetch-debug
|
|
||||||
|
|
||||||
# 生成日报(指定日期)
|
|
||||||
python3 main.py report 2025-12-28
|
python3 main.py report 2025-12-28
|
||||||
|
|
||||||
# 生成今日日报
|
# 生成昨日日报
|
||||||
python3 main.py report-today
|
python3 main.py report-today
|
||||||
|
|
||||||
# 配置测试(验证所有连接)
|
# 手动剔除次月多统计的船
|
||||||
|
python3 main.py --cross-exclude --source-date 2025-12-31 --target-date 2026-01-01 --ship-name "学友洋山" --teu 100
|
||||||
|
|
||||||
|
# 配置测试
|
||||||
python3 main.py config-test
|
python3 main.py config-test
|
||||||
|
|
||||||
# 添加未统计数据
|
# GUI界面
|
||||||
python3 main.py --unaccounted 118 --month 2025-12
|
|
||||||
|
|
||||||
# 显示帮助
|
|
||||||
python3 main.py --help
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GUI 方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 src/gui.py
|
python3 src/gui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
GUI 功能:
|
## 测试模式
|
||||||
- 获取并处理数据
|
|
||||||
- 获取 (Debug模式)
|
|
||||||
- 生成日报
|
|
||||||
- 今日日报(自动获取前一天数据)
|
|
||||||
- 添加未统计数据
|
|
||||||
- 数据库统计(显示当月每艘船的作业量)
|
|
||||||
- 日报内容可复制
|
|
||||||
- 自动刷新排班信息
|
|
||||||
|
|
||||||
## 数据格式
|
如果设置了环境变量 `DEBUG_MODE=true`,系统会使用本地 `layout_output.txt` 文件而不是从 Confluence API 获取数据,方便离线测试。
|
||||||
|
|
||||||
### 日报表 (daily_handover_logs)
|
## 注意事项
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
1. 二次靠泊记录会在解析时自动合并
|
||||||
|------|------|------|
|
2. 重复获取数据不会累加TEU(会跳过已存在的记录)
|
||||||
| id | INTEGER | 主键 |
|
3. 未统计数据在报表中不显示,但会计算到当月实际作业量
|
||||||
| date | TEXT | 日期 YYYY-MM-DD |
|
4. 昨日日报按钮默认获取前一天的数据(因为通常在第二天汇报)
|
||||||
| shift | TEXT | 班次 (白班/夜班) |
|
5. 月底数据调整适用于**每个月最后一天**,而不仅限12月
|
||||||
| ship_name | TEXT | 船名(不含船号前缀) |
|
|
||||||
| teu | INTEGER | 作业量 TEU |
|
|
||||||
| efficiency | REAL | 效率 |
|
|
||||||
| vehicles | INTEGER | 上场车辆数 |
|
|
||||||
| created_at | TEXT | 创建时间 |
|
|
||||||
|
|
||||||
### 未统计表 (monthly_unaccounted)
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | INTEGER | 主键 |
|
|
||||||
| year_month | TEXT | 年月 YYYY-MM |
|
|
||||||
| teu | INTEGER | 未统计的 TEU |
|
|
||||||
| note | TEXT | 备注 |
|
|
||||||
| created_at | TEXT | 创建时间 |
|
|
||||||
|
|
||||||
## 特性说明
|
|
||||||
|
|
||||||
### 二次靠泊合并
|
|
||||||
|
|
||||||
解析时会自动合并同一天的二次靠泊记录:
|
|
||||||
|
|
||||||
- 夜班 学友洋山: 273TEU
|
|
||||||
- 夜班 学友洋山(二次靠泊): 14TEU
|
|
||||||
- 合并后: 夜班 学友洋山: 287TEU
|
|
||||||
|
|
||||||
### 未统计数据
|
|
||||||
|
|
||||||
可以在数据库统计中查看当月每艘船的作业量总计,便于跟踪船舶运营情况。
|
|
||||||
|
|
||||||
## 示例输出
|
|
||||||
|
|
||||||
```
|
|
||||||
日期:12/28
|
|
||||||
|
|
||||||
船名:学友洋山
|
|
||||||
作业量:246TEU
|
|
||||||
|
|
||||||
当日实际作业量:246TEU
|
|
||||||
|
|
||||||
当月计划作业量:8400TEU (用天数*300TEU)
|
|
||||||
当月实际作业量:12632TEU
|
|
||||||
当月完成比例:150.38%
|
|
||||||
|
|
||||||
12/29 白班人员:
|
|
||||||
12/29 夜班人员:
|
|
||||||
24小时值班手机:13107662315
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心模块说明
|
|
||||||
|
|
||||||
### Confluence 模块 (`src/confluence/`)
|
|
||||||
- **`client.py`** - Confluence API 客户端,负责 HTTP 请求和连接管理
|
|
||||||
- **`text.py`** - HTML 文本提取器,保留布局结构
|
|
||||||
- **`log_parser.py`** - 日志解析器,解析船次作业数据
|
|
||||||
- **`parser.py`** - HTML 内容解析器,提取链接、图片、表格
|
|
||||||
- **`manager.py`** - 内容管理器,提供高级内容管理功能
|
|
||||||
|
|
||||||
### 飞书模块 (`src/feishu/`)
|
|
||||||
- **`client.py`** - 飞书 API 客户端
|
|
||||||
- **`parser.py`** - 排班数据解析器
|
|
||||||
- **`manager.py`** - 飞书排班管理器,缓存和刷新排班信息
|
|
||||||
|
|
||||||
### 数据库模块 (`src/database/`)
|
|
||||||
- **`base.py`** - 数据库基类,提供统一的连接管理
|
|
||||||
- **`daily_logs.py`** - 每日交接班日志数据库
|
|
||||||
- **`schedules.py`** - 排班数据库
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -220,65 +199,9 @@ GUI 功能:
|
|||||||
- SQLite3
|
- SQLite3
|
||||||
- Requests (HTTP 客户端)
|
- Requests (HTTP 客户端)
|
||||||
- BeautifulSoup4 (HTML 解析)
|
- BeautifulSoup4 (HTML 解析)
|
||||||
- tkinter (GUI,可选)
|
- tkinter (GUI)
|
||||||
- 类型提示 (Python 3.5+)
|
- 类型提示
|
||||||
|
|
||||||
## 架构特点
|
## 许可证
|
||||||
|
|
||||||
1. **模块化设计** - 每个模块职责单一,便于测试和维护
|
|
||||||
2. **统一配置** - 集中管理所有环境变量和业务配置
|
|
||||||
3. **统一日志** - 标准化的日志配置和文件轮转
|
|
||||||
4. **异常处理** - 详细的错误处理和日志记录
|
|
||||||
5. **类型安全** - 全面的 Python 类型提示
|
|
||||||
|
|
||||||
## 开发指南
|
|
||||||
|
|
||||||
### 添加新功能
|
|
||||||
|
|
||||||
1. **配置管理**: 所有配置项应在 `src/config.py` 中定义
|
|
||||||
2. **日志记录**: 使用 `from src.logging_config import get_logger` 获取日志器
|
|
||||||
3. **异常处理**: 为每个模块创建自定义异常类
|
|
||||||
4. **类型提示**: 所有函数和方法都应包含完整的类型提示
|
|
||||||
5. **数据库操作**: 使用 `src/database/base.py` 中的基类确保连接管理
|
|
||||||
|
|
||||||
### 测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 运行配置测试
|
|
||||||
python3 main.py config-test
|
|
||||||
|
|
||||||
# 测试特定功能
|
|
||||||
python3 main.py fetch
|
|
||||||
python3 main.py report-today
|
|
||||||
```
|
|
||||||
|
|
||||||
### 调试
|
|
||||||
|
|
||||||
1. **日志查看**: 查看 `logs/app.log` 获取详细运行信息
|
|
||||||
2. **调试文件**: 使用 `python3 main.py fetch-debug` 生成带时间戳的调试文件
|
|
||||||
|
|
||||||
### 代码规范
|
|
||||||
|
|
||||||
- 遵循 PEP 8 编码规范
|
|
||||||
- 使用 Black 格式化代码(可选)
|
|
||||||
- 使用 isort 排序导入
|
|
||||||
- 所有公开 API 应有文档字符串
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
1. **连接失败**: 检查 `.env` 文件中的 API 令牌和 URL
|
|
||||||
2. **数据库错误**: 确保 `data/` 目录存在且有写入权限
|
|
||||||
3. **解析错误**: 检查 Confluence 页面结构是否发生变化
|
|
||||||
4. **飞书数据获取失败**: 验证飞书表格权限和 token 有效性
|
|
||||||
|
|
||||||
### 日志级别
|
|
||||||
|
|
||||||
- 默认日志级别: INFO
|
|
||||||
- 调试日志级别: DEBUG (设置环境变量 `LOG_LEVEL=DEBUG`)
|
|
||||||
- 日志文件: `logs/app.log`,自动轮转
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
665
logs/2026-01/2026-01-04.log
Normal file
665
logs/2026-01/2026-01-04.log
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
2026-01-04 01:09:29 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-04 01:09:29 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-04 01:09:29 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-04 01:09:29 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-04 01:09:29 - root - INFO - <string>:10 - 测试日期分片日志 - 时间: 2026-01-04 01:09:29
|
||||||
|
2026-01-04 01:09:29 - root - DEBUG - <string>:11 - Debug级别测试
|
||||||
|
2026-01-04 01:09:29 - root - WARNING - <string>:12 - Warning级别测试
|
||||||
|
2026-01-04 01:10:13 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-04 01:10:13 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-04 01:10:13 - __main__ - INFO - gui.py:614 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-04 01:10:13 - __main__ - INFO - gui.py:627 - 正在刷新排班信息...
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:10:13 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:10:13 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:13 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:230 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-04 的排班信息...
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-05 的排班信息...
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-05 的排班信息 (格式: 01/05/1月5日)
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:13 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-05 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:13 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-04 01:10:14 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 4411秒
|
||||||
|
2026-01-04 01:10:14 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 4411 秒后过期
|
||||||
|
2026-01-04 01:10:14 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:14 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:14 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4410秒
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月5日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月5日 -> 1月5日 (索引: 5)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-05
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-05 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-06 的排班信息...
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-06 的排班信息 (格式: 01/06/1月6日)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-06 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4409秒
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4409秒
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月6日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月6日 -> 1月6日 (索引: 6)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-06
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-06 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-07 的排班信息...
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-07 的排班信息 (格式: 01/07/1月7日)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-07 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4409秒
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4409秒
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月7日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月7日 -> 1月7日 (索引: 7)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-07
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-07 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-08 的排班信息...
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-08 的排班信息 (格式: 01/08/1月8日)
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:15 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-08 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4409秒
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4408秒
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月8日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月8日 -> 1月8日 (索引: 8)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-08
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-08 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-09 的排班信息...
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-09 的排班信息 (格式: 01/09/1月9日)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-09 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4408秒
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4408秒
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月9日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月9日 -> 1月9日 (索引: 9)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-09
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-09 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-10 的排班信息...
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-10 的排班信息 (格式: 01/10/1月10日)
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:16 - src.feishu.manager - INFO - manager.py:145 - 数据库中没有 2026-01-10 的排班信息,从飞书获取
|
||||||
|
2026-01-04 01:10:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4407秒
|
||||||
|
2026-01-04 01:10:17 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-04 01:10:17 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 4407秒
|
||||||
|
2026-01-04 01:10:17 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.schedules - DEBUG - schedules.py:144 - 表格无更新: 2026年排班表 (ID: R35cIj)
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.feishu.manager - INFO - manager.py:192 - 使用日期格式: 1月10日 解析表格: 2026年排班表
|
||||||
|
2026-01-04 01:10:17 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-04 01:10:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-04 01:10:17 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月10日 -> 1月10日 (索引: 10)
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-10
|
||||||
|
2026-01-04 01:10:17 - src.feishu.manager - INFO - manager.py:198 - 已保存 2026-01-10 的排班信息到数据库
|
||||||
|
2026-01-04 01:10:17 - src.feishu.manager - INFO - manager.py:246 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:632 - 排班信息刷新完成
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:648 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:673 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-04 01:10:17 - src.confluence.client - DEBUG - client.py:48 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-04 01:10:17 - src.confluence.client - DEBUG - client.py:69 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:10:17 - src.confluence.client - INFO - client.py:74 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:10:17 - src.confluence.client - INFO - client.py:115 - 获取到Confluence HTML内容,长度: 11084 字符
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:679 - 获取成功,共 11084 字符
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:683 - 正在提取布局文本...
|
||||||
|
2026-01-04 01:10:17 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 11084 字符
|
||||||
|
2026-01-04 01:10:17 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 2104 字符
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:689 - 正在解析日志数据...
|
||||||
|
2026-01-04 01:10:17 - src.confluence.log_parser - INFO - log_parser.py:356 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-04 01:10:17 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 14 条记录
|
||||||
|
2026-01-04 01:10:17 - __main__ - INFO - gui.py:696 - 正在保存到数据库...
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 金祥源
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 东方吉
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 转堆作业
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 学友洋山
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 嘉洋16
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 泽远
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 华信长和
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 华信长和
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 瀚旺
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 金祥源
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:17 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 弘旭968
|
||||||
|
2026-01-04 01:10:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 东方祥
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 学友洋山
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 夜班 学友洋山
|
||||||
|
2026-01-04 01:10:18 - src.database.daily_logs - INFO - daily_logs.py:202 - 批量插入完成,成功 14/14 条记录
|
||||||
|
2026-01-04 01:10:18 - __main__ - INFO - gui.py:700 - 已保存 14 条新记录
|
||||||
|
2026-01-04 01:10:18 - __main__ - INFO - gui.py:728 - 正在生成今日日报...
|
||||||
|
2026-01-04 01:10:18 - __main__ - INFO - gui.py:572 - 生成 2026-01-03 的日报...
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:10:18 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:10:18 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:10:18 - src.report - INFO - report.py:235 - 获取 2026-01-03 日报的班次人员,对应排班表日期: 2026-01-04
|
||||||
|
2026-01-04 01:10:18 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:10:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:10:18 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:10:18 - src.report - INFO - report.py:340 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:10:18 - __main__ - INFO - gui.py:589 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:10:18 - __main__ - INFO - gui.py:733 - 自动获取完成,GUI已就绪
|
||||||
|
2026-01-04 01:14:19 - __main__ - INFO - gui.py:253 - 开始获取数据...
|
||||||
|
2026-01-04 01:14:19 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:19 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:14:19 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:19 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:19 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:19 - __main__ - INFO - gui.py:282 - 使用页面ID映射: 2026-01-03 -> 159049182
|
||||||
|
2026-01-04 01:14:19 - __main__ - INFO - gui.py:287 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-04 01:14:19 - src.confluence.client - DEBUG - client.py:48 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-04 01:14:19 - src.confluence.client - DEBUG - client.py:69 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:14:20 - src.confluence.client - INFO - client.py:74 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:14:20 - src.confluence.client - INFO - client.py:115 - 获取到Confluence HTML内容,长度: 12190 字符
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:297 - 获取成功,共 12190 字符
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:301 - 正在提取布局文本...
|
||||||
|
2026-01-04 01:14:20 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 12190 字符
|
||||||
|
2026-01-04 01:14:20 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 2312 字符
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:305 - 提取完成,共 2311 字符
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:309 - 正在解析日志数据...
|
||||||
|
2026-01-04 01:14:20 - src.confluence.log_parser - INFO - log_parser.py:356 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-04 01:14:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 16 条记录
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:313 - 解析到 16 条记录
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:318 - 正在保存到数据库...
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 金祥源
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 东方吉
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 德盛6
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 海顺丰7
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 转堆作业
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 学友洋山
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 嘉洋16
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 泽远
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 华信长和
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 华信长和
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 瀚旺
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 金祥源
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 弘旭968
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 东方祥
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 学友洋山
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 夜班 学友洋山
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - INFO - daily_logs.py:202 - 批量插入完成,成功 16/16 条记录
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:322 - 已保存 16 条记录
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:327 - 数据库总计: 147 条记录, 30 艘船
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:572 - 生成 2026-01-03 的日报...
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:14:20 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:14:20 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:14:20 - src.report - INFO - report.py:235 - 获取 2026-01-03 日报的班次人员,对应排班表日期: 2026-01-04
|
||||||
|
2026-01-04 01:14:20 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:14:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:14:20 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:14:20 - src.report - INFO - report.py:340 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:589 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:14:20 - __main__ - INFO - gui.py:336 - 数据获取完成
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:253 - 开始获取数据...
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:282 - 使用页面ID映射: 2026-01-03 -> 159049182
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:287 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-04 01:16:15 - src.confluence.client - DEBUG - client.py:48 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-04 01:16:15 - src.confluence.client - DEBUG - client.py:69 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:16:15 - src.confluence.client - INFO - client.py:74 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:16:15 - src.confluence.client - INFO - client.py:115 - 获取到Confluence HTML内容,长度: 12190 字符
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:297 - 获取成功,共 12190 字符
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:301 - 正在提取布局文本...
|
||||||
|
2026-01-04 01:16:15 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 12190 字符
|
||||||
|
2026-01-04 01:16:15 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 2312 字符
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:305 - 提取完成,共 2311 字符
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:309 - 正在解析日志数据...
|
||||||
|
2026-01-04 01:16:15 - src.confluence.log_parser - INFO - log_parser.py:356 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-04 01:16:15 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 16 条记录
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:313 - 解析到 16 条记录
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:318 - 正在保存到数据库...
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 金祥源
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 东方吉
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 德盛6
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 海顺丰7
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 转堆作业
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 学友洋山
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 嘉洋16
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 泽远
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 华信长和
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 华信长和
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 瀚旺
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 金祥源
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 弘旭968
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 东方祥
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 学友洋山
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 夜班 学友洋山
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - INFO - daily_logs.py:202 - 批量插入完成,成功 16/16 条记录
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:322 - 已保存 16 条记录
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:327 - 数据库总计: 147 条记录, 30 艘船
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:572 - 生成 2026-01-03 的日报...
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:16:15 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:16:15 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:16:15 - src.report - INFO - report.py:235 - 获取 2026-01-03 日报的班次人员,对应排班表日期: 2026-01-04
|
||||||
|
2026-01-04 01:16:15 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:16:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:16:15 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:16:15 - src.report - INFO - report.py:340 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:589 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:16:15 - __main__ - INFO - gui.py:336 - 数据获取完成
|
||||||
|
2026-01-04 01:17:20 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-04 01:17:20 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-04 01:17:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 1 条记录
|
||||||
|
2026-01-04 01:17:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 1 条记录
|
||||||
|
2026-01-04 01:17:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 1 条记录
|
||||||
|
2026-01-04 01:17:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 1 条记录
|
||||||
|
2026-01-04 01:17:53 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-04 01:17:53 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:614 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:627 - 正在刷新排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:17:53 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:17:53 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:230 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-04 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-05 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-05 的排班信息 (格式: 01/05/1月5日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-05 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-06 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-06 的排班信息 (格式: 01/06/1月6日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-06 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-07 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-07 的排班信息 (格式: 01/07/1月7日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-07 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-08 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-08 的排班信息 (格式: 01/08/1月8日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-08 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-09 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-09 的排班信息 (格式: 01/09/1月9日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-09 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - DEBUG - manager.py:239 - 刷新 2026-01-10 的排班信息...
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-10 的排班信息 (格式: 01/10/1月10日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-10 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:246 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:632 - 排班信息刷新完成
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:648 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:673 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-04 01:17:53 - src.confluence.client - DEBUG - client.py:48 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-04 01:17:53 - src.confluence.client - DEBUG - client.py:69 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:17:53 - src.confluence.client - INFO - client.py:74 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-04 01:17:53 - src.confluence.client - INFO - client.py:115 - 获取到Confluence HTML内容,长度: 12190 字符
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:679 - 获取成功,共 12190 字符
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:683 - 正在提取布局文本...
|
||||||
|
2026-01-04 01:17:53 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 12190 字符
|
||||||
|
2026-01-04 01:17:53 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 2312 字符
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:689 - 正在解析日志数据...
|
||||||
|
2026-01-04 01:17:53 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-04 01:17:53 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 16 条记录
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:696 - 正在保存到数据库...
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 金祥源
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 白班 东方吉
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 德盛6
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-03 夜班 海顺丰7
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 转堆作业
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 学友洋山
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 嘉洋16
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 泽远
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 白班 华信长和
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 华信长和
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 瀚旺
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-02 夜班 金祥源
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 弘旭968
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 东方祥
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 白班 学友洋山
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:180 - 插入记录: 2026-01-01 夜班 学友洋山
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - INFO - daily_logs.py:202 - 批量插入完成,成功 16/16 条记录
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:700 - 已保存 16 条新记录
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:728 - 正在生成今日日报...
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:572 - 生成 2026-01-03 的日报...
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.daily_logs - DEBUG - daily_logs.py:155 - 数据库表结构初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-04 01:17:53 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-04 01:17:53 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-04 01:17:53 - src.report - INFO - report.py:235 - 获取 2026-01-03 日报的班次人员,对应排班表日期: 2026-01-04
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:136 - 获取 2026-01-04 的排班信息 (格式: 01/04/1月4日)
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-04 01:17:53 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-04 01:17:53 - src.feishu.manager - INFO - manager.py:141 - 从数据库获取 2026-01-04 的排班信息
|
||||||
|
2026-01-04 01:17:53 - src.report - INFO - report.py:340 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:589 - 日报生成完成: 2026-01-03
|
||||||
|
2026-01-04 01:17:53 - __main__ - INFO - gui.py:733 - 自动获取完成,GUI已就绪
|
||||||
180
main.py
180
main.py
@@ -189,6 +189,70 @@ def add_unaccounted(year_month: str, teu: int, note: str = ''):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def remove_unaccounted(year_month: str, teu_to_reduce: int = None):
|
||||||
|
"""
|
||||||
|
去除未统计数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
year_month: 年月字符串,格式 "2025-12"
|
||||||
|
teu_to_reduce: 要减少的TEU数量,如果为None则删除整个记录
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
|
||||||
|
if teu_to_reduce is None:
|
||||||
|
# 如果没有指定减少数量,则删除整个记录
|
||||||
|
result = db.delete_unaccounted(year_month)
|
||||||
|
if result:
|
||||||
|
logger.info(f"已删除 {year_month} 月未统计数据")
|
||||||
|
else:
|
||||||
|
logger.error("删除失败")
|
||||||
|
else:
|
||||||
|
# 减少指定数量的TEU
|
||||||
|
result = db.reduce_unaccounted(year_month, teu_to_reduce)
|
||||||
|
if result:
|
||||||
|
logger.info(f"已减少 {year_month} 月未统计数据: {teu_to_reduce}TEU")
|
||||||
|
else:
|
||||||
|
logger.error("减少失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"去除未统计数据失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def add_cross_month_exclusion(source_date: str, target_date: str, ship_name: str, teu: int,
|
||||||
|
twenty_feet: int = 0, forty_feet: int = 0, reason: str = ''):
|
||||||
|
"""
|
||||||
|
添加跨月剔除调整(手动剔除次月多统计的船)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
source_date: 源日期(上月底日期)
|
||||||
|
target_date: 目标日期(次月日期)
|
||||||
|
ship_name: 船名
|
||||||
|
teu: TEU数量
|
||||||
|
twenty_feet: 20尺箱量
|
||||||
|
forty_feet: 40尺箱量
|
||||||
|
reason: 调整原因
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
success = db.insert_cross_month_exclusion(
|
||||||
|
source_date=source_date,
|
||||||
|
target_date=target_date,
|
||||||
|
ship_name=ship_name,
|
||||||
|
teu=teu,
|
||||||
|
twenty_feet=twenty_feet,
|
||||||
|
forty_feet=forty_feet,
|
||||||
|
reason=reason
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
logger.info(f"已添加跨月剔除调整: {source_date} -> {target_date} {ship_name} {teu}TEU")
|
||||||
|
else:
|
||||||
|
logger.error("添加跨月剔除调整失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"添加跨月剔除调整失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def show_stats(date: str):
|
def show_stats(date: str):
|
||||||
"""
|
"""
|
||||||
显示指定日期的统计
|
显示指定日期的统计
|
||||||
@@ -288,11 +352,30 @@ def main():
|
|||||||
config-test 配置测试
|
config-test 配置测试
|
||||||
stats 显示今日统计
|
stats 显示今日统计
|
||||||
|
|
||||||
|
参数:
|
||||||
|
--unaccounted, -u TEU 添加未统计数据(需同时指定月份)
|
||||||
|
--remove-unaccounted, -r [TEU] 去除未统计数据(需同时指定月份)。如果指定TEU值,则减少该数量;如果不指定,则删除整个记录
|
||||||
|
--month, -m YEAR-MONTH 指定月份(与 -u 或 -r 配合使用)
|
||||||
|
--cross-exclude, -c 手动剔除次月多统计的船(需指定源日期、目标日期、船名和TEU)
|
||||||
|
|
||||||
|
跨月剔除参数:
|
||||||
|
--source-date DATE 源日期(上月底日期),格式: YYYY-MM-DD
|
||||||
|
--target-date DATE 目标日期(次月日期),格式: YYYY-MM-DD
|
||||||
|
--ship-name NAME 船名
|
||||||
|
--teu TEU TEU数量
|
||||||
|
--twenty-feet COUNT 20尺箱量(可选,默认0)
|
||||||
|
--forty-feet COUNT 40尺箱量(可选,默认0)
|
||||||
|
--reason REASON 调整原因(可选)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
python3 main.py fetch
|
python3 main.py fetch
|
||||||
python3 main.py fetch-save
|
python3 main.py fetch-save
|
||||||
python3 main.py report 2025-12-28
|
python3 main.py report 2025-12-28
|
||||||
python3 main.py config-test
|
python3 main.py config-test
|
||||||
|
python3 main.py --unaccounted 118 --month 2025-12
|
||||||
|
python3 main.py --remove-unaccounted --month 2025-12 # 删除整个记录
|
||||||
|
python3 main.py --remove-unaccounted 118 --month 2025-12 # 减少118TEU
|
||||||
|
python3 main.py --cross-exclude --source-date 2025-12-31 --target-date 2026-01-01 --ship-name "学友洋山" --teu 100
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -314,11 +397,67 @@ def main():
|
|||||||
type=int,
|
type=int,
|
||||||
help='添加未统计数据(需同时指定月份,如 -u 118 2025-12)'
|
help='添加未统计数据(需同时指定月份,如 -u 118 2025-12)'
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--remove-unaccounted',
|
||||||
|
'-r',
|
||||||
|
metavar='TEU',
|
||||||
|
nargs='?',
|
||||||
|
const=None,
|
||||||
|
type=int,
|
||||||
|
help='去除未统计数据(需同时指定月份,如 -r 118 2025-12)。如果指定TEU值,则减少该数量;如果不指定,则删除整个记录'
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--month',
|
'--month',
|
||||||
'-m',
|
'-m',
|
||||||
metavar='YEAR-MONTH',
|
metavar='YEAR-MONTH',
|
||||||
help='指定月份(与 --unaccounted 配合使用)'
|
help='指定月份(与 --unaccounted 或 --remove-unaccounted 配合使用)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--cross-exclude',
|
||||||
|
'-c',
|
||||||
|
action='store_true',
|
||||||
|
help='手动剔除次月多统计的船'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--source-date',
|
||||||
|
metavar='DATE',
|
||||||
|
help='源日期(上月底日期),格式: YYYY-MM-DD'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--target-date',
|
||||||
|
metavar='DATE',
|
||||||
|
help='目标日期(次月日期),格式: YYYY-MM-DD'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--ship-name',
|
||||||
|
metavar='NAME',
|
||||||
|
help='船名'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--teu',
|
||||||
|
metavar='TEU',
|
||||||
|
type=int,
|
||||||
|
help='TEU数量'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--twenty-feet',
|
||||||
|
metavar='COUNT',
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help='20尺箱量(可选,默认0)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--forty-feet',
|
||||||
|
metavar='COUNT',
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help='40尺箱量(可选,默认0)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--reason',
|
||||||
|
metavar='REASON',
|
||||||
|
default='手动剔除次月多统计的船',
|
||||||
|
help='调整原因(可选,默认: "手动剔除次月多统计的船")'
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -333,6 +472,45 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 去除未统计数据
|
||||||
|
# 检查是否提供了 --remove-unaccounted 或 -r 参数
|
||||||
|
has_remove_arg = any(arg in sys.argv for arg in ['--remove-unaccounted', '-r'])
|
||||||
|
if has_remove_arg:
|
||||||
|
year_month = args.month or datetime.now().strftime('%Y-%m')
|
||||||
|
try:
|
||||||
|
# args.remove_unaccounted 可能是整数(指定TEU)或 None(未指定)
|
||||||
|
if isinstance(args.remove_unaccounted, int):
|
||||||
|
# 指定了TEU值,减少指定数量
|
||||||
|
remove_unaccounted(year_month, args.remove_unaccounted)
|
||||||
|
else:
|
||||||
|
# 未指定TEU值,删除整个记录
|
||||||
|
remove_unaccounted(year_month)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"去除未统计数据失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 跨月剔除功能
|
||||||
|
if args.cross_exclude:
|
||||||
|
if not all([args.source_date, args.target_date, args.ship_name, args.teu]):
|
||||||
|
logger.error("跨月剔除功能需要指定以下参数: --source-date, --target-date, --ship-name, --teu")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
add_cross_month_exclusion(
|
||||||
|
source_date=args.source_date,
|
||||||
|
target_date=args.target_date,
|
||||||
|
ship_name=args.ship_name,
|
||||||
|
teu=args.teu,
|
||||||
|
twenty_feet=args.twenty_feet,
|
||||||
|
forty_feet=args.forty_feet,
|
||||||
|
reason=args.reason
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"跨月剔除失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
return
|
||||||
|
|
||||||
# 执行功能
|
# 执行功能
|
||||||
try:
|
try:
|
||||||
if args.function == 'report' and args.date:
|
if args.function == 'report' and args.date:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
OrbitIn - Confluence 日志抓取与处理工具包
|
OrbitIn - Confluence 日志抓取与处理工具包
|
||||||
"""
|
"""
|
||||||
from .confluence import ConfluenceClient, HTMLTextExtractor, HandoverLogParser
|
from .confluence import ConfluenceClient, HTMLTextExtractor, HandoverLogParser
|
||||||
from .database import DailyLogsDatabase
|
from .database import DailyLogsDatabase
|
||||||
|
|
||||||
__version__ = '1.0.0'
|
__version__ = '1.0.0'
|
||||||
__all__ = ['ConfluenceClient', 'HTMLTextExtractor', 'HandoverLogParser', 'DailyLogsDatabase']
|
__all__ = ['ConfluenceClient', 'HTMLTextExtractor', 'HandoverLogParser', 'DailyLogsDatabase']
|
||||||
|
|||||||
237
src/config.py
237
src/config.py
@@ -1,107 +1,132 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
统一配置模块
|
统一配置模块
|
||||||
集中管理所有配置项,避免硬编码
|
集中管理所有配置项,避免硬编码
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# 加载环境变量
|
# 加载环境变量
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""应用配置类"""
|
"""应用配置类"""
|
||||||
|
|
||||||
# Confluence 配置
|
# Confluence 配置
|
||||||
CONFLUENCE_BASE_URL = os.getenv('CONFLUENCE_BASE_URL')
|
CONFLUENCE_BASE_URL = os.getenv('CONFLUENCE_BASE_URL')
|
||||||
CONFLUENCE_TOKEN = os.getenv('CONFLUENCE_TOKEN')
|
CONFLUENCE_TOKEN = os.getenv('CONFLUENCE_TOKEN')
|
||||||
CONFLUENCE_CONTENT_ID = os.getenv('CONFLUENCE_CONTENT_ID')
|
CONFLUENCE_CONTENT_ID = os.getenv('CONFLUENCE_CONTENT_ID')
|
||||||
|
|
||||||
# 飞书配置
|
# 飞书配置
|
||||||
FEISHU_BASE_URL = os.getenv('FEISHU_BASE_URL', 'https://open.feishu.cn/open-apis/sheets/v3')
|
FEISHU_BASE_URL = os.getenv('FEISHU_BASE_URL', 'https://open.feishu.cn/open-apis/sheets/v3')
|
||||||
FEISHU_TOKEN = os.getenv('FEISHU_TOKEN')
|
FEISHU_TOKEN = os.getenv('FEISHU_TOKEN')
|
||||||
FEISHU_SPREADSHEET_TOKEN = os.getenv('FEISHU_SPREADSHEET_TOKEN')
|
FEISHU_SPREADSHEET_TOKEN = os.getenv('FEISHU_SPREADSHEET_TOKEN')
|
||||||
|
FEISHU_APP_ID = os.getenv('FEISHU_APP_ID')
|
||||||
# 数据库配置
|
FEISHU_APP_SECRET = os.getenv('FEISHU_APP_SECRET')
|
||||||
DATABASE_PATH = os.getenv('DATABASE_PATH', 'data/daily_logs.db')
|
|
||||||
SCHEDULE_DATABASE_PATH = os.getenv('SCHEDULE_DATABASE_PATH', 'data/daily_logs.db')
|
# 数据库配置
|
||||||
|
DATABASE_PATH = os.getenv('DATABASE_PATH', 'data/daily_logs.db')
|
||||||
# 业务配置
|
SCHEDULE_DATABASE_PATH = os.getenv('SCHEDULE_DATABASE_PATH', 'data/daily_logs.db')
|
||||||
DAILY_TARGET_TEU = int(os.getenv('DAILY_TARGET_TEU', '300'))
|
|
||||||
DUTY_PHONE = os.getenv('DUTY_PHONE', '13107662315')
|
# 业务配置
|
||||||
|
DAILY_TARGET_TEU = int(os.getenv('DAILY_TARGET_TEU', '300'))
|
||||||
# 缓存配置
|
DUTY_PHONE = os.getenv('DUTY_PHONE', '13107662315')
|
||||||
CACHE_TTL = int(os.getenv('CACHE_TTL', '3600')) # 1小时
|
|
||||||
SCHEDULE_CACHE_FILE = os.getenv('SCHEDULE_CACHE_FILE', 'data/schedule_cache.json')
|
# 缓存配置
|
||||||
|
CACHE_TTL = int(os.getenv('CACHE_TTL', '3600')) # 1小时
|
||||||
# 调试目录配置
|
SCHEDULE_CACHE_FILE = os.getenv('SCHEDULE_CACHE_FILE', 'data/schedule_cache.json')
|
||||||
DEBUG_DIR = os.getenv('DEBUG_DIR', 'debug')
|
|
||||||
|
# 调试目录配置
|
||||||
# 飞书表格配置
|
DEBUG_DIR = os.getenv('DEBUG_DIR', 'debug')
|
||||||
SHEET_RANGE = os.getenv('SHEET_RANGE', 'A:AF')
|
|
||||||
REQUEST_TIMEOUT = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
# 飞书表格配置
|
||||||
|
SHEET_RANGE = os.getenv('SHEET_RANGE', 'A:AF')
|
||||||
# GUI 配置
|
REQUEST_TIMEOUT = int(os.getenv('REQUEST_TIMEOUT', '30'))
|
||||||
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
|
||||||
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
# GUI 配置
|
||||||
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '900x700')
|
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
||||||
|
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
||||||
# 排班刷新配置
|
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '1600x900')
|
||||||
SCHEDULE_REFRESH_DAYS = int(os.getenv('SCHEDULE_REFRESH_DAYS', '30'))
|
|
||||||
|
# 排班刷新配置
|
||||||
# 特殊常量
|
SCHEDULE_REFRESH_DAYS = int(os.getenv('SCHEDULE_REFRESH_DAYS', '30'))
|
||||||
FIRST_DAY_OF_MONTH_SPECIAL = 1
|
|
||||||
SEPARATOR_CHAR = '─'
|
# 特殊常量
|
||||||
SEPARATOR_LENGTH = 50
|
FIRST_DAY_OF_MONTH_SPECIAL = 1
|
||||||
|
SEPARATOR_CHAR = '─'
|
||||||
@classmethod
|
SEPARATOR_LENGTH = 50
|
||||||
def validate(cls) -> bool:
|
|
||||||
"""验证必要配置是否完整"""
|
@classmethod
|
||||||
errors = []
|
def validate(cls) -> bool:
|
||||||
|
"""验证必要配置是否完整"""
|
||||||
# 检查 Confluence 配置
|
errors = []
|
||||||
if not cls.CONFLUENCE_BASE_URL:
|
|
||||||
errors.append("CONFLUENCE_BASE_URL 未配置")
|
# 检查 Confluence 配置
|
||||||
if not cls.CONFLUENCE_TOKEN:
|
if not cls.CONFLUENCE_BASE_URL:
|
||||||
errors.append("CONFLUENCE_TOKEN 未配置")
|
errors.append("CONFLUENCE_BASE_URL 未配置")
|
||||||
if not cls.CONFLUENCE_CONTENT_ID:
|
if not cls.CONFLUENCE_TOKEN:
|
||||||
errors.append("CONFLUENCE_CONTENT_ID 未配置")
|
errors.append("CONFLUENCE_TOKEN 未配置")
|
||||||
|
if not cls.CONFLUENCE_CONTENT_ID:
|
||||||
# 检查飞书配置(可选,但建议配置)
|
errors.append("CONFLUENCE_CONTENT_ID 未配置")
|
||||||
if not cls.FEISHU_TOKEN:
|
|
||||||
print("警告: FEISHU_TOKEN 未配置,排班功能将不可用")
|
# 检查飞书配置(可选,但建议配置)
|
||||||
if not cls.FEISHU_SPREADSHEET_TOKEN:
|
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||||
print("警告: FEISHU_SPREADSHEET_TOKEN 未配置,排班功能将不可用")
|
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||||
|
|
||||||
if errors:
|
if not has_feishu_token and not has_app_credentials:
|
||||||
print("配置验证失败:")
|
print("警告: 飞书认证未配置,排班功能将不可用")
|
||||||
for error in errors:
|
print(" 请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||||
print(f" - {error}")
|
elif has_app_credentials:
|
||||||
return False
|
print("信息: 使用飞书应用凭证自动获取token")
|
||||||
|
elif has_feishu_token:
|
||||||
return True
|
print("信息: 使用手动配置的FEISHU_TOKEN")
|
||||||
|
|
||||||
@classmethod
|
if not cls.FEISHU_SPREADSHEET_TOKEN:
|
||||||
def print_summary(cls):
|
print("警告: FEISHU_SPREADSHEET_TOKEN 未配置,排班功能将不可用")
|
||||||
"""打印配置摘要"""
|
|
||||||
print("配置摘要:")
|
if errors:
|
||||||
print(f" Confluence: {'已配置' if cls.CONFLUENCE_BASE_URL else '未配置'}")
|
print("配置验证失败:")
|
||||||
print(f" 飞书: {'已配置' if cls.FEISHU_TOKEN else '未配置'}")
|
for error in errors:
|
||||||
print(f" 数据库路径: {cls.DATABASE_PATH}")
|
print(f" - {error}")
|
||||||
print(f" 每日目标TEU: {cls.DAILY_TARGET_TEU}")
|
return False
|
||||||
print(f" 排班刷新天数: {cls.SCHEDULE_REFRESH_DAYS}")
|
|
||||||
|
return True
|
||||||
|
|
||||||
# 全局配置实例
|
@classmethod
|
||||||
config = Config()
|
def print_summary(cls):
|
||||||
|
"""打印配置摘要"""
|
||||||
|
print("配置摘要:")
|
||||||
if __name__ == '__main__':
|
print(f" Confluence: {'已配置' if cls.CONFLUENCE_BASE_URL else '未配置'}")
|
||||||
# 测试配置
|
|
||||||
config.print_summary()
|
# 飞书配置详情
|
||||||
if config.validate():
|
has_feishu_token = bool(cls.FEISHU_TOKEN)
|
||||||
print("配置验证通过")
|
has_app_credentials = bool(cls.FEISHU_APP_ID and cls.FEISHU_APP_SECRET)
|
||||||
else:
|
has_spreadsheet_token = bool(cls.FEISHU_SPREADSHEET_TOKEN)
|
||||||
|
|
||||||
|
if has_app_credentials:
|
||||||
|
feishu_status = f"应用凭证 (ID: {cls.FEISHU_APP_ID[:8]}...)"
|
||||||
|
elif has_feishu_token:
|
||||||
|
feishu_status = "手动token"
|
||||||
|
else:
|
||||||
|
feishu_status = "未配置"
|
||||||
|
|
||||||
|
print(f" 飞书认证: {feishu_status}")
|
||||||
|
print(f" 飞书表格: {'已配置' if has_spreadsheet_token else '未配置'}")
|
||||||
|
print(f" 数据库路径: {cls.DATABASE_PATH}")
|
||||||
|
print(f" 每日目标TEU: {cls.DAILY_TARGET_TEU}")
|
||||||
|
print(f" 排班刷新天数: {cls.SCHEDULE_REFRESH_DAYS}")
|
||||||
|
|
||||||
|
|
||||||
|
# 全局配置实例
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试配置
|
||||||
|
config.print_summary()
|
||||||
|
if config.validate():
|
||||||
|
print("配置验证通过")
|
||||||
|
else:
|
||||||
print("配置验证失败")
|
print("配置验证失败")
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
Confluence API 模块
|
Confluence API 模块
|
||||||
提供Confluence页面内容获取和解析功能
|
提供Confluence页面内容获取和解析功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .client import ConfluenceClient, ConfluenceClientError
|
from .client import ConfluenceClient, ConfluenceClientError
|
||||||
from .parser import HTMLContentParser
|
from .parser import HTMLContentParser
|
||||||
from .manager import ConfluenceContentManager
|
from .manager import ConfluenceContentManager
|
||||||
from .text import HTMLTextExtractor, HTMLTextExtractorError
|
from .text import HTMLTextExtractor, HTMLTextExtractorError
|
||||||
from .log_parser import HandoverLogParser, ShipLog, LogParserError
|
from .log_parser import HandoverLogParser, ShipLog, LogParserError
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ConfluenceClient',
|
'ConfluenceClient',
|
||||||
'ConfluenceClientError',
|
'ConfluenceClientError',
|
||||||
'HTMLContentParser',
|
'HTMLContentParser',
|
||||||
'ConfluenceContentManager',
|
'ConfluenceContentManager',
|
||||||
'HTMLTextExtractor',
|
'HTMLTextExtractor',
|
||||||
'HTMLTextExtractorError',
|
'HTMLTextExtractorError',
|
||||||
'HandoverLogParser',
|
'HandoverLogParser',
|
||||||
'ShipLog',
|
'ShipLog',
|
||||||
'LogParserError'
|
'LogParserError'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,212 +1,219 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Confluence API 客户端
|
Confluence API 客户端
|
||||||
提供Confluence页面内容获取功能
|
提供Confluence页面内容获取功能
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
from src.retry import retry, retry_on_exception
|
||||||
logger = get_logger(__name__)
|
from src.error_handler import NetworkError, ConfigurationError
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
class ConfluenceClientError(Exception):
|
|
||||||
"""Confluence API 错误"""
|
|
||||||
pass
|
class ConfluenceClientError(Exception):
|
||||||
|
"""Confluence API 错误"""
|
||||||
|
pass
|
||||||
class ConfluenceClient:
|
|
||||||
"""Confluence REST API 客户端"""
|
|
||||||
|
class ConfluenceClient:
|
||||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
"""Confluence REST API 客户端"""
|
||||||
"""
|
|
||||||
初始化客户端
|
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
||||||
|
"""
|
||||||
参数:
|
初始化客户端
|
||||||
base_url: Confluence API 基础URL (不包含 /content),如果为None则使用配置
|
|
||||||
token: Bearer 认证令牌,如果为None则使用配置
|
参数:
|
||||||
"""
|
base_url: Confluence API 基础URL (不包含 /content),如果为None则使用配置
|
||||||
self.base_url = (base_url or config.CONFLUENCE_BASE_URL).rstrip('/')
|
token: Bearer 认证令牌,如果为None则使用配置
|
||||||
self.token = token or config.CONFLUENCE_TOKEN
|
"""
|
||||||
|
self.base_url = (base_url or config.CONFLUENCE_BASE_URL).rstrip('/')
|
||||||
if not self.base_url or not self.token:
|
self.token = token or config.CONFLUENCE_TOKEN
|
||||||
raise ConfluenceClientError("Confluence配置不完整,请检查环境变量")
|
|
||||||
|
if not self.base_url or not self.token:
|
||||||
self.headers = {
|
raise ConfigurationError("Confluence配置不完整,请检查环境变量")
|
||||||
'Authorization': f'Bearer {self.token}',
|
|
||||||
'Accept': 'application/json'
|
self.headers = {
|
||||||
}
|
'Authorization': f'Bearer {self.token}',
|
||||||
|
'Accept': 'application/json'
|
||||||
# 使用 Session 重用连接
|
}
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.headers.update(self.headers)
|
# 使用 Session 重用连接
|
||||||
self.session.timeout = config.REQUEST_TIMEOUT
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update(self.headers)
|
||||||
logger.debug(f"Confluence客户端初始化完成,基础URL: {self.base_url}")
|
self.session.timeout = config.REQUEST_TIMEOUT
|
||||||
|
|
||||||
def fetch_content(self, content_id: str, expand: str = 'body.storage') -> Dict[str, Any]:
|
logger.debug(f"Confluence客户端初始化完成,基础URL: {self.base_url}")
|
||||||
"""
|
|
||||||
获取页面内容
|
@retry_on_exception(
|
||||||
|
exception_type=requests.exceptions.RequestException,
|
||||||
参数:
|
max_attempts=3,
|
||||||
content_id: 页面ID
|
delay=2.0
|
||||||
expand: 展开字段
|
)
|
||||||
|
def fetch_content(self, content_id: str, expand: str = 'body.storage') -> Dict[str, Any]:
|
||||||
返回:
|
"""
|
||||||
API 响应数据
|
获取页面内容
|
||||||
|
|
||||||
异常:
|
参数:
|
||||||
ConfluenceClientError: API调用失败
|
content_id: 页面ID
|
||||||
requests.exceptions.RequestException: 网络请求失败
|
expand: 展开字段
|
||||||
"""
|
|
||||||
url = f'{self.base_url}/content/{content_id}'
|
返回:
|
||||||
params = {'expand': expand}
|
API 响应数据
|
||||||
|
|
||||||
try:
|
异常:
|
||||||
logger.debug(f"获取Confluence内容: {content_id}")
|
ConfluenceClientError: API调用失败
|
||||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
NetworkError: 网络请求失败
|
||||||
response.raise_for_status()
|
"""
|
||||||
|
url = f'{self.base_url}/content/{content_id}'
|
||||||
data = response.json()
|
params = {'expand': expand}
|
||||||
logger.info(f"成功获取Confluence内容: {content_id}")
|
|
||||||
return data
|
try:
|
||||||
|
logger.debug(f"获取Confluence内容: {content_id}")
|
||||||
except requests.exceptions.HTTPError as e:
|
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||||
status_code = e.response.status_code if e.response else '未知'
|
response.raise_for_status()
|
||||||
error_msg = f"Confluence API HTTP错误: {status_code}, URL: {url}"
|
|
||||||
logger.error(error_msg)
|
data = response.json()
|
||||||
raise ConfluenceClientError(error_msg) from e
|
logger.info(f"成功获取Confluence内容: {content_id}")
|
||||||
|
return data
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
error_msg = f"Confluence API 网络错误: {e}"
|
except requests.exceptions.HTTPError as e:
|
||||||
logger.error(error_msg)
|
status_code = e.response.status_code if e.response else '未知'
|
||||||
raise ConfluenceClientError(error_msg) from e
|
error_msg = f"Confluence API HTTP错误: {status_code}, URL: {url}"
|
||||||
|
logger.error(error_msg)
|
||||||
except ValueError as e:
|
raise NetworkError(error_msg) from e
|
||||||
error_msg = f"Confluence API 响应解析失败: {e}"
|
|
||||||
logger.error(error_msg)
|
except requests.exceptions.RequestException as e:
|
||||||
raise ConfluenceClientError(error_msg) from e
|
error_msg = f"Confluence API 网络错误: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
def get_html(self, content_id: str) -> str:
|
raise NetworkError(error_msg) from e
|
||||||
"""
|
|
||||||
获取页面HTML内容
|
except ValueError as e:
|
||||||
|
error_msg = f"Confluence API 响应解析失败: {e}"
|
||||||
参数:
|
logger.error(error_msg)
|
||||||
content_id: 页面ID
|
raise ConfluenceClientError(error_msg) from e
|
||||||
|
|
||||||
返回:
|
def get_html(self, content_id: str) -> str:
|
||||||
HTML 字符串
|
"""
|
||||||
|
获取页面HTML内容
|
||||||
异常:
|
|
||||||
ConfluenceClientError: API调用失败或HTML内容为空
|
参数:
|
||||||
"""
|
content_id: 页面ID
|
||||||
try:
|
|
||||||
data = self.fetch_content(content_id)
|
返回:
|
||||||
html = data.get('body', {}).get('storage', {}).get('value', '')
|
HTML 字符串
|
||||||
|
|
||||||
if not html:
|
异常:
|
||||||
error_msg = f"Confluence页面HTML内容为空: {content_id}"
|
ConfluenceClientError: API调用失败或HTML内容为空
|
||||||
logger.error(error_msg)
|
"""
|
||||||
raise ConfluenceClientError(error_msg)
|
try:
|
||||||
|
data = self.fetch_content(content_id)
|
||||||
logger.info(f"获取到Confluence HTML内容,长度: {len(html)} 字符")
|
html = data.get('body', {}).get('storage', {}).get('value', '')
|
||||||
return html
|
|
||||||
|
if not html:
|
||||||
except KeyError as e:
|
error_msg = f"Confluence页面HTML内容为空: {content_id}"
|
||||||
error_msg = f"Confluence响应格式错误,缺少字段: {e}"
|
logger.error(error_msg)
|
||||||
logger.error(error_msg)
|
raise ConfluenceClientError(error_msg)
|
||||||
raise ConfluenceClientError(error_msg) from e
|
|
||||||
|
logger.info(f"获取到Confluence HTML内容,长度: {len(html)} 字符")
|
||||||
def test_connection(self, content_id: Optional[str] = None) -> bool:
|
return html
|
||||||
"""
|
|
||||||
测试Confluence连接是否正常
|
except KeyError as e:
|
||||||
|
error_msg = f"Confluence响应格式错误,缺少字段: {e}"
|
||||||
参数:
|
logger.error(error_msg)
|
||||||
content_id: 测试页面ID,如果为None则使用配置
|
raise ConfluenceClientError(error_msg) from e
|
||||||
|
|
||||||
返回:
|
def test_connection(self, content_id: Optional[str] = None) -> bool:
|
||||||
连接是否正常
|
"""
|
||||||
"""
|
测试Confluence连接是否正常
|
||||||
test_content_id = content_id or config.CONFLUENCE_CONTENT_ID
|
|
||||||
|
参数:
|
||||||
try:
|
content_id: 测试页面ID,如果为None则使用配置
|
||||||
data = self.fetch_content(test_content_id)
|
|
||||||
title = data.get('title', '未知标题')
|
返回:
|
||||||
logger.info(f"Confluence连接测试成功,页面: {title}")
|
连接是否正常
|
||||||
return True
|
"""
|
||||||
|
test_content_id = content_id or config.CONFLUENCE_CONTENT_ID
|
||||||
except ConfluenceClientError as e:
|
|
||||||
logger.error(f"Confluence连接测试失败: {e}")
|
try:
|
||||||
return False
|
data = self.fetch_content(test_content_id)
|
||||||
|
title = data.get('title', '未知标题')
|
||||||
except Exception as e:
|
logger.info(f"Confluence连接测试成功,页面: {title}")
|
||||||
logger.error(f"Confluence连接测试异常: {e}")
|
return True
|
||||||
return False
|
|
||||||
|
except ConfluenceClientError as e:
|
||||||
def get_page_info(self, content_id: str) -> Dict[str, Any]:
|
logger.error(f"Confluence连接测试失败: {e}")
|
||||||
"""
|
return False
|
||||||
获取页面基本信息
|
|
||||||
|
except Exception as e:
|
||||||
参数:
|
logger.error(f"Confluence连接测试异常: {e}")
|
||||||
content_id: 页面ID
|
return False
|
||||||
|
|
||||||
返回:
|
def get_page_info(self, content_id: str) -> Dict[str, Any]:
|
||||||
页面信息字典
|
"""
|
||||||
"""
|
获取页面基本信息
|
||||||
try:
|
|
||||||
data = self.fetch_content(content_id)
|
参数:
|
||||||
return {
|
content_id: 页面ID
|
||||||
'id': data.get('id'),
|
|
||||||
'title': data.get('title'),
|
返回:
|
||||||
'version': data.get('version', {}).get('number'),
|
页面信息字典
|
||||||
'created': data.get('history', {}).get('createdDate'),
|
"""
|
||||||
'last_updated': data.get('version', {}).get('when'),
|
try:
|
||||||
'space': data.get('space', {}).get('key'),
|
data = self.fetch_content(content_id)
|
||||||
'url': f"{self.base_url.replace('/rest/api', '')}/pages/{content_id}"
|
return {
|
||||||
}
|
'id': data.get('id'),
|
||||||
|
'title': data.get('title'),
|
||||||
except Exception as e:
|
'version': data.get('version', {}).get('number'),
|
||||||
error_msg = f"获取页面信息失败: {e}"
|
'created': data.get('history', {}).get('createdDate'),
|
||||||
logger.error(error_msg)
|
'last_updated': data.get('version', {}).get('when'),
|
||||||
raise ConfluenceClientError(error_msg) from e
|
'space': data.get('space', {}).get('key'),
|
||||||
|
'url': f"{self.base_url.replace('/rest/api', '')}/pages/{content_id}"
|
||||||
|
}
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试代码
|
except Exception as e:
|
||||||
import sys
|
error_msg = f"获取页面信息失败: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
# 设置日志
|
raise ConfluenceClientError(error_msg) from e
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
try:
|
if __name__ == '__main__':
|
||||||
# 测试连接
|
# 测试代码
|
||||||
client = ConfluenceClient()
|
import sys
|
||||||
|
|
||||||
if client.test_connection():
|
# 设置日志
|
||||||
print("Confluence连接测试成功")
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# 获取HTML内容
|
try:
|
||||||
content_id = config.CONFLUENCE_CONTENT_ID
|
# 测试连接
|
||||||
if content_id:
|
client = ConfluenceClient()
|
||||||
html = client.get_html(content_id)
|
|
||||||
print(f"获取到HTML内容,长度: {len(html)} 字符")
|
if client.test_connection():
|
||||||
|
print("Confluence连接测试成功")
|
||||||
# 获取页面信息
|
|
||||||
page_info = client.get_page_info(content_id)
|
# 获取HTML内容
|
||||||
print(f"页面标题: {page_info.get('title')}")
|
content_id = config.CONFLUENCE_CONTENT_ID
|
||||||
print(f"页面URL: {page_info.get('url')}")
|
if content_id:
|
||||||
else:
|
html = client.get_html(content_id)
|
||||||
print("未配置CONFLUENCE_CONTENT_ID,跳过HTML获取")
|
print(f"获取到HTML内容,长度: {len(html)} 字符")
|
||||||
else:
|
|
||||||
print("Confluence连接测试失败")
|
# 获取页面信息
|
||||||
sys.exit(1)
|
page_info = client.get_page_info(content_id)
|
||||||
|
print(f"页面标题: {page_info.get('title')}")
|
||||||
except ConfluenceClientError as e:
|
print(f"页面URL: {page_info.get('url')}")
|
||||||
print(f"Confluence客户端错误: {e}")
|
else:
|
||||||
sys.exit(1)
|
print("未配置CONFLUENCE_CONTENT_ID,跳过HTML获取")
|
||||||
except Exception as e:
|
else:
|
||||||
print(f"未知错误: {e}")
|
print("Confluence连接测试失败")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except ConfluenceClientError as e:
|
||||||
|
print(f"Confluence客户端错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"未知错误: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1,350 +1,582 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
日志解析模块
|
日志解析模块
|
||||||
完善类型提示和异常处理
|
完善类型提示和异常处理
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from typing import List, Dict, Optional, Tuple, Any
|
from typing import List, Dict, Optional, Tuple, Any
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ShipLog:
|
class ShipLog:
|
||||||
"""船次日志数据类"""
|
"""船次日志数据类"""
|
||||||
date: str
|
date: str
|
||||||
shift: str
|
shift: str
|
||||||
ship_name: str
|
ship_name: str
|
||||||
teu: Optional[int] = None
|
teu: Optional[int] = None
|
||||||
efficiency: Optional[float] = None
|
efficiency: Optional[float] = None
|
||||||
vehicles: Optional[int] = None
|
vehicles: Optional[int] = None
|
||||||
|
twenty_feet: Optional[int] = None # 20尺箱量
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
forty_feet: Optional[int] = None # 40尺箱量
|
||||||
"""转换为字典"""
|
|
||||||
return asdict(self)
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""转换为字典"""
|
||||||
|
return asdict(self)
|
||||||
class LogParserError(Exception):
|
|
||||||
"""日志解析错误"""
|
|
||||||
pass
|
class LogParserError(Exception):
|
||||||
|
"""日志解析错误"""
|
||||||
|
pass
|
||||||
class HandoverLogParser:
|
|
||||||
"""交接班日志解析器"""
|
|
||||||
|
class HandoverLogParser:
|
||||||
SEPARATOR = '———————————————————————————————————————————————'
|
"""交接班日志解析器"""
|
||||||
|
|
||||||
def __init__(self):
|
SEPARATOR = '———————————————————————————————————————————————'
|
||||||
"""初始化解析器"""
|
|
||||||
pass
|
def __init__(self):
|
||||||
|
"""初始化解析器"""
|
||||||
@staticmethod
|
pass
|
||||||
def parse_date(date_str: str) -> str:
|
|
||||||
"""
|
@staticmethod
|
||||||
解析日期字符串
|
def parse_date(date_str: str) -> str:
|
||||||
|
"""
|
||||||
参数:
|
解析日期字符串
|
||||||
date_str: 日期字符串,格式 "2025.12.30"
|
|
||||||
|
参数:
|
||||||
返回:
|
date_str: 日期字符串,格式 "2025.12.30"
|
||||||
标准化日期字符串 "2025-12-30"
|
|
||||||
|
返回:
|
||||||
异常:
|
标准化日期字符串 "2025-12-30"
|
||||||
ValueError: 日期格式无效
|
|
||||||
"""
|
异常:
|
||||||
if not date_str:
|
ValueError: 日期格式无效
|
||||||
return date_str
|
"""
|
||||||
|
if not date_str:
|
||||||
try:
|
return date_str
|
||||||
parts = date_str.split('.')
|
|
||||||
if len(parts) == 3:
|
try:
|
||||||
# 验证每个部分都是数字
|
parts = date_str.split('.')
|
||||||
year, month, day = parts
|
if len(parts) == 3:
|
||||||
if not (year.isdigit() and month.isdigit() and day.isdigit()):
|
# 验证每个部分都是数字
|
||||||
raise ValueError(f"日期包含非数字字符: {date_str}")
|
year, month, day = parts
|
||||||
|
if not (year.isdigit() and month.isdigit() and day.isdigit()):
|
||||||
# 标准化为YYYY-MM-DD格式
|
raise ValueError(f"日期包含非数字字符: {date_str}")
|
||||||
return f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
|
||||||
|
# 标准化为YYYY-MM-DD格式
|
||||||
# 如果不是点分隔格式,尝试其他格式
|
return f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
||||||
if '-' in date_str:
|
|
||||||
# 已经是标准格式
|
# 如果不是点分隔格式,尝试其他格式
|
||||||
return date_str
|
if '-' in date_str:
|
||||||
|
# 已经是标准格式
|
||||||
logger.warning(f"无法解析日期格式: {date_str}")
|
return date_str
|
||||||
return date_str
|
|
||||||
|
logger.warning(f"无法解析日期格式: {date_str}")
|
||||||
except Exception as e:
|
return date_str
|
||||||
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
|
||||||
return date_str
|
except Exception as e:
|
||||||
|
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
||||||
def parse(self, text: str) -> List[ShipLog]:
|
return date_str
|
||||||
"""
|
|
||||||
解析日志文本
|
def parse(self, text: str) -> List[ShipLog]:
|
||||||
|
"""
|
||||||
参数:
|
解析日志文本
|
||||||
text: 日志文本
|
|
||||||
|
参数:
|
||||||
返回:
|
text: 日志文本
|
||||||
船次日志列表(已合并同日期同班次同船名的记录)
|
|
||||||
|
返回:
|
||||||
异常:
|
船次日志列表(已合并同日期同班次同船名的记录)
|
||||||
LogParserError: 解析失败
|
|
||||||
ValueError: 输入参数无效
|
异常:
|
||||||
"""
|
LogParserError: 解析失败
|
||||||
if not text:
|
ValueError: 输入参数无效
|
||||||
logger.warning("日志文本为空")
|
"""
|
||||||
return []
|
if not text:
|
||||||
|
logger.warning("日志文本为空")
|
||||||
if not isinstance(text, str):
|
return []
|
||||||
error_msg = f"日志文本类型错误,应为字符串,实际为: {type(text)}"
|
|
||||||
logger.error(error_msg)
|
if not isinstance(text, str):
|
||||||
raise ValueError(error_msg)
|
error_msg = f"日志文本类型错误,应为字符串,实际为: {type(text)}"
|
||||||
|
logger.error(error_msg)
|
||||||
try:
|
raise ValueError(error_msg)
|
||||||
logs: List[ShipLog] = []
|
|
||||||
|
try:
|
||||||
# 预处理:移除单行分隔符(前后都是空行的分隔符)
|
logs: List[ShipLog] = []
|
||||||
# 保留真正的内容分隔符(前后有内容的)
|
|
||||||
lines = text.split('\n')
|
# 预处理:修复日期格式和特殊字符
|
||||||
processed_lines: List[str] = []
|
# 1. 修复日期格式:将 "2026.1.1" 转换为 "2026.01.01"
|
||||||
i = 0
|
def fix_date_format(match):
|
||||||
while i < len(lines):
|
date_str = match.group(1)
|
||||||
line = lines[i]
|
parts = date_str.split('.')
|
||||||
if line.strip() == self.SEPARATOR:
|
if len(parts) == 3:
|
||||||
# 检查是否是单行分隔符(前后都是空行或分隔符)
|
year, month, day = parts
|
||||||
prev_empty = i == 0 or not lines[i-1].strip() or lines[i-1].strip() == self.SEPARATOR
|
# 补零
|
||||||
next_empty = i == len(lines) - 1 or not lines[i+1].strip() or lines[i+1].strip() == self.SEPARATOR
|
month = month.zfill(2)
|
||||||
if prev_empty and next_empty:
|
day = day.zfill(2)
|
||||||
# 单行分隔符,跳过
|
return f"日期:{year}.{month}.{day}"
|
||||||
i += 1
|
return match.group(0)
|
||||||
continue
|
|
||||||
processed_lines.append(line)
|
# 修复日期格式
|
||||||
i += 1
|
text = re.sub(r'日期:(\d{4}\.\d{1,2}\.\d{1,2})', fix_date_format, text)
|
||||||
|
|
||||||
processed_text = '\n'.join(processed_lines)
|
# 2. 修复特殊空格字符(\xa0 转换为普通空格)
|
||||||
blocks = processed_text.split(self.SEPARATOR)
|
text = text.replace('\xa0', ' ')
|
||||||
|
|
||||||
for block in blocks:
|
# 3. 移除单行分隔符(前后都是空行的分隔符)
|
||||||
if not block.strip() or '日期:' not in block:
|
# 保留真正的内容分隔符(前后有内容的)
|
||||||
continue
|
lines = text.split('\n')
|
||||||
|
processed_lines: List[str] = []
|
||||||
# 解析日期
|
i = 0
|
||||||
date_match = re.search(r'日期:(\d{4}\.\d{2}\.\d{2})', block)
|
while i < len(lines):
|
||||||
if not date_match:
|
line = lines[i]
|
||||||
continue
|
if line.strip() == self.SEPARATOR:
|
||||||
|
# 检查是否是单行分隔符(前后都是空行或分隔符)
|
||||||
date = self.parse_date(date_match.group(1))
|
prev_empty = i == 0 or not lines[i-1].strip() or lines[i-1].strip() == self.SEPARATOR
|
||||||
self._parse_block(block, date, logs)
|
next_empty = i == len(lines) - 1 or not lines[i+1].strip() or lines[i+1].strip() == self.SEPARATOR
|
||||||
|
if prev_empty and next_empty:
|
||||||
# 合并同日期同班次同船名的记录(累加TEU)
|
# 单行分隔符,跳过
|
||||||
merged: Dict[Tuple[str, str, str], ShipLog] = {}
|
i += 1
|
||||||
for log in logs:
|
continue
|
||||||
key = (log.date, log.shift, log.ship_name)
|
processed_lines.append(line)
|
||||||
if key not in merged:
|
i += 1
|
||||||
merged[key] = ShipLog(
|
|
||||||
date=log.date,
|
processed_text = '\n'.join(processed_lines)
|
||||||
shift=log.shift,
|
blocks = processed_text.split(self.SEPARATOR)
|
||||||
ship_name=log.ship_name,
|
|
||||||
teu=log.teu,
|
current_date = None
|
||||||
efficiency=log.efficiency,
|
for block in blocks:
|
||||||
vehicles=log.vehicles
|
if not block.strip():
|
||||||
)
|
continue
|
||||||
else:
|
|
||||||
# 累加TEU
|
# 检查块中是否包含日期(使用改进后的正则表达式)
|
||||||
if log.teu:
|
date_match = re.search(r'日期:(\d{4}\.\d{2}\.\d{2})', block)
|
||||||
if merged[key].teu is None:
|
if date_match:
|
||||||
merged[key].teu = log.teu
|
current_date = self.parse_date(date_match.group(1))
|
||||||
else:
|
|
||||||
merged[key].teu += log.teu
|
# 如果当前有日期,解析该块
|
||||||
# 累加车辆数
|
if current_date:
|
||||||
if log.vehicles:
|
self._parse_block(block, current_date, logs)
|
||||||
if merged[key].vehicles is None:
|
|
||||||
merged[key].vehicles = log.vehicles
|
# 合并同日期同班次同船名的记录(累加TEU和尺寸箱量)
|
||||||
else:
|
merged: Dict[Tuple[str, str, str], ShipLog] = {}
|
||||||
merged[key].vehicles += log.vehicles
|
for log in logs:
|
||||||
|
key = (log.date, log.shift, log.ship_name)
|
||||||
result = list(merged.values())
|
if key not in merged:
|
||||||
logger.info(f"日志解析完成,共 {len(result)} 条记录")
|
merged[key] = ShipLog(
|
||||||
return result
|
date=log.date,
|
||||||
|
shift=log.shift,
|
||||||
except Exception as e:
|
ship_name=log.ship_name,
|
||||||
error_msg = f"日志解析失败: {e}"
|
teu=log.teu,
|
||||||
logger.error(error_msg)
|
efficiency=log.efficiency,
|
||||||
raise LogParserError(error_msg) from e
|
vehicles=log.vehicles,
|
||||||
|
twenty_feet=log.twenty_feet,
|
||||||
def _parse_block(self, block: str, date: str, logs: List[ShipLog]) -> None:
|
forty_feet=log.forty_feet
|
||||||
"""解析日期块"""
|
)
|
||||||
try:
|
else:
|
||||||
for shift in ['白班', '夜班']:
|
# 累加TEU
|
||||||
shift_pattern = f'{shift}:'
|
if log.teu:
|
||||||
if shift_pattern not in block:
|
if merged[key].teu is None:
|
||||||
continue
|
merged[key].teu = log.teu
|
||||||
|
else:
|
||||||
shift_start = block.find(shift_pattern) + len(shift_pattern)
|
merged[key].teu += log.teu
|
||||||
|
# 累加车辆数
|
||||||
# 只找到下一个班次作为边界,不限制"注意事项:"
|
if log.vehicles:
|
||||||
next_pos = len(block)
|
if merged[key].vehicles is None:
|
||||||
for next_shift in ['白班', '夜班']:
|
merged[key].vehicles = log.vehicles
|
||||||
if next_shift != shift:
|
else:
|
||||||
pos = block.find(f'{next_shift}:', shift_start)
|
merged[key].vehicles += log.vehicles
|
||||||
if pos != -1 and pos < next_pos:
|
# 累加20尺箱量
|
||||||
next_pos = pos
|
if log.twenty_feet:
|
||||||
|
if merged[key].twenty_feet is None:
|
||||||
shift_content = block[shift_start:next_pos]
|
merged[key].twenty_feet = log.twenty_feet
|
||||||
self._parse_ships(shift_content, date, shift, logs)
|
else:
|
||||||
|
merged[key].twenty_feet += log.twenty_feet
|
||||||
except Exception as e:
|
# 累加40尺箱量
|
||||||
logger.warning(f"解析日期块失败: {date}, 错误: {e}")
|
if log.forty_feet:
|
||||||
|
if merged[key].forty_feet is None:
|
||||||
def _parse_ships(self, content: str, date: str, shift: str, logs: List[ShipLog]) -> None:
|
merged[key].forty_feet = log.forty_feet
|
||||||
"""解析船次"""
|
else:
|
||||||
try:
|
merged[key].forty_feet += log.forty_feet
|
||||||
parts = content.split('实船作业:')
|
|
||||||
|
result = list(merged.values())
|
||||||
for part in parts:
|
logger.info(f"日志解析完成,共 {len(result)} 条记录")
|
||||||
if not part.strip():
|
return result
|
||||||
continue
|
|
||||||
|
except Exception as e:
|
||||||
cleaned = part.replace('\xa0', ' ').strip()
|
error_msg = f"日志解析失败: {e}"
|
||||||
# 匹配 "xxx# 船名" 格式(船号和船名分开)
|
logger.error(error_msg)
|
||||||
ship_match = re.search(r'(\d+)#\s*(\S+)', cleaned)
|
raise LogParserError(error_msg) from e
|
||||||
|
|
||||||
if not ship_match:
|
def _parse_block(self, block: str, date: str, logs: List[ShipLog]) -> None:
|
||||||
continue
|
"""解析日期块"""
|
||||||
|
try:
|
||||||
# 船名只取纯船名(去掉xx#前缀和二次靠泊等标注)
|
for shift in ['白班', '夜班']:
|
||||||
ship_name = ship_match.group(2)
|
shift_pattern = f'{shift}:'
|
||||||
# 移除二次靠泊等标注
|
if shift_pattern not in block:
|
||||||
ship_name = re.sub(r'(二次靠泊)|(再次靠泊)|\(二次靠泊\)|\(再次靠泊\)', '', ship_name).strip()
|
continue
|
||||||
|
|
||||||
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
|
shift_start = block.find(shift_pattern) + len(shift_pattern)
|
||||||
teu_eff_match = re.search(
|
|
||||||
r'作业量/效率:(\d+)TEU[,,\s]*', cleaned
|
# 只找到下一个班次作为边界,不限制"注意事项:"
|
||||||
)
|
next_pos = len(block)
|
||||||
|
for next_shift in ['白班', '夜班']:
|
||||||
# 解析TEU
|
if next_shift != shift:
|
||||||
teu = None
|
pos = block.find(f'{next_shift}:', shift_start)
|
||||||
if teu_eff_match:
|
if pos != -1 and pos < next_pos:
|
||||||
try:
|
next_pos = pos
|
||||||
teu = int(teu_eff_match.group(1))
|
|
||||||
except ValueError as e:
|
shift_content = block[shift_start:next_pos]
|
||||||
logger.warning(f"TEU解析失败: {teu_eff_match.group(1)}, 错误: {e}")
|
self._parse_ships(shift_content, date, shift, logs)
|
||||||
|
|
||||||
# 解析车辆数
|
except Exception as e:
|
||||||
vehicles = None
|
logger.warning(f"解析日期块失败: {date}, 错误: {e}")
|
||||||
if vehicles_match:
|
|
||||||
try:
|
def _parse_ships(self, content: str, date: str, shift: str, logs: List[ShipLog]) -> None:
|
||||||
vehicles = int(vehicles_match.group(1))
|
"""解析船次"""
|
||||||
except ValueError as e:
|
try:
|
||||||
logger.warning(f"车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
|
# 首先解析转堆作业(无船名)
|
||||||
|
relocation_content = self._parse_relocation(content, date, shift)
|
||||||
log = ShipLog(
|
if relocation_content:
|
||||||
date=date,
|
logs.append(relocation_content)
|
||||||
shift=shift,
|
|
||||||
ship_name=ship_name,
|
# 然后解析实船作业(按 "实船作业:" 分割)
|
||||||
teu=teu,
|
parts = content.split('实船作业:')
|
||||||
efficiency=None, # 目前日志中没有效率数据
|
|
||||||
vehicles=vehicles
|
for part in parts:
|
||||||
)
|
if not part.strip():
|
||||||
logs.append(log)
|
continue
|
||||||
|
|
||||||
except Exception as e:
|
cleaned = part.replace('\xa0', ' ').strip()
|
||||||
logger.warning(f"解析船次失败: {date} {shift}, 错误: {e}")
|
# 匹配 "xxx# 船名" 格式(船号和船名分开)
|
||||||
|
ship_match = re.search(r'(\d+)#\s*(\S+)', cleaned)
|
||||||
def parse_from_file(self, filepath: str) -> List[ShipLog]:
|
|
||||||
"""
|
if not ship_match:
|
||||||
从文件解析日志
|
continue
|
||||||
|
|
||||||
参数:
|
# 船名只取纯船名(去掉xx#前缀和二次靠泊等标注)
|
||||||
filepath: 文件路径
|
ship_name = ship_match.group(2)
|
||||||
|
# 移除二次靠泊等标注
|
||||||
返回:
|
ship_name = re.sub(r'(二次靠泊)|(再次靠泊)|\(二次靠泊\)|\(再次靠泊\)', '', ship_name).strip()
|
||||||
船次日志列表
|
|
||||||
|
# 解析车辆数、TEU、尺寸箱量
|
||||||
异常:
|
self._parse_log_entry(cleaned, date, shift, ship_name, logs)
|
||||||
FileNotFoundError: 文件不存在
|
|
||||||
LogParserError: 解析失败
|
except Exception as e:
|
||||||
"""
|
logger.warning(f"解析船次失败: {date} {shift}, 错误: {e}")
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
def _parse_relocation(self, content: str, date: str, shift: str) -> Optional[ShipLog]:
|
||||||
text = f.read()
|
"""
|
||||||
|
解析转堆作业(无船名的作业类型)
|
||||||
return self.parse(text)
|
|
||||||
|
参数:
|
||||||
except FileNotFoundError as e:
|
content: 班次内容
|
||||||
error_msg = f"日志文件不存在: {filepath}"
|
date: 日期
|
||||||
logger.error(error_msg)
|
shift: 班次
|
||||||
raise
|
|
||||||
except Exception as e:
|
返回:
|
||||||
error_msg = f"从文件解析日志失败: {filepath}, 错误: {e}"
|
ShipLog 对象,如果未找到转堆作业则返回 None
|
||||||
logger.error(error_msg)
|
"""
|
||||||
raise LogParserError(error_msg) from e
|
try:
|
||||||
|
# 检查是否包含转堆作业
|
||||||
|
if '转堆作业:' not in content:
|
||||||
if __name__ == '__main__':
|
return None
|
||||||
# 测试代码
|
|
||||||
import sys
|
# 提取转堆作业部分(从 "转堆作业:" 到下一个空行或下一个实船作业)
|
||||||
|
relocation_start = content.find('转堆作业:') + len('转堆作业:')
|
||||||
# 设置日志
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
# 查找下一个 "实船作业:" 作为结束标记
|
||||||
|
next_ship = content.find('实船作业:', relocation_start)
|
||||||
parser = HandoverLogParser()
|
if next_ship == -1:
|
||||||
|
relocation_content = content[relocation_start:]
|
||||||
# 测试日期解析
|
else:
|
||||||
test_dates = ["2025.12.30", "2025.01.01", "无效日期", "2025-12-30"]
|
relocation_content = content[relocation_start:next_ship]
|
||||||
for date in test_dates:
|
|
||||||
parsed = parser.parse_date(date)
|
cleaned = relocation_content.replace('\xa0', ' ').strip()
|
||||||
print(f"解析日期 '{date}' -> '{parsed}'")
|
|
||||||
|
# 解析车辆数、TEU、尺寸箱量
|
||||||
# 测试日志解析
|
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
|
||||||
test_text = """
|
teu_eff_match = re.search(r'作业量/效率:(\d+)TEU', cleaned)
|
||||||
日期:2025.12.30
|
|
||||||
———————————————————————————————————————————————
|
teu = None
|
||||||
白班:
|
if teu_eff_match:
|
||||||
实船作业:123# 测试船1
|
try:
|
||||||
上场车辆数:5
|
teu = int(teu_eff_match.group(1))
|
||||||
作业量/效率:100TEU,
|
except ValueError as e:
|
||||||
注意事项:无
|
logger.warning(f"转堆作业 TEU 解析失败: {teu_eff_match.group(1)}, 错误: {e}")
|
||||||
———————————————————————————————————————————————
|
|
||||||
夜班:
|
vehicles = None
|
||||||
实船作业:456# 测试船2
|
if vehicles_match:
|
||||||
上场车辆数:3
|
try:
|
||||||
作业量/效率:80TEU,
|
vehicles = int(vehicles_match.group(1))
|
||||||
注意事项:无
|
except ValueError as e:
|
||||||
"""
|
logger.warning(f"转堆作业车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
try:
|
# 解析尺寸箱量
|
||||||
logs = parser.parse(test_text)
|
twenty_feet = None
|
||||||
print(f"\n解析到 {len(logs)} 条记录")
|
forty_feet = None
|
||||||
for log in logs:
|
|
||||||
print(f" {log.date} {log.shift} {log.ship_name}: {log.teu}TEU, {log.vehicles}辆车")
|
# 尝试多种格式匹配
|
||||||
except LogParserError as e:
|
size_pattern = None
|
||||||
print(f"日志解析失败: {e}")
|
|
||||||
sys.exit(1)
|
# 格式1: TEU,(20尺*2,40尺*3) 或 TEU(20尺*2,40尺*3)
|
||||||
|
size_pattern = re.search(r'TEU[,,\s]*(([^)]+))', cleaned)
|
||||||
# 测试合并功能
|
if not size_pattern:
|
||||||
duplicate_text = """
|
# 格式2: TEU(20尺*2,40尺*3)- 无空格版本
|
||||||
日期:2025.12.30
|
size_pattern = re.search(r'TEU[((]([^))]+)[))]', cleaned)
|
||||||
———————————————————————————————————————————————
|
if not size_pattern:
|
||||||
白班:
|
# 格式3: 作业量/效率:100TEU,20尺*2,40尺*3
|
||||||
实船作业:123# 测试船1
|
size_pattern = re.search(r'(\d+)尺\*(\d+)', cleaned)
|
||||||
上场车辆数:5
|
|
||||||
作业量/效率:100TEU,
|
if size_pattern:
|
||||||
实船作业:123# 测试船1(二次靠泊)
|
size_text = size_pattern.group(1)
|
||||||
上场车辆数:3
|
|
||||||
作业量/效率:50TEU,
|
# 尝试直接匹配 20尺*数字 或 40尺*数字
|
||||||
"""
|
twenty_match = re.search(r'20尺\s*[×x*]?\s*(\d+)', size_text)
|
||||||
|
if not twenty_match:
|
||||||
try:
|
twenty_match = re.search(r'(\d+)\s*×\s*20尺', size_text)
|
||||||
logs = parser.parse(duplicate_text)
|
|
||||||
print(f"\n合并测试,解析到 {len(logs)} 条记录")
|
if twenty_match:
|
||||||
for log in logs:
|
try:
|
||||||
print(f" {log.date} {log.shift} {log.ship_name}: {log.teu}TEU, {log.vehicles}辆车")
|
twenty_feet = int(twenty_match.group(1))
|
||||||
except LogParserError as e:
|
except ValueError as e:
|
||||||
print(f"合并测试失败: {e}")
|
logger.warning(f"转堆作业 20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
forty_match = re.search(r'40尺\s*[×x*]?\s*(\d+)', size_text)
|
||||||
|
if not forty_match:
|
||||||
|
forty_match = re.search(r'(\d+)\s*×\s*40尺', size_text)
|
||||||
|
|
||||||
|
if forty_match:
|
||||||
|
try:
|
||||||
|
forty_feet = int(forty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"转堆作业 40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
# 备用解析:直接在cleaned中查找尺寸信息
|
||||||
|
if twenty_feet is None:
|
||||||
|
twenty_match = re.search(r'20尺\s*[×x*]?\s*(\d+)', cleaned)
|
||||||
|
if twenty_match:
|
||||||
|
try:
|
||||||
|
twenty_feet = int(twenty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"转堆作业 20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
if forty_feet is None:
|
||||||
|
forty_match = re.search(r'40尺\s*[×x*]?\s*(\d+)', cleaned)
|
||||||
|
if forty_match:
|
||||||
|
try:
|
||||||
|
forty_feet = int(forty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"转堆作业 40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
# 使用 "转堆作业" 作为船名标识
|
||||||
|
log = ShipLog(
|
||||||
|
date=date,
|
||||||
|
shift=shift,
|
||||||
|
ship_name='转堆作业',
|
||||||
|
teu=teu,
|
||||||
|
efficiency=None,
|
||||||
|
vehicles=vehicles,
|
||||||
|
twenty_feet=twenty_feet,
|
||||||
|
forty_feet=forty_feet
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"解析转堆作业: {date} {shift} {log.teu}TEU")
|
||||||
|
return log
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"解析转堆作业失败: {date} {shift}, 错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_log_entry(self, cleaned: str, date: str, shift: str, ship_name: str, logs: List[ShipLog]) -> None:
|
||||||
|
"""解析单条日志记录(车辆数、TEU、尺寸箱量)"""
|
||||||
|
try:
|
||||||
|
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
|
||||||
|
teu_eff_match = re.search(r'作业量/效率:(\d+)TEU[,,\s]*', cleaned)
|
||||||
|
|
||||||
|
# 解析TEU
|
||||||
|
teu = None
|
||||||
|
if teu_eff_match:
|
||||||
|
try:
|
||||||
|
teu = int(teu_eff_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"TEU解析失败: {teu_eff_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
# 解析车辆数
|
||||||
|
vehicles = None
|
||||||
|
if vehicles_match:
|
||||||
|
try:
|
||||||
|
vehicles = int(vehicles_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
# 解析尺寸箱量
|
||||||
|
twenty_feet = None
|
||||||
|
forty_feet = None
|
||||||
|
|
||||||
|
# 尝试多种格式匹配
|
||||||
|
size_pattern = None
|
||||||
|
|
||||||
|
# 格式1: TEU,(20尺*2,40尺*3) 或 TEU(20尺*2,40尺*3)
|
||||||
|
size_pattern = re.search(r'TEU[,,\s]*(([^)]+))', cleaned)
|
||||||
|
if not size_pattern:
|
||||||
|
# 格式2: TEU(20尺*2,40尺*3)- 无空格版本
|
||||||
|
size_pattern = re.search(r'TEU[((]([^))]+)[))]', cleaned)
|
||||||
|
if not size_pattern:
|
||||||
|
# 格式3: 作业量/效率:100TEU,20尺*2,40尺*3
|
||||||
|
size_pattern = re.search(r'(\d+)尺\*(\d+)', cleaned)
|
||||||
|
|
||||||
|
if size_pattern:
|
||||||
|
size_text = size_pattern.group(1)
|
||||||
|
|
||||||
|
# 尝试直接匹配 20尺*数字 或 40尺*数字
|
||||||
|
twenty_match = re.search(r'20尺\s*[×x*]?\s*(\d+)', size_text)
|
||||||
|
if not twenty_match:
|
||||||
|
twenty_match = re.search(r'(\d+)\s*×\s*20尺', size_text)
|
||||||
|
|
||||||
|
if twenty_match:
|
||||||
|
try:
|
||||||
|
twenty_feet = int(twenty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
forty_match = re.search(r'40尺\s*[×x*]?\s*(\d+)', size_text)
|
||||||
|
if not forty_match:
|
||||||
|
forty_match = re.search(r'(\d+)\s*×\s*40尺', size_text)
|
||||||
|
|
||||||
|
if forty_match:
|
||||||
|
try:
|
||||||
|
forty_feet = int(forty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
# 备用解析:直接在cleaned中查找尺寸信息
|
||||||
|
if twenty_feet is None:
|
||||||
|
twenty_match = re.search(r'20尺\s*[×x*]?\s*(\d+)', cleaned)
|
||||||
|
if twenty_match:
|
||||||
|
try:
|
||||||
|
twenty_feet = int(twenty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
if forty_feet is None:
|
||||||
|
forty_match = re.search(r'40尺\s*[×x*]?\s*(\d+)', cleaned)
|
||||||
|
if forty_match:
|
||||||
|
try:
|
||||||
|
forty_feet = int(forty_match.group(1))
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
|
||||||
|
|
||||||
|
log = ShipLog(
|
||||||
|
date=date,
|
||||||
|
shift=shift,
|
||||||
|
ship_name=ship_name,
|
||||||
|
teu=teu,
|
||||||
|
efficiency=None,
|
||||||
|
vehicles=vehicles,
|
||||||
|
twenty_feet=twenty_feet,
|
||||||
|
forty_feet=forty_feet
|
||||||
|
)
|
||||||
|
logs.append(log)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"解析日志记录失败: {date} {shift} {ship_name}, 错误: {e}")
|
||||||
|
|
||||||
|
def parse_from_file(self, filepath: str) -> List[ShipLog]:
|
||||||
|
"""
|
||||||
|
从文件解析日志
|
||||||
|
|
||||||
|
参数:
|
||||||
|
filepath: 文件路径
|
||||||
|
|
||||||
|
返回:
|
||||||
|
船次日志列表
|
||||||
|
|
||||||
|
异常:
|
||||||
|
FileNotFoundError: 文件不存在
|
||||||
|
LogParserError: 解析失败
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
return self.parse(text)
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
error_msg = f"日志文件不存在: {filepath}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"从文件解析日志失败: {filepath}, 错误: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise LogParserError(error_msg) from e
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试代码
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 设置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
parser = HandoverLogParser()
|
||||||
|
|
||||||
|
# 测试日期解析
|
||||||
|
test_dates = ["2025.12.30", "2025.01.01", "无效日期", "2025-12-30"]
|
||||||
|
for date in test_dates:
|
||||||
|
parsed = parser.parse_date(date)
|
||||||
|
print(f"解析日期 '{date}' -> '{parsed}'")
|
||||||
|
|
||||||
|
# 测试日志解析
|
||||||
|
test_text = """
|
||||||
|
日期:2025.12.30
|
||||||
|
———————————————————————————————————————————————
|
||||||
|
白班:
|
||||||
|
实船作业:123# 测试船1
|
||||||
|
上场车辆数:5
|
||||||
|
作业量/效率:100TEU,
|
||||||
|
注意事项:无
|
||||||
|
———————————————————————————————————————————————
|
||||||
|
夜班:
|
||||||
|
实船作业:456# 测试船2
|
||||||
|
上场车辆数:3
|
||||||
|
作业量/效率:80TEU,
|
||||||
|
注意事项:无
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
logs = parser.parse(test_text)
|
||||||
|
print(f"\n解析到 {len(logs)} 条记录")
|
||||||
|
for log in logs:
|
||||||
|
print(f" {log.date} {log.shift} {log.ship_name}: {log.teu}TEU, {log.vehicles}辆车")
|
||||||
|
except LogParserError as e:
|
||||||
|
print(f"日志解析失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 测试合并功能
|
||||||
|
duplicate_text = """
|
||||||
|
日期:2025.12.30
|
||||||
|
———————————————————————————————————————————————
|
||||||
|
白班:
|
||||||
|
实船作业:123# 测试船1
|
||||||
|
上场车辆数:5
|
||||||
|
作业量/效率:100TEU,
|
||||||
|
实船作业:123# 测试船1(二次靠泊)
|
||||||
|
上场车辆数:3
|
||||||
|
作业量/效率:50TEU,
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
logs = parser.parse(duplicate_text)
|
||||||
|
print(f"\n合并测试,解析到 {len(logs)} 条记录")
|
||||||
|
for log in logs:
|
||||||
|
print(f" {log.date} {log.shift} {log.ship_name}: {log.teu}TEU, {log.vehicles}辆车")
|
||||||
|
except LogParserError as e:
|
||||||
|
print(f"合并测试失败: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1,354 +1,354 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Confluence 内容管理器
|
Confluence 内容管理器
|
||||||
提供高级的Confluence内容管理功能
|
提供高级的Confluence内容管理功能
|
||||||
"""
|
"""
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
from .client import ConfluenceClient, ConfluenceClientError
|
from .client import ConfluenceClient, ConfluenceClientError
|
||||||
from .parser import HTMLContentParser
|
from .parser import HTMLContentParser
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConfluenceContentManager:
|
class ConfluenceContentManager:
|
||||||
"""Confluence 内容管理器"""
|
"""Confluence 内容管理器"""
|
||||||
|
|
||||||
def __init__(self, client: Optional[ConfluenceClient] = None):
|
def __init__(self, client: Optional[ConfluenceClient] = None):
|
||||||
"""
|
"""
|
||||||
初始化内容管理器
|
初始化内容管理器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
client: Confluence客户端实例,如果为None则创建新实例
|
client: Confluence客户端实例,如果为None则创建新实例
|
||||||
"""
|
"""
|
||||||
self.client = client or ConfluenceClient()
|
self.client = client or ConfluenceClient()
|
||||||
self.parser = HTMLContentParser()
|
self.parser = HTMLContentParser()
|
||||||
logger.debug("Confluence内容管理器初始化完成")
|
logger.debug("Confluence内容管理器初始化完成")
|
||||||
|
|
||||||
def get_content_with_analysis(self, content_id: str) -> Dict[str, Any]:
|
def get_content_with_analysis(self, content_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取内容并进行分析
|
获取内容并进行分析
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
content_id: 页面ID
|
content_id: 页面ID
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
包含内容和分析结果的字典
|
包含内容和分析结果的字典
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"获取并分析Confluence内容: {content_id}")
|
logger.info(f"获取并分析Confluence内容: {content_id}")
|
||||||
|
|
||||||
# 获取页面信息
|
# 获取页面信息
|
||||||
page_info = self.client.get_page_info(content_id)
|
page_info = self.client.get_page_info(content_id)
|
||||||
|
|
||||||
# 获取HTML内容
|
# 获取HTML内容
|
||||||
html = self.client.get_html(content_id)
|
html = self.client.get_html(content_id)
|
||||||
|
|
||||||
# 分析内容
|
# 分析内容
|
||||||
analysis = self.parser.analyze_content(html)
|
analysis = self.parser.analyze_content(html)
|
||||||
|
|
||||||
# 提取纯文本(前500字符)
|
# 提取纯文本(前500字符)
|
||||||
plain_text = self.parser.extract_plain_text(html)
|
plain_text = self.parser.extract_plain_text(html)
|
||||||
preview_text = plain_text[:500] + "..." if len(plain_text) > 500 else plain_text
|
preview_text = plain_text[:500] + "..." if len(plain_text) > 500 else plain_text
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'page_info': page_info,
|
'page_info': page_info,
|
||||||
'html_length': len(html),
|
'html_length': len(html),
|
||||||
'analysis': analysis,
|
'analysis': analysis,
|
||||||
'preview_text': preview_text,
|
'preview_text': preview_text,
|
||||||
'has_content': len(html) > 0,
|
'has_content': len(html) > 0,
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"内容分析完成: {content_id}")
|
logger.info(f"内容分析完成: {content_id}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except ConfluenceClientError as e:
|
except ConfluenceClientError as e:
|
||||||
logger.error(f"获取内容失败: {e}")
|
logger.error(f"获取内容失败: {e}")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"内容分析失败: {e}"
|
error_msg = f"内容分析失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def check_content_health(self, content_id: str) -> Dict[str, Any]:
|
def check_content_health(self, content_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
检查内容健康状况
|
检查内容健康状况
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
content_id: 页面ID
|
content_id: 页面ID
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
健康检查结果
|
健康检查结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"检查内容健康状况: {content_id}")
|
logger.info(f"检查内容健康状况: {content_id}")
|
||||||
|
|
||||||
# 获取页面信息
|
# 获取页面信息
|
||||||
page_info = self.client.get_page_info(content_id)
|
page_info = self.client.get_page_info(content_id)
|
||||||
|
|
||||||
# 获取HTML内容
|
# 获取HTML内容
|
||||||
html = self.client.get_html(content_id)
|
html = self.client.get_html(content_id)
|
||||||
|
|
||||||
# 分析内容
|
# 分析内容
|
||||||
analysis = self.parser.analyze_content(html)
|
analysis = self.parser.analyze_content(html)
|
||||||
|
|
||||||
# 检查健康状况
|
# 检查健康状况
|
||||||
health_checks = {
|
health_checks = {
|
||||||
'has_content': len(html) > 0,
|
'has_content': len(html) > 0,
|
||||||
'has_text': analysis['plain_text_length'] > 0,
|
'has_text': analysis['plain_text_length'] > 0,
|
||||||
'has_structure': analysis['has_tables'] or analysis['has_links'] or analysis['has_images'],
|
'has_structure': analysis['has_tables'] or analysis['has_links'] or analysis['has_images'],
|
||||||
'content_size_ok': 100 <= len(html) <= 1000000, # 100字节到1MB
|
'content_size_ok': 100 <= len(html) <= 1000000, # 100字节到1MB
|
||||||
'text_ratio_ok': analysis['plain_text_length'] / max(len(html), 1) > 0.1, # 文本占比至少10%
|
'text_ratio_ok': analysis['plain_text_length'] / max(len(html), 1) > 0.1, # 文本占比至少10%
|
||||||
'word_count_ok': analysis['word_count'] >= 10, # 至少10个单词
|
'word_count_ok': analysis['word_count'] >= 10, # 至少10个单词
|
||||||
'has_links': analysis['has_links'],
|
'has_links': analysis['has_links'],
|
||||||
'has_images': analysis['has_images'],
|
'has_images': analysis['has_images'],
|
||||||
'has_tables': analysis['has_tables']
|
'has_tables': analysis['has_tables']
|
||||||
}
|
}
|
||||||
|
|
||||||
# 计算健康分数
|
# 计算健康分数
|
||||||
passed_checks = sum(1 for check in health_checks.values() if check)
|
passed_checks = sum(1 for check in health_checks.values() if check)
|
||||||
total_checks = len(health_checks)
|
total_checks = len(health_checks)
|
||||||
health_score = passed_checks / total_checks
|
health_score = passed_checks / total_checks
|
||||||
|
|
||||||
# 生成建议
|
# 生成建议
|
||||||
suggestions = []
|
suggestions = []
|
||||||
if not health_checks['has_content']:
|
if not health_checks['has_content']:
|
||||||
suggestions.append("页面内容为空")
|
suggestions.append("页面内容为空")
|
||||||
if not health_checks['has_text']:
|
if not health_checks['has_text']:
|
||||||
suggestions.append("页面缺少文本内容")
|
suggestions.append("页面缺少文本内容")
|
||||||
if not health_checks['content_size_ok']:
|
if not health_checks['content_size_ok']:
|
||||||
suggestions.append("页面内容大小异常")
|
suggestions.append("页面内容大小异常")
|
||||||
if not health_checks['text_ratio_ok']:
|
if not health_checks['text_ratio_ok']:
|
||||||
suggestions.append("文本占比过低")
|
suggestions.append("文本占比过低")
|
||||||
if not health_checks['word_count_ok']:
|
if not health_checks['word_count_ok']:
|
||||||
suggestions.append("单词数量不足")
|
suggestions.append("单词数量不足")
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'page_info': page_info,
|
'page_info': page_info,
|
||||||
'health_score': health_score,
|
'health_score': health_score,
|
||||||
'health_status': '健康' if health_score >= 0.8 else '警告' if health_score >= 0.5 else '异常',
|
'health_status': '健康' if health_score >= 0.8 else '警告' if health_score >= 0.5 else '异常',
|
||||||
'health_checks': health_checks,
|
'health_checks': health_checks,
|
||||||
'analysis': analysis,
|
'analysis': analysis,
|
||||||
'suggestions': suggestions,
|
'suggestions': suggestions,
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"健康检查完成: {content_id}, 分数: {health_score:.2f}")
|
logger.info(f"健康检查完成: {content_id}, 分数: {health_score:.2f}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except ConfluenceClientError as e:
|
except ConfluenceClientError as e:
|
||||||
logger.error(f"健康检查失败: {e}")
|
logger.error(f"健康检查失败: {e}")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"健康检查失败: {e}"
|
error_msg = f"健康检查失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def extract_content_summary(self, content_id: str, max_length: int = 200) -> Dict[str, Any]:
|
def extract_content_summary(self, content_id: str, max_length: int = 200) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
提取内容摘要
|
提取内容摘要
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
content_id: 页面ID
|
content_id: 页面ID
|
||||||
max_length: 摘要最大长度
|
max_length: 摘要最大长度
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
内容摘要
|
内容摘要
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"提取内容摘要: {content_id}")
|
logger.info(f"提取内容摘要: {content_id}")
|
||||||
|
|
||||||
# 获取页面信息
|
# 获取页面信息
|
||||||
page_info = self.client.get_page_info(content_id)
|
page_info = self.client.get_page_info(content_id)
|
||||||
|
|
||||||
# 获取HTML内容
|
# 获取HTML内容
|
||||||
html = self.client.get_html(content_id)
|
html = self.client.get_html(content_id)
|
||||||
|
|
||||||
# 提取纯文本
|
# 提取纯文本
|
||||||
plain_text = self.parser.extract_plain_text(html)
|
plain_text = self.parser.extract_plain_text(html)
|
||||||
|
|
||||||
# 生成摘要
|
# 生成摘要
|
||||||
if len(plain_text) <= max_length:
|
if len(plain_text) <= max_length:
|
||||||
summary = plain_text
|
summary = plain_text
|
||||||
else:
|
else:
|
||||||
# 尝试在句子边界处截断
|
# 尝试在句子边界处截断
|
||||||
sentences = plain_text.split('. ')
|
sentences = plain_text.split('. ')
|
||||||
summary_parts = []
|
summary_parts = []
|
||||||
current_length = 0
|
current_length = 0
|
||||||
|
|
||||||
for sentence in sentences:
|
for sentence in sentences:
|
||||||
if current_length + len(sentence) + 2 <= max_length: # +2 for ". "
|
if current_length + len(sentence) + 2 <= max_length: # +2 for ". "
|
||||||
summary_parts.append(sentence)
|
summary_parts.append(sentence)
|
||||||
current_length += len(sentence) + 2
|
current_length += len(sentence) + 2
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
summary = '. '.join(summary_parts)
|
summary = '. '.join(summary_parts)
|
||||||
if summary and not summary.endswith('.'):
|
if summary and not summary.endswith('.'):
|
||||||
summary += '...'
|
summary += '...'
|
||||||
|
|
||||||
# 提取关键信息
|
# 提取关键信息
|
||||||
links = self.parser.extract_links(html)
|
links = self.parser.extract_links(html)
|
||||||
images = self.parser.extract_images(html)
|
images = self.parser.extract_images(html)
|
||||||
tables = self.parser.extract_tables(html)
|
tables = self.parser.extract_tables(html)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'page_info': page_info,
|
'page_info': page_info,
|
||||||
'summary': summary,
|
'summary': summary,
|
||||||
'summary_length': len(summary),
|
'summary_length': len(summary),
|
||||||
'total_length': len(plain_text),
|
'total_length': len(plain_text),
|
||||||
'key_elements': {
|
'key_elements': {
|
||||||
'link_count': len(links),
|
'link_count': len(links),
|
||||||
'image_count': len(images),
|
'image_count': len(images),
|
||||||
'table_count': len(tables)
|
'table_count': len(tables)
|
||||||
},
|
},
|
||||||
'has_rich_content': len(links) > 0 or len(images) > 0 or len(tables) > 0,
|
'has_rich_content': len(links) > 0 or len(images) > 0 or len(tables) > 0,
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"内容摘要提取完成: {content_id}")
|
logger.info(f"内容摘要提取完成: {content_id}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except ConfluenceClientError as e:
|
except ConfluenceClientError as e:
|
||||||
logger.error(f"提取摘要失败: {e}")
|
logger.error(f"提取摘要失败: {e}")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"提取摘要失败: {e}"
|
error_msg = f"提取摘要失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def batch_analyze_pages(self, content_ids: List[str]) -> Dict[str, Any]:
|
def batch_analyze_pages(self, content_ids: List[str]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
批量分析多个页面
|
批量分析多个页面
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
content_ids: 页面ID列表
|
content_ids: 页面ID列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
批量分析结果
|
批量分析结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"批量分析 {len(content_ids)} 个页面")
|
logger.info(f"批量分析 {len(content_ids)} 个页面")
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for content_id in content_ids:
|
for content_id in content_ids:
|
||||||
try:
|
try:
|
||||||
result = self.get_content_with_analysis(content_id)
|
result = self.get_content_with_analysis(content_id)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
logger.debug(f"页面分析完成: {content_id}")
|
logger.debug(f"页面分析完成: {content_id}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append({
|
errors.append({
|
||||||
'content_id': content_id,
|
'content_id': content_id,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
})
|
})
|
||||||
logger.warning(f"页面分析失败: {content_id}, 错误: {e}")
|
logger.warning(f"页面分析失败: {content_id}, 错误: {e}")
|
||||||
|
|
||||||
# 计算统计信息
|
# 计算统计信息
|
||||||
if results:
|
if results:
|
||||||
total_pages = len(results)
|
total_pages = len(results)
|
||||||
successful_pages = len(results)
|
successful_pages = len(results)
|
||||||
failed_pages = len(errors)
|
failed_pages = len(errors)
|
||||||
|
|
||||||
total_html_length = sum(r['html_length'] for r in results)
|
total_html_length = sum(r['html_length'] for r in results)
|
||||||
avg_html_length = total_html_length / successful_pages if successful_pages > 0 else 0
|
avg_html_length = total_html_length / successful_pages if successful_pages > 0 else 0
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
'total_pages': total_pages,
|
'total_pages': total_pages,
|
||||||
'successful_pages': successful_pages,
|
'successful_pages': successful_pages,
|
||||||
'failed_pages': failed_pages,
|
'failed_pages': failed_pages,
|
||||||
'success_rate': successful_pages / total_pages if total_pages > 0 else 0,
|
'success_rate': successful_pages / total_pages if total_pages > 0 else 0,
|
||||||
'total_html_length': total_html_length,
|
'total_html_length': total_html_length,
|
||||||
'avg_html_length': avg_html_length,
|
'avg_html_length': avg_html_length,
|
||||||
'has_content_pages': sum(1 for r in results if r['has_content']),
|
'has_content_pages': sum(1 for r in results if r['has_content']),
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
stats = {
|
stats = {
|
||||||
'total_pages': 0,
|
'total_pages': 0,
|
||||||
'successful_pages': 0,
|
'successful_pages': 0,
|
||||||
'failed_pages': len(errors),
|
'failed_pages': len(errors),
|
||||||
'success_rate': 0,
|
'success_rate': 0,
|
||||||
'total_html_length': 0,
|
'total_html_length': 0,
|
||||||
'avg_html_length': 0,
|
'avg_html_length': 0,
|
||||||
'has_content_pages': 0,
|
'has_content_pages': 0,
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
batch_result = {
|
batch_result = {
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'results': results,
|
'results': results,
|
||||||
'errors': errors,
|
'errors': errors,
|
||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"批量分析完成: 成功 {len(results)} 个,失败 {len(errors)} 个")
|
logger.info(f"批量分析完成: 成功 {len(results)} 个,失败 {len(errors)} 个")
|
||||||
return batch_result
|
return batch_result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"批量分析失败: {e}"
|
error_msg = f"批量分析失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建管理器
|
# 创建管理器
|
||||||
manager = ConfluenceContentManager()
|
manager = ConfluenceContentManager()
|
||||||
|
|
||||||
# 测试连接
|
# 测试连接
|
||||||
from src.config import config
|
from src.config import config
|
||||||
content_id = config.CONFLUENCE_CONTENT_ID
|
content_id = config.CONFLUENCE_CONTENT_ID
|
||||||
|
|
||||||
if not content_id:
|
if not content_id:
|
||||||
print("未配置CONFLUENCE_CONTENT_ID,跳过测试")
|
print("未配置CONFLUENCE_CONTENT_ID,跳过测试")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if manager.client.test_connection(content_id):
|
if manager.client.test_connection(content_id):
|
||||||
print("Confluence连接测试成功")
|
print("Confluence连接测试成功")
|
||||||
|
|
||||||
# 测试内容分析
|
# 测试内容分析
|
||||||
print("\n1. 测试内容分析:")
|
print("\n1. 测试内容分析:")
|
||||||
analysis = manager.get_content_with_analysis(content_id)
|
analysis = manager.get_content_with_analysis(content_id)
|
||||||
print(f" 页面标题: {analysis['page_info'].get('title')}")
|
print(f" 页面标题: {analysis['page_info'].get('title')}")
|
||||||
print(f" 内容长度: {analysis['html_length']} 字符")
|
print(f" 内容长度: {analysis['html_length']} 字符")
|
||||||
print(f" 文本预览: {analysis['preview_text'][:100]}...")
|
print(f" 文本预览: {analysis['preview_text'][:100]}...")
|
||||||
|
|
||||||
# 测试健康检查
|
# 测试健康检查
|
||||||
print("\n2. 测试健康检查:")
|
print("\n2. 测试健康检查:")
|
||||||
health = manager.check_content_health(content_id)
|
health = manager.check_content_health(content_id)
|
||||||
print(f" 健康分数: {health['health_score']:.2f}")
|
print(f" 健康分数: {health['health_score']:.2f}")
|
||||||
print(f" 健康状态: {health['health_status']}")
|
print(f" 健康状态: {health['health_status']}")
|
||||||
print(f" 建议: {health['suggestions']}")
|
print(f" 建议: {health['suggestions']}")
|
||||||
|
|
||||||
# 测试内容摘要
|
# 测试内容摘要
|
||||||
print("\n3. 测试内容摘要:")
|
print("\n3. 测试内容摘要:")
|
||||||
summary = manager.extract_content_summary(content_id)
|
summary = manager.extract_content_summary(content_id)
|
||||||
print(f" 摘要: {summary['summary']}")
|
print(f" 摘要: {summary['summary']}")
|
||||||
print(f" 摘要长度: {summary['summary_length']} 字符")
|
print(f" 摘要长度: {summary['summary_length']} 字符")
|
||||||
print(f" 总长度: {summary['total_length']} 字符")
|
print(f" 总长度: {summary['total_length']} 字符")
|
||||||
|
|
||||||
print("\n所有测试通过")
|
print("\n所有测试通过")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Confluence连接测试失败")
|
print("Confluence连接测试失败")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
except ConfluenceClientError as e:
|
except ConfluenceClientError as e:
|
||||||
print(f"Confluence客户端错误: {e}")
|
print(f"Confluence客户端错误: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"未知错误: {e}")
|
print(f"未知错误: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1,244 +1,244 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Confluence HTML 内容解析器
|
Confluence HTML 内容解析器
|
||||||
提供Confluence HTML内容的解析和格式化功能
|
提供Confluence HTML内容的解析和格式化功能
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HTMLContentParser:
|
class HTMLContentParser:
|
||||||
"""Confluence HTML 内容解析器"""
|
"""Confluence HTML 内容解析器"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""初始化解析器"""
|
"""初始化解析器"""
|
||||||
logger.debug("HTML内容解析器初始化完成")
|
logger.debug("HTML内容解析器初始化完成")
|
||||||
|
|
||||||
def extract_plain_text(self, html: str) -> str:
|
def extract_plain_text(self, html: str) -> str:
|
||||||
"""
|
"""
|
||||||
从HTML中提取纯文本(简单版本)
|
从HTML中提取纯文本(简单版本)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
纯文本字符串
|
纯文本字符串
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 移除HTML标签
|
# 移除HTML标签
|
||||||
text = re.sub(r'<[^>]+>', ' ', html)
|
text = re.sub(r'<[^>]+>', ' ', html)
|
||||||
# 合并多个空格
|
# 合并多个空格
|
||||||
text = re.sub(r'\s+', ' ', text)
|
text = re.sub(r'\s+', ' ', text)
|
||||||
# 解码HTML实体(简单版本)
|
# 解码HTML实体(简单版本)
|
||||||
text = text.replace(' ', ' ').replace('&', '&').replace('<', '<').replace('>', '>')
|
text = text.replace(' ', ' ').replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
# 去除首尾空格
|
# 去除首尾空格
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
|
|
||||||
logger.debug(f"提取纯文本完成,长度: {len(text)} 字符")
|
logger.debug(f"提取纯文本完成,长度: {len(text)} 字符")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"提取纯文本失败: {e}"
|
error_msg = f"提取纯文本失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def extract_links(self, html: str) -> List[Dict[str, str]]:
|
def extract_links(self, html: str) -> List[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
从HTML中提取链接
|
从HTML中提取链接
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
链接列表,每个链接包含 'text' 和 'url'
|
链接列表,每个链接包含 'text' 和 'url'
|
||||||
"""
|
"""
|
||||||
links = []
|
links = []
|
||||||
try:
|
try:
|
||||||
# 简单的正则表达式匹配链接
|
# 简单的正则表达式匹配链接
|
||||||
link_pattern = r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>'
|
link_pattern = r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>'
|
||||||
matches = re.findall(link_pattern, html, re.IGNORECASE)
|
matches = re.findall(link_pattern, html, re.IGNORECASE)
|
||||||
|
|
||||||
for url, text in matches:
|
for url, text in matches:
|
||||||
links.append({
|
links.append({
|
||||||
'text': text.strip(),
|
'text': text.strip(),
|
||||||
'url': url.strip()
|
'url': url.strip()
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug(f"提取到 {len(links)} 个链接")
|
logger.debug(f"提取到 {len(links)} 个链接")
|
||||||
return links
|
return links
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"提取链接失败: {e}"
|
error_msg = f"提取链接失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def extract_images(self, html: str) -> List[Dict[str, str]]:
|
def extract_images(self, html: str) -> List[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
从HTML中提取图片
|
从HTML中提取图片
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
图片列表,每个图片包含 'src' 和 'alt'
|
图片列表,每个图片包含 'src' 和 'alt'
|
||||||
"""
|
"""
|
||||||
images = []
|
images = []
|
||||||
try:
|
try:
|
||||||
# 简单的正则表达式匹配图片
|
# 简单的正则表达式匹配图片
|
||||||
img_pattern = r'<img\s+[^>]*src=["\']([^"\']+)["\'][^>]*alt=["\']([^"\']*)["\'][^>]*>'
|
img_pattern = r'<img\s+[^>]*src=["\']([^"\']+)["\'][^>]*alt=["\']([^"\']*)["\'][^>]*>'
|
||||||
matches = re.findall(img_pattern, html, re.IGNORECASE)
|
matches = re.findall(img_pattern, html, re.IGNORECASE)
|
||||||
|
|
||||||
for src, alt in matches:
|
for src, alt in matches:
|
||||||
images.append({
|
images.append({
|
||||||
'src': src.strip(),
|
'src': src.strip(),
|
||||||
'alt': alt.strip()
|
'alt': alt.strip()
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug(f"提取到 {len(images)} 张图片")
|
logger.debug(f"提取到 {len(images)} 张图片")
|
||||||
return images
|
return images
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"提取图片失败: {e}"
|
error_msg = f"提取图片失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def extract_tables(self, html: str) -> List[List[List[str]]]:
|
def extract_tables(self, html: str) -> List[List[List[str]]]:
|
||||||
"""
|
"""
|
||||||
从HTML中提取表格数据
|
从HTML中提取表格数据
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
表格列表,每个表格是二维列表
|
表格列表,每个表格是二维列表
|
||||||
"""
|
"""
|
||||||
tables = []
|
tables = []
|
||||||
try:
|
try:
|
||||||
# 简单的表格提取(仅支持简单表格)
|
# 简单的表格提取(仅支持简单表格)
|
||||||
table_pattern = r'<table[^>]*>(.*?)</table>'
|
table_pattern = r'<table[^>]*>(.*?)</table>'
|
||||||
table_matches = re.findall(table_pattern, html, re.IGNORECASE | re.DOTALL)
|
table_matches = re.findall(table_pattern, html, re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
for table_html in table_matches:
|
for table_html in table_matches:
|
||||||
rows = []
|
rows = []
|
||||||
# 提取行
|
# 提取行
|
||||||
row_pattern = r'<tr[^>]*>(.*?)</tr>'
|
row_pattern = r'<tr[^>]*>(.*?)</tr>'
|
||||||
row_matches = re.findall(row_pattern, table_html, re.IGNORECASE | re.DOTALL)
|
row_matches = re.findall(row_pattern, table_html, re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
for row_html in row_matches:
|
for row_html in row_matches:
|
||||||
cells = []
|
cells = []
|
||||||
# 提取单元格
|
# 提取单元格
|
||||||
cell_pattern = r'<t[dh][^>]*>(.*?)</t[dh]>'
|
cell_pattern = r'<t[dh][^>]*>(.*?)</t[dh]>'
|
||||||
cell_matches = re.findall(cell_pattern, row_html, re.IGNORECASE | re.DOTALL)
|
cell_matches = re.findall(cell_pattern, row_html, re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
for cell_html in cell_matches:
|
for cell_html in cell_matches:
|
||||||
# 清理单元格内容
|
# 清理单元格内容
|
||||||
cell_text = re.sub(r'<[^>]+>', '', cell_html)
|
cell_text = re.sub(r'<[^>]+>', '', cell_html)
|
||||||
cell_text = re.sub(r'\s+', ' ', cell_text).strip()
|
cell_text = re.sub(r'\s+', ' ', cell_text).strip()
|
||||||
cells.append(cell_text)
|
cells.append(cell_text)
|
||||||
|
|
||||||
rows.append(cells)
|
rows.append(cells)
|
||||||
|
|
||||||
if rows: # 只添加非空表格
|
if rows: # 只添加非空表格
|
||||||
tables.append(rows)
|
tables.append(rows)
|
||||||
|
|
||||||
logger.debug(f"提取到 {len(tables)} 个表格")
|
logger.debug(f"提取到 {len(tables)} 个表格")
|
||||||
return tables
|
return tables
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"提取表格失败: {e}"
|
error_msg = f"提取表格失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
def analyze_content(self, html: str) -> Dict[str, any]:
|
def analyze_content(self, html: str) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
分析HTML内容
|
分析HTML内容
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
内容分析结果
|
内容分析结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
plain_text = self.extract_plain_text(html)
|
plain_text = self.extract_plain_text(html)
|
||||||
links = self.extract_links(html)
|
links = self.extract_links(html)
|
||||||
images = self.extract_images(html)
|
images = self.extract_images(html)
|
||||||
tables = self.extract_tables(html)
|
tables = self.extract_tables(html)
|
||||||
|
|
||||||
analysis = {
|
analysis = {
|
||||||
'total_length': len(html),
|
'total_length': len(html),
|
||||||
'plain_text_length': len(plain_text),
|
'plain_text_length': len(plain_text),
|
||||||
'link_count': len(links),
|
'link_count': len(links),
|
||||||
'image_count': len(images),
|
'image_count': len(images),
|
||||||
'table_count': len(tables),
|
'table_count': len(tables),
|
||||||
'word_count': len(plain_text.split()),
|
'word_count': len(plain_text.split()),
|
||||||
'line_count': plain_text.count('\n') + 1,
|
'line_count': plain_text.count('\n') + 1,
|
||||||
'has_tables': len(tables) > 0,
|
'has_tables': len(tables) > 0,
|
||||||
'has_images': len(images) > 0,
|
'has_images': len(images) > 0,
|
||||||
'has_links': len(links) > 0
|
'has_links': len(links) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"内容分析完成: {analysis}")
|
logger.info(f"内容分析完成: {analysis}")
|
||||||
return analysis
|
return analysis
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"内容分析失败: {e}"
|
error_msg = f"内容分析失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# 测试HTML
|
# 测试HTML
|
||||||
test_html = """
|
test_html = """
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<h1>测试页面</h1>
|
<h1>测试页面</h1>
|
||||||
<p>这是一个测试页面,包含<a href="https://example.com">链接</a>和图片。</p>
|
<p>这是一个测试页面,包含<a href="https://example.com">链接</a>和图片。</p>
|
||||||
<img src="test.jpg" alt="测试图片">
|
<img src="test.jpg" alt="测试图片">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>标题1</th><th>标题2</th></tr>
|
<tr><th>标题1</th><th>标题2</th></tr>
|
||||||
<tr><td>数据1</td><td>数据2</td></tr>
|
<tr><td>数据1</td><td>数据2</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parser = HTMLContentParser()
|
parser = HTMLContentParser()
|
||||||
|
|
||||||
# 测试纯文本提取
|
# 测试纯文本提取
|
||||||
text = parser.extract_plain_text(test_html)
|
text = parser.extract_plain_text(test_html)
|
||||||
print(f"纯文本: {text[:100]}...")
|
print(f"纯文本: {text[:100]}...")
|
||||||
|
|
||||||
# 测试链接提取
|
# 测试链接提取
|
||||||
links = parser.extract_links(test_html)
|
links = parser.extract_links(test_html)
|
||||||
print(f"链接: {links}")
|
print(f"链接: {links}")
|
||||||
|
|
||||||
# 测试图片提取
|
# 测试图片提取
|
||||||
images = parser.extract_images(test_html)
|
images = parser.extract_images(test_html)
|
||||||
print(f"图片: {images}")
|
print(f"图片: {images}")
|
||||||
|
|
||||||
# 测试表格提取
|
# 测试表格提取
|
||||||
tables = parser.extract_tables(test_html)
|
tables = parser.extract_tables(test_html)
|
||||||
print(f"表格: {tables}")
|
print(f"表格: {tables}")
|
||||||
|
|
||||||
# 测试内容分析
|
# 测试内容分析
|
||||||
analysis = parser.analyze_content(test_html)
|
analysis = parser.analyze_content(test_html)
|
||||||
print(f"内容分析: {analysis}")
|
print(f"内容分析: {analysis}")
|
||||||
|
|
||||||
print("所有测试通过")
|
print("所有测试通过")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"测试失败: {e}")
|
print(f"测试失败: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1,309 +1,309 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
HTML 文本提取模块
|
HTML 文本提取模块
|
||||||
改进异常处理和类型提示
|
改进异常处理和类型提示
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from bs4 import BeautifulSoup, Tag, NavigableString
|
from bs4 import BeautifulSoup, Tag, NavigableString
|
||||||
from typing import List, Optional, Any, Union
|
from typing import List, Optional, Any, Union
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HTMLTextExtractorError(Exception):
|
class HTMLTextExtractorError(Exception):
|
||||||
"""HTML文本提取错误"""
|
"""HTML文本提取错误"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HTMLTextExtractor:
|
class HTMLTextExtractor:
|
||||||
"""HTML 文本提取器 - 保留布局结构"""
|
"""HTML 文本提取器 - 保留布局结构"""
|
||||||
|
|
||||||
# 块级元素列表
|
# 块级元素列表
|
||||||
BLOCK_TAGS = {
|
BLOCK_TAGS = {
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'section',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'section',
|
||||||
'table', 'tr', 'td', 'th', 'li', 'ul', 'ol', 'blockquote',
|
'table', 'tr', 'td', 'th', 'li', 'ul', 'ol', 'blockquote',
|
||||||
'pre', 'hr', 'br', 'tbody', 'thead', 'tfoot'
|
'pre', 'hr', 'br', 'tbody', 'thead', 'tfoot'
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""初始化提取器"""
|
"""初始化提取器"""
|
||||||
self.output_lines: List[str] = []
|
self.output_lines: List[str] = []
|
||||||
|
|
||||||
def extract(self, html: str) -> str:
|
def extract(self, html: str) -> str:
|
||||||
"""
|
"""
|
||||||
从HTML中提取保留布局的文本
|
从HTML中提取保留布局的文本
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
html: HTML字符串
|
html: HTML字符串
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
格式化的纯文本
|
格式化的纯文本
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
HTMLTextExtractorError: HTML解析失败
|
HTMLTextExtractorError: HTML解析失败
|
||||||
ValueError: 输入参数无效
|
ValueError: 输入参数无效
|
||||||
"""
|
"""
|
||||||
if not html:
|
if not html:
|
||||||
logger.warning("HTML内容为空")
|
logger.warning("HTML内容为空")
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
if not isinstance(html, str):
|
if not isinstance(html, str):
|
||||||
error_msg = f"HTML参数类型错误,应为字符串,实际为: {type(html)}"
|
error_msg = f"HTML参数类型错误,应为字符串,实际为: {type(html)}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"开始解析HTML,长度: {len(html)} 字符")
|
logger.debug(f"开始解析HTML,长度: {len(html)} 字符")
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
|
||||||
# 移除不需要的元素
|
# 移除不需要的元素
|
||||||
for tag in soup(["script", "style", "noscript"]):
|
for tag in soup(["script", "style", "noscript"]):
|
||||||
tag.decompose()
|
tag.decompose()
|
||||||
|
|
||||||
# 移除 Confluence 宏
|
# 移除 Confluence 宏
|
||||||
for macro in soup.find_all(attrs={"ac:name": True}):
|
for macro in soup.find_all(attrs={"ac:name": True}):
|
||||||
macro.decompose()
|
macro.decompose()
|
||||||
|
|
||||||
self.output_lines = []
|
self.output_lines = []
|
||||||
|
|
||||||
# 处理 body 或整个文档
|
# 处理 body 或整个文档
|
||||||
body = soup.body if soup.body else soup
|
body = soup.body if soup.body else soup
|
||||||
for child in body.children:
|
for child in body.children:
|
||||||
self._process_node(child)
|
self._process_node(child)
|
||||||
|
|
||||||
# 清理结果
|
# 清理结果
|
||||||
result = ''.join(self.output_lines)
|
result = ''.join(self.output_lines)
|
||||||
result = re.sub(r'\n\s*\n\s*\n', '\n\n', result)
|
result = re.sub(r'\n\s*\n\s*\n', '\n\n', result)
|
||||||
result = '\n'.join(line.rstrip() for line in result.split('\n'))
|
result = '\n'.join(line.rstrip() for line in result.split('\n'))
|
||||||
|
|
||||||
logger.info(f"HTML提取完成,输出长度: {len(result)} 字符")
|
logger.info(f"HTML提取完成,输出长度: {len(result)} 字符")
|
||||||
return result.strip()
|
return result.strip()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"HTML解析失败: {e}"
|
error_msg = f"HTML解析失败: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise HTMLTextExtractorError(error_msg) from e
|
raise HTMLTextExtractorError(error_msg) from e
|
||||||
|
|
||||||
def _process_node(self, node: Union[Tag, NavigableString], indent: int = 0,
|
def _process_node(self, node: Union[Tag, NavigableString], indent: int = 0,
|
||||||
list_context: Optional[tuple] = None) -> None:
|
list_context: Optional[tuple] = None) -> None:
|
||||||
"""递归处理节点"""
|
"""递归处理节点"""
|
||||||
if isinstance(node, NavigableString):
|
if isinstance(node, NavigableString):
|
||||||
text = str(node).strip()
|
text = str(node).strip()
|
||||||
if text:
|
if text:
|
||||||
text = re.sub(r'\s+', ' ', text)
|
text = re.sub(r'\s+', ' ', text)
|
||||||
if self.output_lines and not self.output_lines[-1].endswith('\n'):
|
if self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||||
self.output_lines[-1] += text
|
self.output_lines[-1] += text
|
||||||
else:
|
else:
|
||||||
self.output_lines.append(' ' * indent + text)
|
self.output_lines.append(' ' * indent + text)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(node, Tag):
|
if not isinstance(node, Tag):
|
||||||
return
|
return
|
||||||
|
|
||||||
tag_name = node.name.lower()
|
tag_name = node.name.lower()
|
||||||
is_block = tag_name in self.BLOCK_TAGS
|
is_block = tag_name in self.BLOCK_TAGS
|
||||||
|
|
||||||
# 块级元素前添加换行
|
# 块级元素前添加换行
|
||||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||||
self.output_lines.append('\n')
|
self.output_lines.append('\n')
|
||||||
|
|
||||||
# 处理特定标签
|
# 处理特定标签
|
||||||
if tag_name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
if tag_name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||||
try:
|
try:
|
||||||
level = int(tag_name[1])
|
level = int(tag_name[1])
|
||||||
prefix = '#' * level + ' '
|
prefix = '#' * level + ' '
|
||||||
text = node.get_text().strip()
|
text = node.get_text().strip()
|
||||||
if text:
|
if text:
|
||||||
self.output_lines.append(' ' * indent + prefix + text + '\n')
|
self.output_lines.append(' ' * indent + prefix + text + '\n')
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
logger.warning(f"解析标题标签失败: {tag_name}, 错误: {e}")
|
logger.warning(f"解析标题标签失败: {tag_name}, 错误: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'p':
|
elif tag_name == 'p':
|
||||||
text = node.get_text().strip()
|
text = node.get_text().strip()
|
||||||
if text:
|
if text:
|
||||||
self.output_lines.append(' ' * indent + text + '\n')
|
self.output_lines.append(' ' * indent + text + '\n')
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'hr':
|
elif tag_name == 'hr':
|
||||||
self.output_lines.append(' ' * indent + config.SEPARATOR_CHAR * config.SEPARATOR_LENGTH + '\n')
|
self.output_lines.append(' ' * indent + config.SEPARATOR_CHAR * config.SEPARATOR_LENGTH + '\n')
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'br':
|
elif tag_name == 'br':
|
||||||
self.output_lines.append('\n')
|
self.output_lines.append('\n')
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'table':
|
elif tag_name == 'table':
|
||||||
self._process_table(node, indent)
|
self._process_table(node, indent)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name in ('ul', 'ol'):
|
elif tag_name in ('ul', 'ol'):
|
||||||
self._process_list(node, indent, tag_name)
|
self._process_list(node, indent, tag_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'li':
|
elif tag_name == 'li':
|
||||||
self._process_list_item(node, indent, list_context)
|
self._process_list_item(node, indent, list_context)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name == 'a':
|
elif tag_name == 'a':
|
||||||
try:
|
try:
|
||||||
href = node.get('href', '')
|
href = node.get('href', '')
|
||||||
text = node.get_text().strip()
|
text = node.get_text().strip()
|
||||||
if href and text:
|
if href and text:
|
||||||
self.output_lines.append(f'{text} ({href})')
|
self.output_lines.append(f'{text} ({href})')
|
||||||
elif text:
|
elif text:
|
||||||
self.output_lines.append(text)
|
self.output_lines.append(text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"解析链接标签失败: {e}")
|
logger.warning(f"解析链接标签失败: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name in ('strong', 'b'):
|
elif tag_name in ('strong', 'b'):
|
||||||
text = node.get_text().strip()
|
text = node.get_text().strip()
|
||||||
if text:
|
if text:
|
||||||
self.output_lines.append(f'**{text}**')
|
self.output_lines.append(f'**{text}**')
|
||||||
return
|
return
|
||||||
|
|
||||||
elif tag_name in ('em', 'i'):
|
elif tag_name in ('em', 'i'):
|
||||||
text = node.get_text().strip()
|
text = node.get_text().strip()
|
||||||
if text:
|
if text:
|
||||||
self.output_lines.append(f'*{text}*')
|
self.output_lines.append(f'*{text}*')
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 默认递归处理子元素
|
# 默认递归处理子元素
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
self._process_node(child, indent, list_context)
|
self._process_node(child, indent, list_context)
|
||||||
|
|
||||||
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
if is_block and self.output_lines and not self.output_lines[-1].endswith('\n'):
|
||||||
self.output_lines.append('\n')
|
self.output_lines.append('\n')
|
||||||
|
|
||||||
def _process_table(self, table: Tag, indent: int) -> None:
|
def _process_table(self, table: Tag, indent: int) -> None:
|
||||||
"""处理表格"""
|
"""处理表格"""
|
||||||
try:
|
try:
|
||||||
rows = []
|
rows = []
|
||||||
for tr in table.find_all('tr'):
|
for tr in table.find_all('tr'):
|
||||||
row = []
|
row = []
|
||||||
for td in tr.find_all(['td', 'th']):
|
for td in tr.find_all(['td', 'th']):
|
||||||
row.append(td.get_text().strip())
|
row.append(td.get_text().strip())
|
||||||
if row:
|
if row:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
if rows:
|
if rows:
|
||||||
# 计算列宽
|
# 计算列宽
|
||||||
col_widths = []
|
col_widths = []
|
||||||
max_cols = max(len(r) for r in rows)
|
max_cols = max(len(r) for r in rows)
|
||||||
for i in range(max_cols):
|
for i in range(max_cols):
|
||||||
col_width = max((len(r[i]) if i < len(r) else 0) for r in rows)
|
col_width = max((len(r[i]) if i < len(r) else 0) for r in rows)
|
||||||
col_widths.append(col_width)
|
col_widths.append(col_width)
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
line = ' ' * indent
|
line = ' ' * indent
|
||||||
for i, cell in enumerate(row):
|
for i, cell in enumerate(row):
|
||||||
width = col_widths[i] if i < len(col_widths) else 0
|
width = col_widths[i] if i < len(col_widths) else 0
|
||||||
line += cell.ljust(width) + ' '
|
line += cell.ljust(width) + ' '
|
||||||
self.output_lines.append(line.rstrip() + '\n')
|
self.output_lines.append(line.rstrip() + '\n')
|
||||||
self.output_lines.append('\n')
|
self.output_lines.append('\n')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"处理表格失败: {e}")
|
logger.warning(f"处理表格失败: {e}")
|
||||||
# 降级处理:简单提取表格文本
|
# 降级处理:简单提取表格文本
|
||||||
table_text = table.get_text().strip()
|
table_text = table.get_text().strip()
|
||||||
if table_text:
|
if table_text:
|
||||||
self.output_lines.append(' ' * indent + table_text + '\n')
|
self.output_lines.append(' ' * indent + table_text + '\n')
|
||||||
|
|
||||||
def _process_list(self, ul: Tag, indent: int, list_type: str) -> None:
|
def _process_list(self, ul: Tag, indent: int, list_type: str) -> None:
|
||||||
"""处理列表"""
|
"""处理列表"""
|
||||||
try:
|
try:
|
||||||
counter = 1 if list_type == 'ol' else None
|
counter = 1 if list_type == 'ol' else None
|
||||||
for child in ul.children:
|
for child in ul.children:
|
||||||
if isinstance(child, Tag) and child.name == 'li':
|
if isinstance(child, Tag) and child.name == 'li':
|
||||||
ctx = (list_type, counter) if counter else (list_type, 1)
|
ctx = (list_type, counter) if counter else (list_type, 1)
|
||||||
self._process_list_item(child, indent, ctx)
|
self._process_list_item(child, indent, ctx)
|
||||||
if counter:
|
if counter:
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
self._process_node(child, indent, (list_type, 1) if not counter else None)
|
self._process_node(child, indent, (list_type, 1) if not counter else None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"处理列表失败: {e}")
|
logger.warning(f"处理列表失败: {e}")
|
||||||
|
|
||||||
def _process_list_item(self, li: Tag, indent: int, list_context: Optional[tuple]) -> None:
|
def _process_list_item(self, li: Tag, indent: int, list_context: Optional[tuple]) -> None:
|
||||||
"""处理列表项"""
|
"""处理列表项"""
|
||||||
try:
|
try:
|
||||||
prefix = ''
|
prefix = ''
|
||||||
if list_context:
|
if list_context:
|
||||||
list_type, num = list_context
|
list_type, num = list_context
|
||||||
prefix = '• ' if list_type == 'ul' else f'{num}. '
|
prefix = '• ' if list_type == 'ul' else f'{num}. '
|
||||||
|
|
||||||
# 收集直接文本
|
# 收集直接文本
|
||||||
direct_parts = []
|
direct_parts = []
|
||||||
for child in li.children:
|
for child in li.children:
|
||||||
if isinstance(child, NavigableString):
|
if isinstance(child, NavigableString):
|
||||||
text = str(child).strip()
|
text = str(child).strip()
|
||||||
if text:
|
if text:
|
||||||
direct_parts.append(text)
|
direct_parts.append(text)
|
||||||
elif isinstance(child, Tag) and child.name == 'a':
|
elif isinstance(child, Tag) and child.name == 'a':
|
||||||
href = child.get('href', '')
|
href = child.get('href', '')
|
||||||
link_text = child.get_text().strip()
|
link_text = child.get_text().strip()
|
||||||
if href and link_text:
|
if href and link_text:
|
||||||
direct_parts.append(f'{link_text} ({href})')
|
direct_parts.append(f'{link_text} ({href})')
|
||||||
|
|
||||||
if direct_parts:
|
if direct_parts:
|
||||||
self.output_lines.append(' ' * indent + prefix + ' '.join(direct_parts) + '\n')
|
self.output_lines.append(' ' * indent + prefix + ' '.join(direct_parts) + '\n')
|
||||||
|
|
||||||
# 处理子元素
|
# 处理子元素
|
||||||
for child in li.children:
|
for child in li.children:
|
||||||
if isinstance(child, Tag) and child.name != 'a':
|
if isinstance(child, Tag) and child.name != 'a':
|
||||||
self._process_node(child, indent + 2, None)
|
self._process_node(child, indent + 2, None)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"处理列表项失败: {e}")
|
logger.warning(f"处理列表项失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
extractor = HTMLTextExtractor()
|
extractor = HTMLTextExtractor()
|
||||||
|
|
||||||
# 测试正常HTML
|
# 测试正常HTML
|
||||||
html = "<h1>标题</h1><p>段落</p><ul><li>项目1</li><li>项目2</li></ul>"
|
html = "<h1>标题</h1><p>段落</p><ul><li>项目1</li><li>项目2</li></ul>"
|
||||||
try:
|
try:
|
||||||
result = extractor.extract(html)
|
result = extractor.extract(html)
|
||||||
print(f"测试1 - 正常HTML提取结果:\n{result}")
|
print(f"测试1 - 正常HTML提取结果:\n{result}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"测试1失败: {e}")
|
print(f"测试1失败: {e}")
|
||||||
|
|
||||||
# 测试空HTML
|
# 测试空HTML
|
||||||
try:
|
try:
|
||||||
result = extractor.extract("")
|
result = extractor.extract("")
|
||||||
print(f"测试2 - 空HTML提取结果: '{result}'")
|
print(f"测试2 - 空HTML提取结果: '{result}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"测试2失败: {e}")
|
print(f"测试2失败: {e}")
|
||||||
|
|
||||||
# 测试无效HTML
|
# 测试无效HTML
|
||||||
try:
|
try:
|
||||||
result = extractor.extract("<invalid>html")
|
result = extractor.extract("<invalid>html")
|
||||||
print(f"测试3 - 无效HTML提取结果:\n{result}")
|
print(f"测试3 - 无效HTML提取结果:\n{result}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"测试3失败: {e}")
|
print(f"测试3失败: {e}")
|
||||||
|
|
||||||
# 测试表格
|
# 测试表格
|
||||||
table_html = """
|
table_html = """
|
||||||
<table>
|
<table>
|
||||||
<tr><th>姓名</th><th>年龄</th></tr>
|
<tr><th>姓名</th><th>年龄</th></tr>
|
||||||
<tr><td>张三</td><td>25</td></tr>
|
<tr><td>张三</td><td>25</td></tr>
|
||||||
<tr><td>李四</td><td>30</td></tr>
|
<tr><td>李四</td><td>30</td></tr>
|
||||||
</table>
|
</table>
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = extractor.extract(table_html)
|
result = extractor.extract(table_html)
|
||||||
print(f"测试4 - 表格提取结果:\n{result}")
|
print(f"测试4 - 表格提取结果:\n{result}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"测试4失败: {e}")
|
print(f"测试4失败: {e}")
|
||||||
BIN
src/data/daily_logs.db
Normal file
BIN
src/data/daily_logs.db
Normal file
Binary file not shown.
@@ -1,15 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
数据库模块包
|
数据库模块包
|
||||||
提供统一的数据库接口
|
提供统一的数据库接口
|
||||||
"""
|
"""
|
||||||
from src.database.base import DatabaseBase, DatabaseConnectionError
|
from src.database.base import DatabaseBase, DatabaseConnectionError
|
||||||
from src.database.daily_logs import DailyLogsDatabase
|
from src.database.daily_logs import DailyLogsDatabase
|
||||||
from src.database.schedules import ScheduleDatabase
|
from src.database.schedules import ScheduleDatabase
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'DatabaseBase',
|
'DatabaseBase',
|
||||||
'DatabaseConnectionError',
|
'DatabaseConnectionError',
|
||||||
'DailyLogsDatabase',
|
'DailyLogsDatabase',
|
||||||
'ScheduleDatabase'
|
'ScheduleDatabase'
|
||||||
]
|
]
|
||||||
@@ -1,257 +1,257 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
数据库基类模块
|
数据库基类模块
|
||||||
提供统一的数据库连接管理和上下文管理器
|
提供统一的数据库连接管理和上下文管理器
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Generator, Optional, Any
|
from typing import Generator, Optional, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConnectionError(Exception):
|
class DatabaseConnectionError(Exception):
|
||||||
"""数据库连接错误"""
|
"""数据库连接错误"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DatabaseBase:
|
class DatabaseBase:
|
||||||
"""数据库基类,提供统一的连接管理"""
|
"""数据库基类,提供统一的连接管理"""
|
||||||
|
|
||||||
def __init__(self, db_path: Optional[str] = None):
|
def __init__(self, db_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
初始化数据库基类
|
初始化数据库基类
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||||
"""
|
"""
|
||||||
self.db_path = db_path or config.DATABASE_PATH
|
self.db_path = db_path or config.DATABASE_PATH
|
||||||
self._connection: Optional[sqlite3.Connection] = None
|
self._connection: Optional[sqlite3.Connection] = None
|
||||||
self._ensure_directory()
|
self._ensure_directory()
|
||||||
|
|
||||||
def _ensure_directory(self):
|
def _ensure_directory(self):
|
||||||
"""确保数据库目录存在"""
|
"""确保数据库目录存在"""
|
||||||
data_dir = os.path.dirname(self.db_path)
|
data_dir = os.path.dirname(self.db_path)
|
||||||
if data_dir and not os.path.exists(data_dir):
|
if data_dir and not os.path.exists(data_dir):
|
||||||
os.makedirs(data_dir)
|
os.makedirs(data_dir)
|
||||||
logger.info(f"创建数据库目录: {data_dir}")
|
logger.info(f"创建数据库目录: {data_dir}")
|
||||||
|
|
||||||
def _connect(self) -> sqlite3.Connection:
|
def _connect(self) -> sqlite3.Connection:
|
||||||
"""
|
"""
|
||||||
创建数据库连接
|
创建数据库连接
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
sqlite3.Connection 对象
|
sqlite3.Connection 对象
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
DatabaseConnectionError: 连接失败时抛出
|
DatabaseConnectionError: 连接失败时抛出
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
logger.debug(f"数据库连接已建立: {self.db_path}")
|
logger.debug(f"数据库连接已建立: {self.db_path}")
|
||||||
return conn
|
return conn
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
error_msg = f"数据库连接失败: {self.db_path}, 错误: {e}"
|
error_msg = f"数据库连接失败: {self.db_path}, 错误: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise DatabaseConnectionError(error_msg) from e
|
raise DatabaseConnectionError(error_msg) from e
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||||
"""
|
"""
|
||||||
获取数据库连接的上下文管理器
|
获取数据库连接的上下文管理器
|
||||||
|
|
||||||
使用示例:
|
使用示例:
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(...)
|
cursor.execute(...)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
数据库连接对象
|
数据库连接对象
|
||||||
"""
|
"""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = self._connect()
|
conn = self._connect()
|
||||||
yield conn
|
yield conn
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
logger.error(f"数据库操作失败: {e}")
|
logger.error(f"数据库操作失败: {e}")
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
logger.debug("数据库连接已关闭")
|
logger.debug("数据库连接已关闭")
|
||||||
|
|
||||||
def execute_query(self, query: str, params: tuple = ()) -> list:
|
def execute_query(self, query: str, params: tuple = ()) -> list:
|
||||||
"""
|
"""
|
||||||
执行查询并返回结果
|
执行查询并返回结果
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
query: SQL查询语句
|
query: SQL查询语句
|
||||||
params: 查询参数
|
params: 查询参数
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
查询结果列表
|
查询结果列表
|
||||||
"""
|
"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(query, params)
|
cursor.execute(query, params)
|
||||||
return [dict(row) for row in cursor.fetchall()]
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
def execute_update(self, query: str, params: tuple = ()) -> int:
|
def execute_update(self, query: str, params: tuple = ()) -> int:
|
||||||
"""
|
"""
|
||||||
执行更新操作
|
执行更新操作
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
query: SQL更新语句
|
query: SQL更新语句
|
||||||
params: 更新参数
|
params: 更新参数
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
受影响的行数
|
受影响的行数
|
||||||
"""
|
"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(query, params)
|
cursor.execute(query, params)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|
||||||
def execute_many(self, query: str, params_list: list) -> int:
|
def execute_many(self, query: str, params_list: list) -> int:
|
||||||
"""
|
"""
|
||||||
批量执行操作
|
批量执行操作
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
query: SQL语句
|
query: SQL语句
|
||||||
params_list: 参数列表
|
params_list: 参数列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
受影响的总行数
|
受影响的总行数
|
||||||
"""
|
"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.executemany(query, params_list)
|
cursor.executemany(query, params_list)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|
||||||
def table_exists(self, table_name: str) -> bool:
|
def table_exists(self, table_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
检查表是否存在
|
检查表是否存在
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
table_name: 表名
|
table_name: 表名
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
表是否存在
|
表是否存在
|
||||||
"""
|
"""
|
||||||
query = """
|
query = """
|
||||||
SELECT name FROM sqlite_master
|
SELECT name FROM sqlite_master
|
||||||
WHERE type='table' AND name=?
|
WHERE type='table' AND name=?
|
||||||
"""
|
"""
|
||||||
result = self.execute_query(query, (table_name,))
|
result = self.execute_query(query, (table_name,))
|
||||||
return len(result) > 0
|
return len(result) > 0
|
||||||
|
|
||||||
def get_table_info(self, table_name: str) -> list:
|
def get_table_info(self, table_name: str) -> list:
|
||||||
"""
|
"""
|
||||||
获取表结构信息
|
获取表结构信息
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
table_name: 表名
|
table_name: 表名
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
表结构信息列表
|
表结构信息列表
|
||||||
"""
|
"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
return [dict(row) for row in cursor.fetchall()]
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
def vacuum(self):
|
def vacuum(self):
|
||||||
"""执行数据库整理"""
|
"""执行数据库整理"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
conn.execute("VACUUM")
|
conn.execute("VACUUM")
|
||||||
logger.info("数据库整理完成")
|
logger.info("数据库整理完成")
|
||||||
|
|
||||||
def backup(self, backup_path: Optional[str] = None):
|
def backup(self, backup_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
备份数据库
|
备份数据库
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
backup_path: 备份文件路径,如果为None则使用默认路径
|
backup_path: 备份文件路径,如果为None则使用默认路径
|
||||||
"""
|
"""
|
||||||
if backup_path is None:
|
if backup_path is None:
|
||||||
backup_dir = "backups"
|
backup_dir = "backups"
|
||||||
os.makedirs(backup_dir, exist_ok=True)
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
timestamp = os.path.getmtime(self.db_path)
|
timestamp = os.path.getmtime(self.db_path)
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
dt = datetime.fromtimestamp(timestamp)
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
backup_path = os.path.join(
|
backup_path = os.path.join(
|
||||||
backup_dir,
|
backup_dir,
|
||||||
f"backup_{dt.strftime('%Y%m%d_%H%M%S')}.db"
|
f"backup_{dt.strftime('%Y%m%d_%H%M%S')}.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self.get_connection() as src_conn:
|
with self.get_connection() as src_conn:
|
||||||
dest_conn = sqlite3.connect(backup_path)
|
dest_conn = sqlite3.connect(backup_path)
|
||||||
src_conn.backup(dest_conn)
|
src_conn.backup(dest_conn)
|
||||||
dest_conn.close()
|
dest_conn.close()
|
||||||
logger.info(f"数据库备份完成: {backup_path}")
|
logger.info(f"数据库备份完成: {backup_path}")
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
logger.error(f"数据库备份失败: {e}")
|
logger.error(f"数据库备份失败: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
# 全局数据库连接池(可选,用于高性能场景)
|
# 全局数据库连接池(可选,用于高性能场景)
|
||||||
class ConnectionPool:
|
class ConnectionPool:
|
||||||
"""简单的数据库连接池"""
|
"""简单的数据库连接池"""
|
||||||
|
|
||||||
def __init__(self, db_path: str, max_connections: int = 5):
|
def __init__(self, db_path: str, max_connections: int = 5):
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.max_connections = max_connections
|
self.max_connections = max_connections
|
||||||
self._connections: list[sqlite3.Connection] = []
|
self._connections: list[sqlite3.Connection] = []
|
||||||
self._in_use: set[sqlite3.Connection] = set()
|
self._in_use: set[sqlite3.Connection] = set()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
||||||
"""从连接池获取连接"""
|
"""从连接池获取连接"""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
if self._connections:
|
if self._connections:
|
||||||
conn = self._connections.pop()
|
conn = self._connections.pop()
|
||||||
elif len(self._in_use) < self.max_connections:
|
elif len(self._in_use) < self.max_connections:
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
else:
|
else:
|
||||||
raise DatabaseConnectionError("连接池已满")
|
raise DatabaseConnectionError("连接池已满")
|
||||||
|
|
||||||
self._in_use.add(conn)
|
self._in_use.add(conn)
|
||||||
yield conn
|
yield conn
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
self._in_use.remove(conn)
|
self._in_use.remove(conn)
|
||||||
self._connections.append(conn)
|
self._connections.append(conn)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试数据库基类
|
# 测试数据库基类
|
||||||
db = DatabaseBase()
|
db = DatabaseBase()
|
||||||
|
|
||||||
# 测试连接
|
# 测试连接
|
||||||
with db.get_connection() as conn:
|
with db.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT sqlite_version()")
|
cursor.execute("SELECT sqlite_version()")
|
||||||
version = cursor.fetchone()[0]
|
version = cursor.fetchone()[0]
|
||||||
print(f"SQLite版本: {version}")
|
print(f"SQLite版本: {version}")
|
||||||
|
|
||||||
# 测试查询
|
# 测试查询
|
||||||
if db.table_exists("sqlite_master"):
|
if db.table_exists("sqlite_master"):
|
||||||
print("sqlite_master表存在")
|
print("sqlite_master表存在")
|
||||||
|
|
||||||
# 测试备份
|
# 测试备份
|
||||||
try:
|
try:
|
||||||
db.backup("test_backup.db")
|
db.backup("test_backup.db")
|
||||||
print("备份测试完成")
|
print("备份测试完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"备份测试失败: {e}")
|
print(f"备份测试失败: {e}")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,342 +1,342 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
排班人员数据库模块
|
排班人员数据库模块
|
||||||
基于新的数据库基类重构
|
基于新的数据库基类重构
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import List, Dict, Optional, Any
|
from typing import List, Dict, Optional, Any
|
||||||
|
|
||||||
from src.database.base import DatabaseBase
|
from src.database.base import DatabaseBase
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScheduleDatabase(DatabaseBase):
|
class ScheduleDatabase(DatabaseBase):
|
||||||
"""排班人员数据库"""
|
"""排班人员数据库"""
|
||||||
|
|
||||||
def __init__(self, db_path: Optional[str] = None):
|
def __init__(self, db_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
初始化数据库
|
初始化数据库
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
db_path: 数据库文件路径,如果为None则使用默认配置
|
db_path: 数据库文件路径,如果为None则使用默认配置
|
||||||
"""
|
"""
|
||||||
super().__init__(db_path)
|
super().__init__(db_path)
|
||||||
self._init_schema()
|
self._init_schema()
|
||||||
|
|
||||||
def _init_schema(self):
|
def _init_schema(self):
|
||||||
"""初始化表结构"""
|
"""初始化表结构"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 创建排班人员表
|
# 创建排班人员表
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS schedule_personnel (
|
CREATE TABLE IF NOT EXISTS schedule_personnel (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
day_shift TEXT,
|
day_shift TEXT,
|
||||||
night_shift TEXT,
|
night_shift TEXT,
|
||||||
day_shift_list TEXT, -- JSON数组
|
day_shift_list TEXT, -- JSON数组
|
||||||
night_shift_list TEXT, -- JSON数组
|
night_shift_list TEXT, -- JSON数组
|
||||||
sheet_id TEXT,
|
sheet_id TEXT,
|
||||||
sheet_title TEXT,
|
sheet_title TEXT,
|
||||||
data_hash TEXT, -- 数据哈希,用于检测更新
|
data_hash TEXT, -- 数据哈希,用于检测更新
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(date)
|
UNIQUE(date)
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 创建表格版本表(用于检测表格是否有更新)
|
# 创建表格版本表(用于检测表格是否有更新)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS sheet_versions (
|
CREATE TABLE IF NOT EXISTS sheet_versions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
sheet_id TEXT NOT NULL,
|
sheet_id TEXT NOT NULL,
|
||||||
sheet_title TEXT NOT NULL,
|
sheet_title TEXT NOT NULL,
|
||||||
revision INTEGER NOT NULL,
|
revision INTEGER NOT NULL,
|
||||||
data_hash TEXT,
|
data_hash TEXT,
|
||||||
last_checked_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
last_checked_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(sheet_id)
|
UNIQUE(sheet_id)
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 创建索引
|
# 创建索引
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_date ON schedule_personnel(date)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_date ON schedule_personnel(date)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_sheet ON schedule_personnel(sheet_id)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_schedule_sheet ON schedule_personnel(sheet_id)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sheet_versions ON sheet_versions(sheet_id)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sheet_versions ON sheet_versions(sheet_id)')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.debug("排班数据库表结构初始化完成")
|
logger.debug("排班数据库表结构初始化完成")
|
||||||
|
|
||||||
def _calculate_hash(self, data: Dict[str, Any]) -> str:
|
def _calculate_hash(self, data: Dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
计算数据哈希值
|
计算数据哈希值
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
data: 数据字典
|
data: 数据字典
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
MD5哈希值
|
MD5哈希值
|
||||||
"""
|
"""
|
||||||
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
|
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
|
||||||
return hashlib.md5(data_str.encode('utf-8')).hexdigest()
|
return hashlib.md5(data_str.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
def check_sheet_update(self, sheet_id: str, sheet_title: str, revision: int, data: Dict[str, Any]) -> bool:
|
def check_sheet_update(self, sheet_id: str, sheet_title: str, revision: int, data: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
检查表格是否有更新
|
检查表格是否有更新
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
sheet_id: 表格ID
|
sheet_id: 表格ID
|
||||||
sheet_title: 表格标题
|
sheet_title: 表格标题
|
||||||
revision: 表格版本号
|
revision: 表格版本号
|
||||||
data: 表格数据
|
data: 表格数据
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
True: 有更新,需要重新获取
|
True: 有更新,需要重新获取
|
||||||
False: 无更新,可以使用缓存
|
False: 无更新,可以使用缓存
|
||||||
"""
|
"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 查询当前版本
|
# 查询当前版本
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'SELECT revision, data_hash FROM sheet_versions WHERE sheet_id = ?',
|
'SELECT revision, data_hash FROM sheet_versions WHERE sheet_id = ?',
|
||||||
(sheet_id,)
|
(sheet_id,)
|
||||||
)
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
# 第一次获取,记录版本
|
# 第一次获取,记录版本
|
||||||
data_hash = self._calculate_hash(data)
|
data_hash = self._calculate_hash(data)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO sheet_versions (sheet_id, sheet_title, revision, data_hash, last_checked_at)
|
INSERT INTO sheet_versions (sheet_id, sheet_title, revision, data_hash, last_checked_at)
|
||||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
''', (sheet_id, sheet_title, revision, data_hash))
|
''', (sheet_id, sheet_title, revision, data_hash))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.debug(f"首次记录表格版本: {sheet_title} (ID: {sheet_id})")
|
logger.debug(f"首次记录表格版本: {sheet_title} (ID: {sheet_id})")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 检查版本号或数据是否有变化
|
# 检查版本号或数据是否有变化
|
||||||
old_revision = result['revision']
|
old_revision = result['revision']
|
||||||
old_hash = result['data_hash']
|
old_hash = result['data_hash']
|
||||||
new_hash = self._calculate_hash(data)
|
new_hash = self._calculate_hash(data)
|
||||||
|
|
||||||
if old_revision != revision or old_hash != new_hash:
|
if old_revision != revision or old_hash != new_hash:
|
||||||
# 有更新,更新版本信息
|
# 有更新,更新版本信息
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE sheet_versions
|
UPDATE sheet_versions
|
||||||
SET revision = ?, data_hash = ?, last_checked_at = CURRENT_TIMESTAMP
|
SET revision = ?, data_hash = ?, last_checked_at = CURRENT_TIMESTAMP
|
||||||
WHERE sheet_id = ?
|
WHERE sheet_id = ?
|
||||||
''', (revision, new_hash, sheet_id))
|
''', (revision, new_hash, sheet_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info(f"表格有更新: {sheet_title} (ID: {sheet_id})")
|
logger.info(f"表格有更新: {sheet_title} (ID: {sheet_id})")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 无更新,更新检查时间
|
# 无更新,更新检查时间
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE sheet_versions
|
UPDATE sheet_versions
|
||||||
SET last_checked_at = CURRENT_TIMESTAMP
|
SET last_checked_at = CURRENT_TIMESTAMP
|
||||||
WHERE sheet_id = ?
|
WHERE sheet_id = ?
|
||||||
''', (sheet_id,))
|
''', (sheet_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.debug(f"表格无更新: {sheet_title} (ID: {sheet_id})")
|
logger.debug(f"表格无更新: {sheet_title} (ID: {sheet_id})")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def save_schedule(self, date: str, schedule_data: Dict[str, Any],
|
def save_schedule(self, date: str, schedule_data: Dict[str, Any],
|
||||||
sheet_id: Optional[str] = None, sheet_title: Optional[str] = None) -> bool:
|
sheet_id: Optional[str] = None, sheet_title: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
保存排班信息到数据库
|
保存排班信息到数据库
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date: 日期 (YYYY-MM-DD)
|
date: 日期 (YYYY-MM-DD)
|
||||||
schedule_data: 排班数据
|
schedule_data: 排班数据
|
||||||
sheet_id: 表格ID
|
sheet_id: 表格ID
|
||||||
sheet_title: 表格标题
|
sheet_title: 表格标题
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
是否成功
|
是否成功
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 准备数据
|
# 准备数据
|
||||||
day_shift = schedule_data.get('day_shift', '')
|
day_shift = schedule_data.get('day_shift', '')
|
||||||
night_shift = schedule_data.get('night_shift', '')
|
night_shift = schedule_data.get('night_shift', '')
|
||||||
day_shift_list = json.dumps(schedule_data.get('day_shift_list', []), ensure_ascii=False)
|
day_shift_list = json.dumps(schedule_data.get('day_shift_list', []), ensure_ascii=False)
|
||||||
night_shift_list = json.dumps(schedule_data.get('night_shift_list', []), ensure_ascii=False)
|
night_shift_list = json.dumps(schedule_data.get('night_shift_list', []), ensure_ascii=False)
|
||||||
data_hash = self._calculate_hash(schedule_data)
|
data_hash = self._calculate_hash(schedule_data)
|
||||||
|
|
||||||
# 使用 INSERT OR REPLACE 来更新已存在的记录
|
# 使用 INSERT OR REPLACE 来更新已存在的记录
|
||||||
query = '''
|
query = '''
|
||||||
INSERT OR REPLACE INTO schedule_personnel
|
INSERT OR REPLACE INTO schedule_personnel
|
||||||
(date, day_shift, night_shift, day_shift_list, night_shift_list,
|
(date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||||
sheet_id, sheet_title, data_hash, updated_at)
|
sheet_id, sheet_title, data_hash, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
'''
|
'''
|
||||||
params = (
|
params = (
|
||||||
date, day_shift, night_shift, day_shift_list, night_shift_list,
|
date, day_shift, night_shift, day_shift_list, night_shift_list,
|
||||||
sheet_id, sheet_title, data_hash
|
sheet_id, sheet_title, data_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
self.execute_update(query, params)
|
self.execute_update(query, params)
|
||||||
logger.debug(f"保存排班信息: {date}")
|
logger.debug(f"保存排班信息: {date}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存排班信息失败: {e}, 日期: {date}")
|
logger.error(f"保存排班信息失败: {e}, 日期: {date}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_schedule(self, date: str) -> Optional[Dict[str, Any]]:
|
def get_schedule(self, date: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取指定日期的排班信息
|
获取指定日期的排班信息
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date: 日期 (YYYY-MM-DD)
|
date: 日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
排班信息字典,未找到返回None
|
排班信息字典,未找到返回None
|
||||||
"""
|
"""
|
||||||
query = 'SELECT * FROM schedule_personnel WHERE date = ?'
|
query = 'SELECT * FROM schedule_personnel WHERE date = ?'
|
||||||
result = self.execute_query(query, (date,))
|
result = self.execute_query(query, (date,))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
row = result[0]
|
row = result[0]
|
||||||
|
|
||||||
# 解析JSON数组
|
# 解析JSON数组
|
||||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'date': row['date'],
|
'date': row['date'],
|
||||||
'day_shift': row['day_shift'],
|
'day_shift': row['day_shift'],
|
||||||
'night_shift': row['night_shift'],
|
'night_shift': row['night_shift'],
|
||||||
'day_shift_list': day_shift_list,
|
'day_shift_list': day_shift_list,
|
||||||
'night_shift_list': night_shift_list,
|
'night_shift_list': night_shift_list,
|
||||||
'sheet_id': row['sheet_id'],
|
'sheet_id': row['sheet_id'],
|
||||||
'sheet_title': row['sheet_title'],
|
'sheet_title': row['sheet_title'],
|
||||||
'updated_at': row['updated_at']
|
'updated_at': row['updated_at']
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取日期范围内的排班信息
|
获取日期范围内的排班信息
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
start_date: 开始日期 (YYYY-MM-DD)
|
start_date: 开始日期 (YYYY-MM-DD)
|
||||||
end_date: 结束日期 (YYYY-MM-DD)
|
end_date: 结束日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
排班信息列表
|
排班信息列表
|
||||||
"""
|
"""
|
||||||
query = '''
|
query = '''
|
||||||
SELECT * FROM schedule_personnel
|
SELECT * FROM schedule_personnel
|
||||||
WHERE date >= ? AND date <= ?
|
WHERE date >= ? AND date <= ?
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
'''
|
'''
|
||||||
results = self.execute_query(query, (start_date, end_date))
|
results = self.execute_query(query, (start_date, end_date))
|
||||||
|
|
||||||
processed_results = []
|
processed_results = []
|
||||||
for row in results:
|
for row in results:
|
||||||
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
day_shift_list = json.loads(row['day_shift_list']) if row['day_shift_list'] else []
|
||||||
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
night_shift_list = json.loads(row['night_shift_list']) if row['night_shift_list'] else []
|
||||||
|
|
||||||
processed_results.append({
|
processed_results.append({
|
||||||
'date': row['date'],
|
'date': row['date'],
|
||||||
'day_shift': row['day_shift'],
|
'day_shift': row['day_shift'],
|
||||||
'night_shift': row['night_shift'],
|
'night_shift': row['night_shift'],
|
||||||
'day_shift_list': day_shift_list,
|
'day_shift_list': day_shift_list,
|
||||||
'night_shift_list': night_shift_list,
|
'night_shift_list': night_shift_list,
|
||||||
'sheet_id': row['sheet_id'],
|
'sheet_id': row['sheet_id'],
|
||||||
'sheet_title': row['sheet_title'],
|
'sheet_title': row['sheet_title'],
|
||||||
'updated_at': row['updated_at']
|
'updated_at': row['updated_at']
|
||||||
})
|
})
|
||||||
|
|
||||||
return processed_results
|
return processed_results
|
||||||
|
|
||||||
def delete_old_schedules(self, before_date: str) -> int:
|
def delete_old_schedules(self, before_date: str) -> int:
|
||||||
"""
|
"""
|
||||||
删除指定日期之前的排班记录
|
删除指定日期之前的排班记录
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
before_date: 日期 (YYYY-MM-DD)
|
before_date: 日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
删除的记录数
|
删除的记录数
|
||||||
"""
|
"""
|
||||||
query = 'DELETE FROM schedule_personnel WHERE date < ?'
|
query = 'DELETE FROM schedule_personnel WHERE date < ?'
|
||||||
return self.execute_update(query, (before_date,))
|
return self.execute_update(query, (before_date,))
|
||||||
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
"""获取统计信息"""
|
"""获取统计信息"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('SELECT COUNT(*) FROM schedule_personnel')
|
cursor.execute('SELECT COUNT(*) FROM schedule_personnel')
|
||||||
total = cursor.fetchone()[0]
|
total = cursor.fetchone()[0]
|
||||||
|
|
||||||
cursor.execute('SELECT MIN(date), MAX(date) FROM schedule_personnel')
|
cursor.execute('SELECT MIN(date), MAX(date) FROM schedule_personnel')
|
||||||
date_range = cursor.fetchone()
|
date_range = cursor.fetchone()
|
||||||
|
|
||||||
cursor.execute('SELECT COUNT(DISTINCT sheet_id) FROM schedule_personnel')
|
cursor.execute('SELECT COUNT(DISTINCT sheet_id) FROM schedule_personnel')
|
||||||
sheet_count = cursor.fetchone()[0]
|
sheet_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'total': total,
|
'total': total,
|
||||||
'date_range': {'start': date_range[0], 'end': date_range[1]},
|
'date_range': {'start': date_range[0], 'end': date_range[1]},
|
||||||
'sheet_count': sheet_count
|
'sheet_count': sheet_count
|
||||||
}
|
}
|
||||||
|
|
||||||
def clear_all(self) -> int:
|
def clear_all(self) -> int:
|
||||||
"""
|
"""
|
||||||
清空所有排班数据
|
清空所有排班数据
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
删除的记录数
|
删除的记录数
|
||||||
"""
|
"""
|
||||||
query1 = 'DELETE FROM schedule_personnel'
|
query1 = 'DELETE FROM schedule_personnel'
|
||||||
query2 = 'DELETE FROM sheet_versions'
|
query2 = 'DELETE FROM sheet_versions'
|
||||||
|
|
||||||
count1 = self.execute_update(query1)
|
count1 = self.execute_update(query1)
|
||||||
count2 = self.execute_update(query2)
|
count2 = self.execute_update(query2)
|
||||||
|
|
||||||
logger.info(f"清空排班数据,删除 {count1} 条排班记录和 {count2} 条版本记录")
|
logger.info(f"清空排班数据,删除 {count1} 条排班记录和 {count2} 条版本记录")
|
||||||
return count1 + count2
|
return count1 + count2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
db = ScheduleDatabase()
|
db = ScheduleDatabase()
|
||||||
|
|
||||||
# 测试保存
|
# 测试保存
|
||||||
test_schedule = {
|
test_schedule = {
|
||||||
'day_shift': '张勤、杨俊豪',
|
'day_shift': '张勤、杨俊豪',
|
||||||
'night_shift': '刘炜彬、梁启迟',
|
'night_shift': '刘炜彬、梁启迟',
|
||||||
'day_shift_list': ['张勤', '杨俊豪'],
|
'day_shift_list': ['张勤', '杨俊豪'],
|
||||||
'night_shift_list': ['刘炜彬', '梁启迟']
|
'night_shift_list': ['刘炜彬', '梁启迟']
|
||||||
}
|
}
|
||||||
|
|
||||||
success = db.save_schedule('2025-12-31', test_schedule, 'zcYLIk', '12月')
|
success = db.save_schedule('2025-12-31', test_schedule, 'zcYLIk', '12月')
|
||||||
print(f"保存测试: {'成功' if success else '失败'}")
|
print(f"保存测试: {'成功' if success else '失败'}")
|
||||||
|
|
||||||
# 测试获取
|
# 测试获取
|
||||||
schedule = db.get_schedule('2025-12-31')
|
schedule = db.get_schedule('2025-12-31')
|
||||||
print(f"获取结果: {schedule}")
|
print(f"获取结果: {schedule}")
|
||||||
|
|
||||||
# 测试范围查询
|
# 测试范围查询
|
||||||
schedules = db.get_schedule_by_range('2025-12-01', '2025-12-31')
|
schedules = db.get_schedule_by_range('2025-12-01', '2025-12-31')
|
||||||
print(f"范围查询: {len(schedules)} 条记录")
|
print(f"范围查询: {len(schedules)} 条记录")
|
||||||
|
|
||||||
# 测试统计
|
# 测试统计
|
||||||
stats = db.get_stats()
|
stats = db.get_stats()
|
||||||
print(f"统计信息: {stats}")
|
print(f"统计信息: {stats}")
|
||||||
|
|
||||||
# 测试表格版本检查
|
# 测试表格版本检查
|
||||||
test_data = {'values': [['姓名', '12月31日'], ['张三', '白']]}
|
test_data = {'values': [['姓名', '12月31日'], ['张三', '白']]}
|
||||||
needs_update = db.check_sheet_update('test_sheet', '测试表格', 1, test_data)
|
needs_update = db.check_sheet_update('test_sheet', '测试表格', 1, test_data)
|
||||||
print(f"表格更新检查: {'需要更新' if needs_update else '无需更新'}")
|
print(f"表格更新检查: {'需要更新' if needs_update else '无需更新'}")
|
||||||
|
|
||||||
# 清理测试数据
|
# 清理测试数据
|
||||||
db.delete_old_schedules('2026-01-01')
|
db.delete_old_schedules('2026-01-01')
|
||||||
print("测试数据已清理")
|
print("测试数据已清理")
|
||||||
348
src/error_handler.py
Normal file
348
src/error_handler.py
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
统一错误处理模块
|
||||||
|
提供自定义异常类和错误处理装饰器
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Any, Optional, Type
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OrbitInError(Exception):
|
||||||
|
"""OrbitIn 基础异常类"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(OrbitInError):
|
||||||
|
"""验证错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationError(OrbitInError):
|
||||||
|
"""配置错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseError(OrbitInError):
|
||||||
|
"""数据库错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkError(OrbitInError):
|
||||||
|
"""网络错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParsingError(OrbitInError):
|
||||||
|
"""解析错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReportError(OrbitInError):
|
||||||
|
"""报表生成错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def handle_errors(
|
||||||
|
default_return: Any = None,
|
||||||
|
reraise: bool = False,
|
||||||
|
log_level: int = logging.ERROR,
|
||||||
|
exception_types: Optional[tuple] = None
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
错误处理装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
default_return: 发生异常时返回的默认值
|
||||||
|
reraise: 是否重新抛出异常
|
||||||
|
log_level: 日志级别
|
||||||
|
exception_types: 要捕获的异常类型,None表示捕获所有异常
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@handle_errors(default_return=None, reraise=False)
|
||||||
|
def my_function():
|
||||||
|
# 可能抛出异常的代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
# 检查是否需要捕获此异常
|
||||||
|
if exception_types and not isinstance(e, exception_types):
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
func_name = func.__name__
|
||||||
|
module = func.__module__
|
||||||
|
|
||||||
|
log_message = f"Error in {module}.{func_name}: {str(e)}"
|
||||||
|
|
||||||
|
if log_level == logging.DEBUG:
|
||||||
|
logger.debug(log_message, exc_info=True)
|
||||||
|
elif log_level == logging.INFO:
|
||||||
|
logger.info(log_message, exc_info=True)
|
||||||
|
elif log_level == logging.WARNING:
|
||||||
|
logger.warning(log_message, exc_info=True)
|
||||||
|
elif log_level == logging.ERROR:
|
||||||
|
logger.error(log_message, exc_info=True)
|
||||||
|
elif log_level == logging.CRITICAL:
|
||||||
|
logger.critical(log_message, exc_info=True)
|
||||||
|
else:
|
||||||
|
logger.error(log_message, exc_info=True)
|
||||||
|
|
||||||
|
# 决定是否重新抛出异常
|
||||||
|
if reraise:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return default_return
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def validate_input(
|
||||||
|
date: Optional[str] = None,
|
||||||
|
ship_name: Optional[str] = None,
|
||||||
|
teu: Optional[int] = None,
|
||||||
|
twenty_feet: Optional[int] = None,
|
||||||
|
forty_feet: Optional[int] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
验证输入参数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||||
|
ship_name: 船名
|
||||||
|
teu: TEU数量
|
||||||
|
twenty_feet: 20尺箱量
|
||||||
|
forty_feet: 40尺箱量
|
||||||
|
|
||||||
|
异常:
|
||||||
|
ValidationError: 验证失败
|
||||||
|
"""
|
||||||
|
if date is not None:
|
||||||
|
if not isinstance(date, str):
|
||||||
|
raise ValidationError(f"日期必须是字符串类型,实际类型: {type(date)}")
|
||||||
|
try:
|
||||||
|
datetime.strptime(date, '%Y-%m-%d')
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(f"日期格式无效: {date},应为 YYYY-MM-DD")
|
||||||
|
|
||||||
|
if ship_name is not None:
|
||||||
|
if not isinstance(ship_name, str):
|
||||||
|
raise ValidationError(f"船名必须是字符串类型,实际类型: {type(ship_name)}")
|
||||||
|
if len(ship_name.strip()) == 0:
|
||||||
|
raise ValidationError("船名不能为空")
|
||||||
|
|
||||||
|
if teu is not None:
|
||||||
|
if not isinstance(teu, int):
|
||||||
|
raise ValidationError(f"TEU必须是整数类型,实际类型: {type(teu)}")
|
||||||
|
if teu < 0:
|
||||||
|
raise ValidationError(f"TEU数量不能为负数: {teu}")
|
||||||
|
|
||||||
|
if twenty_feet is not None:
|
||||||
|
if not isinstance(twenty_feet, int):
|
||||||
|
raise ValidationError(f"20尺箱量必须是整数类型,实际类型: {type(twenty_feet)}")
|
||||||
|
if twenty_feet < 0:
|
||||||
|
raise ValidationError(f"20尺箱量不能为负数: {twenty_feet}")
|
||||||
|
|
||||||
|
if forty_feet is not None:
|
||||||
|
if not isinstance(forty_feet, int):
|
||||||
|
raise ValidationError(f"40尺箱量必须是整数类型,实际类型: {type(forty_feet)}")
|
||||||
|
if forty_feet < 0:
|
||||||
|
raise ValidationError(f"40尺箱量不能为负数: {forty_feet}")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_execute(
|
||||||
|
func: Callable,
|
||||||
|
*args,
|
||||||
|
default_return: Any = None,
|
||||||
|
log_error: bool = True,
|
||||||
|
**kwargs
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
安全执行函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
func: 要执行的函数
|
||||||
|
*args: 位置参数
|
||||||
|
default_return: 发生异常时返回的默认值
|
||||||
|
log_error: 是否记录错误日志
|
||||||
|
**kwargs: 关键字参数
|
||||||
|
|
||||||
|
返回:
|
||||||
|
函数执行结果或默认值
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
result = safe_execute(
|
||||||
|
some_function,
|
||||||
|
arg1, arg2,
|
||||||
|
default_return=None,
|
||||||
|
log_error=True
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
if log_error:
|
||||||
|
func_name = getattr(func, '__name__', 'unknown_function')
|
||||||
|
module = getattr(func, '__module__', 'unknown_module')
|
||||||
|
logger.error(f"Error in {module}.{func_name}: {e}", exc_info=True)
|
||||||
|
return default_return
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorContext:
|
||||||
|
"""错误上下文管理器"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
operation_name: str,
|
||||||
|
reraise: bool = False,
|
||||||
|
log_level: int = logging.ERROR
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化错误上下文
|
||||||
|
|
||||||
|
参数:
|
||||||
|
operation_name: 操作名称
|
||||||
|
reraise: 是否重新抛出异常
|
||||||
|
log_level: 日志级别
|
||||||
|
"""
|
||||||
|
self.operation_name = operation_name
|
||||||
|
self.reraise = reraise
|
||||||
|
self.log_level = log_level
|
||||||
|
self.start_time = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
logger.debug(f"开始执行: {self.operation_name}")
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
elapsed = (datetime.now() - self.start_time).total_seconds()
|
||||||
|
|
||||||
|
if exc_type is None:
|
||||||
|
logger.debug(f"成功完成: {self.operation_name} (耗时: {elapsed:.2f}s)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 发生异常
|
||||||
|
log_message = f"执行失败: {self.operation_name} (耗时: {elapsed:.2f}s) - {exc_val}"
|
||||||
|
|
||||||
|
if self.log_level == logging.DEBUG:
|
||||||
|
logger.debug(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
elif self.log_level == logging.INFO:
|
||||||
|
logger.info(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
elif self.log_level == logging.WARNING:
|
||||||
|
logger.warning(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
elif self.log_level == logging.ERROR:
|
||||||
|
logger.error(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
elif self.log_level == logging.CRITICAL:
|
||||||
|
logger.critical(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
else:
|
||||||
|
logger.error(log_message, exc_info=(exc_type, exc_val, exc_tb))
|
||||||
|
|
||||||
|
# 决定是否抑制异常
|
||||||
|
return not self.reraise
|
||||||
|
|
||||||
|
|
||||||
|
def convert_exception(
|
||||||
|
from_exception: Type[Exception],
|
||||||
|
to_exception: Type[Exception],
|
||||||
|
message: Optional[str] = None
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
异常转换装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
from_exception: 源异常类型
|
||||||
|
to_exception: 目标异常类型
|
||||||
|
message: 转换时的消息模板,可以使用 {original} 占位符
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@convert_exception(ValueError, ValidationError, "验证失败: {original}")
|
||||||
|
def my_function():
|
||||||
|
# 可能抛出 ValueError 的代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except from_exception as e:
|
||||||
|
if message:
|
||||||
|
new_message = message.format(original=str(e))
|
||||||
|
else:
|
||||||
|
new_message = str(e)
|
||||||
|
raise to_exception(new_message) from e
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def log_execution_time(func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
记录函数执行时间的装饰器
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@log_execution_time
|
||||||
|
def my_function():
|
||||||
|
# 函数代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
start_time = datetime.now()
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
elapsed = (datetime.now() - start_time).total_seconds()
|
||||||
|
logger.info(f"{func.__name__} 执行成功,耗时: {elapsed:.2f}s")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = (datetime.now() - start_time).total_seconds()
|
||||||
|
logger.error(f"{func.__name__} 执行失败,耗时: {elapsed:.2f}s,错误: {e}")
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试代码
|
||||||
|
|
||||||
|
# 测试错误处理装饰器
|
||||||
|
@handle_errors(default_return="error", reraise=False)
|
||||||
|
def test_function():
|
||||||
|
raise ValueError("测试异常")
|
||||||
|
|
||||||
|
result = test_function()
|
||||||
|
print(f"测试函数返回: {result}")
|
||||||
|
|
||||||
|
# 测试输入验证
|
||||||
|
try:
|
||||||
|
validate_input(date="2025-12-30", ship_name="测试船", teu=100)
|
||||||
|
print("输入验证通过")
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"输入验证失败: {e}")
|
||||||
|
|
||||||
|
# 测试错误上下文管理器
|
||||||
|
try:
|
||||||
|
with ErrorContext("测试操作", reraise=True):
|
||||||
|
print("执行测试操作...")
|
||||||
|
raise RuntimeError("测试错误")
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"捕获到错误: {e}")
|
||||||
|
|
||||||
|
# 测试异常转换
|
||||||
|
@convert_exception(ValueError, ValidationError, "验证失败: {original}")
|
||||||
|
def test_convert():
|
||||||
|
raise ValueError("原始错误")
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_convert()
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"转换后的异常: {e}")
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
飞书模块包
|
飞书模块包
|
||||||
提供统一的飞书API接口
|
提供统一的飞书API接口
|
||||||
"""
|
"""
|
||||||
from src.feishu.client import FeishuSheetsClient, FeishuClientError
|
from src.feishu.client import FeishuSheetsClient, FeishuClientError
|
||||||
from src.feishu.parser import ScheduleDataParser
|
from src.feishu.parser import ScheduleDataParser
|
||||||
from src.feishu.manager import FeishuScheduleManager
|
from src.feishu.manager import FeishuScheduleManager
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'FeishuSheetsClient',
|
'FeishuSheetsClient',
|
||||||
'FeishuClientError',
|
'FeishuClientError',
|
||||||
'ScheduleDataParser',
|
'ScheduleDataParser',
|
||||||
'FeishuScheduleManager'
|
'FeishuScheduleManager'
|
||||||
]
|
]
|
||||||
@@ -1,182 +1,367 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
飞书表格 API 客户端模块
|
飞书表格 API 客户端模块
|
||||||
统一版本,支持月度表格和年度表格
|
统一版本,支持月度表格和年度表格
|
||||||
"""
|
支持自动获取和刷新 tenant_access_token
|
||||||
import requests
|
"""
|
||||||
from typing import Dict, List, Optional
|
import requests
|
||||||
import logging
|
import time
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
from src.config import config
|
import logging
|
||||||
from src.logging_config import get_logger
|
|
||||||
|
from src.config import config
|
||||||
logger = get_logger(__name__)
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
class FeishuClientError(Exception):
|
|
||||||
"""飞书客户端异常基类"""
|
|
||||||
pass
|
class FeishuClientError(Exception):
|
||||||
|
"""飞书客户端异常基类"""
|
||||||
|
pass
|
||||||
class FeishuSheetsClient:
|
|
||||||
"""飞书表格 API 客户端"""
|
|
||||||
|
class FeishuSheetsClient:
|
||||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
"""飞书表格 API 客户端"""
|
||||||
spreadsheet_token: Optional[str] = None):
|
|
||||||
"""
|
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||||
初始化客户端
|
spreadsheet_token: Optional[str] = None, app_id: Optional[str] = None,
|
||||||
|
app_secret: Optional[str] = None):
|
||||||
参数:
|
"""
|
||||||
base_url: 飞书 API 基础URL,如果为None则使用配置
|
初始化客户端
|
||||||
token: Bearer 认证令牌,如果为None则使用配置
|
|
||||||
spreadsheet_token: 表格 token,如果为None则使用配置
|
参数:
|
||||||
"""
|
base_url: 飞书 API 基础URL,如果为None则使用配置
|
||||||
self.base_url = (base_url or config.FEISHU_BASE_URL).rstrip('/')
|
token: Bearer 认证令牌,如果为None则使用配置或自动获取
|
||||||
self.spreadsheet_token = spreadsheet_token or config.FEISHU_SPREADSHEET_TOKEN
|
spreadsheet_token: 表格 token,如果为None则使用配置
|
||||||
self.token = token or config.FEISHU_TOKEN
|
app_id: 飞书应用ID,用于获取tenant_access_token
|
||||||
|
app_secret: 飞书应用密钥,用于获取tenant_access_token
|
||||||
self.headers = {
|
"""
|
||||||
'Authorization': f'Bearer {self.token}',
|
self.base_url = (base_url or config.FEISHU_BASE_URL).rstrip('/')
|
||||||
'Content-Type': 'application/json',
|
self.spreadsheet_token = spreadsheet_token or config.FEISHU_SPREADSHEET_TOKEN
|
||||||
'Accept': 'application/json'
|
self.app_id = app_id or config.FEISHU_APP_ID
|
||||||
}
|
self.app_secret = app_secret or config.FEISHU_APP_SECRET
|
||||||
|
|
||||||
# 使用 Session 重用连接
|
# Token管理相关属性
|
||||||
self.session = requests.Session()
|
self._token = token or config.FEISHU_TOKEN
|
||||||
self.session.headers.update(self.headers)
|
self._token_expire_time = 0 # token过期时间戳
|
||||||
self.session.timeout = config.REQUEST_TIMEOUT
|
self._token_obtained_time = 0 # token获取时间戳
|
||||||
|
|
||||||
logger.debug(f"飞书客户端初始化完成,基础URL: {self.base_url}")
|
# 使用 Session 重用连接
|
||||||
|
self.session = requests.Session()
|
||||||
def get_sheets_info(self) -> List[Dict[str, str]]:
|
self.session.timeout = config.REQUEST_TIMEOUT
|
||||||
"""
|
|
||||||
获取所有表格信息(sheet_id 和 title)
|
# 初始化headers
|
||||||
|
self._update_session_headers()
|
||||||
返回:
|
|
||||||
表格信息列表 [{'sheet_id': 'xxx', 'title': 'xxx'}, ...]
|
logger.debug(f"飞书客户端初始化完成,基础URL: {self.base_url}")
|
||||||
|
logger.debug(f"使用应用ID: {self.app_id[:8]}... 如果配置" if self.app_id else "未配置应用ID")
|
||||||
异常:
|
|
||||||
requests.exceptions.RequestException: 网络请求失败
|
def _update_session_headers(self):
|
||||||
ValueError: API返回错误
|
"""更新session的headers"""
|
||||||
"""
|
if self._token:
|
||||||
url = f'{self.base_url}/spreadsheets/{self.spreadsheet_token}/sheets/query'
|
self.session.headers.update({
|
||||||
|
'Authorization': f'Bearer {self._token}',
|
||||||
try:
|
'Content-Type': 'application/json',
|
||||||
response = self.session.get(url, timeout=config.REQUEST_TIMEOUT)
|
'Accept': 'application/json'
|
||||||
response.raise_for_status()
|
})
|
||||||
data = response.json()
|
else:
|
||||||
|
# 如果没有token,移除Authorization头
|
||||||
if data.get('code') != 0:
|
if 'Authorization' in self.session.headers:
|
||||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
del self.session.headers['Authorization']
|
||||||
logger.error(error_msg)
|
|
||||||
raise ValueError(error_msg)
|
def _get_tenant_access_token(self) -> Tuple[str, int]:
|
||||||
|
"""
|
||||||
sheets = data.get('data', {}).get('sheets', [])
|
获取tenant_access_token
|
||||||
result = []
|
|
||||||
for sheet in sheets:
|
返回:
|
||||||
result.append({
|
(token, expire_time): token字符串和过期时间(秒)
|
||||||
'sheet_id': sheet.get('sheet_id'),
|
|
||||||
'title': sheet.get('title')
|
异常:
|
||||||
})
|
requests.exceptions.RequestException: 网络请求失败
|
||||||
|
ValueError: API返回错误
|
||||||
logger.info(f"获取到 {len(result)} 个表格")
|
"""
|
||||||
return result
|
if not self.app_id or not self.app_secret:
|
||||||
|
raise ValueError("未配置飞书应用ID和密钥,无法获取tenant_access_token")
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.error(f"获取表格信息失败: {e}")
|
token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
||||||
raise
|
|
||||||
except Exception as e:
|
payload = {
|
||||||
logger.error(f"解析表格信息失败: {e}")
|
"app_id": self.app_id,
|
||||||
raise
|
"app_secret": self.app_secret
|
||||||
|
}
|
||||||
def get_sheet_data(self, sheet_id: str, range_: Optional[str] = None) -> Dict:
|
|
||||||
"""
|
headers = {
|
||||||
获取指定表格的数据
|
"Content-Type": "application/json; charset=utf-8"
|
||||||
|
}
|
||||||
参数:
|
|
||||||
sheet_id: 表格ID
|
try:
|
||||||
range_: 数据范围,如果为None则使用配置
|
logger.info(f"正在获取tenant_access_token,应用ID: {self.app_id[:8]}...")
|
||||||
|
response = requests.post(token_url, json=payload, headers=headers, timeout=config.REQUEST_TIMEOUT)
|
||||||
返回:
|
response.raise_for_status()
|
||||||
飞书API返回的原始数据,包含revision版本号
|
data = response.json()
|
||||||
|
|
||||||
异常:
|
if data.get('code') != 0:
|
||||||
requests.exceptions.RequestException: 网络请求失败
|
error_msg = f"获取tenant_access_token失败: {data.get('msg')}"
|
||||||
ValueError: API返回错误
|
logger.error(error_msg)
|
||||||
"""
|
raise ValueError(error_msg)
|
||||||
if range_ is None:
|
|
||||||
range_ = config.SHEET_RANGE
|
token = data.get('tenant_access_token')
|
||||||
|
expire = data.get('expire', 7200) # 默认2小时
|
||||||
# 注意:获取表格数据使用 v2 API,而不是 v3
|
|
||||||
url = f'{self.base_url.replace("/v3", "/v2")}/spreadsheets/{self.spreadsheet_token}/values/{sheet_id}!{range_}'
|
if not token:
|
||||||
params = {
|
raise ValueError("API返回的token为空")
|
||||||
'valueRenderOption': 'ToString',
|
|
||||||
'dateTimeRenderOption': 'FormattedString'
|
logger.info(f"成功获取tenant_access_token,有效期: {expire}秒")
|
||||||
}
|
return token, expire
|
||||||
|
|
||||||
try:
|
except requests.exceptions.RequestException as e:
|
||||||
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
logger.error(f"获取tenant_access_token网络请求失败: {e}")
|
||||||
response.raise_for_status()
|
raise
|
||||||
data = response.json()
|
except Exception as e:
|
||||||
|
logger.error(f"获取tenant_access_token失败: {e}")
|
||||||
if data.get('code') != 0:
|
raise
|
||||||
error_msg = f"飞书API错误: {data.get('msg')}"
|
|
||||||
logger.error(error_msg)
|
def _ensure_valid_token(self):
|
||||||
raise ValueError(error_msg)
|
"""
|
||||||
|
确保当前token有效,如果无效则重新获取
|
||||||
logger.debug(f"获取表格数据成功: {sheet_id}, 范围: {range_}")
|
|
||||||
return data.get('data', {})
|
返回:
|
||||||
|
bool: token是否有效
|
||||||
except requests.exceptions.RequestException as e:
|
"""
|
||||||
logger.error(f"获取表格数据失败: {e}, sheet_id: {sheet_id}")
|
current_time = time.time()
|
||||||
raise
|
|
||||||
except Exception as e:
|
# 如果有手动配置的token,直接使用
|
||||||
logger.error(f"解析表格数据失败: {e}, sheet_id: {sheet_id}")
|
if config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN:
|
||||||
raise
|
logger.debug("使用手动配置的FEISHU_TOKEN")
|
||||||
|
return True
|
||||||
def test_connection(self) -> bool:
|
|
||||||
"""
|
# 检查token是否过期(提前30分钟刷新)
|
||||||
测试飞书连接是否正常
|
if self._token and self._token_expire_time > 0:
|
||||||
|
time_remaining = self._token_expire_time - current_time
|
||||||
返回:
|
if time_remaining > 1800: # 剩余时间大于30分钟
|
||||||
连接是否正常
|
logger.debug(f"token仍然有效,剩余时间: {int(time_remaining)}秒")
|
||||||
"""
|
return True
|
||||||
try:
|
elif time_remaining > 0: # 剩余时间小于30分钟但大于0
|
||||||
sheets = self.get_sheets_info()
|
logger.info(f"token即将过期,剩余时间: {int(time_remaining)}秒,重新获取")
|
||||||
if sheets:
|
else: # 已过期
|
||||||
logger.info(f"飞书连接测试成功,找到 {len(sheets)} 个表格")
|
logger.info("token已过期,重新获取")
|
||||||
return True
|
|
||||||
else:
|
# 需要获取新token
|
||||||
logger.warning("飞书连接测试成功,但未找到表格")
|
try:
|
||||||
return False
|
token, expire = self._get_tenant_access_token()
|
||||||
except Exception as e:
|
self._token = token
|
||||||
logger.error(f"飞书连接测试失败: {e}")
|
self._token_obtained_time = current_time
|
||||||
return False
|
self._token_expire_time = current_time + expire
|
||||||
|
self._update_session_headers()
|
||||||
|
logger.info(f"token获取成功,将在 {expire} 秒后过期")
|
||||||
if __name__ == '__main__':
|
return True
|
||||||
# 测试代码
|
except Exception as e:
|
||||||
import sys
|
logger.error(f"获取token失败: {e}")
|
||||||
|
# 如果配置了备用token,尝试使用
|
||||||
# 设置日志级别
|
if config.FEISHU_TOKEN and config.FEISHU_TOKEN != self._token:
|
||||||
logging.basicConfig(level=logging.INFO)
|
logger.warning("使用备用FEISHU_TOKEN")
|
||||||
|
self._token = config.FEISHU_TOKEN
|
||||||
# 测试连接
|
self._update_session_headers()
|
||||||
client = FeishuSheetsClient()
|
return True
|
||||||
|
return False
|
||||||
if client.test_connection():
|
|
||||||
print("飞书连接测试成功")
|
def get_sheets_info(self) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
# 获取表格信息
|
获取所有表格信息(sheet_id 和 title)
|
||||||
sheets = client.get_sheets_info()
|
|
||||||
for sheet in sheets[:3]: # 只显示前3个
|
返回:
|
||||||
print(f"表格: {sheet['title']} (ID: {sheet['sheet_id']})")
|
表格信息列表 [{'sheet_id': 'xxx', 'title': 'xxx'}, ...]
|
||||||
|
|
||||||
if sheets:
|
异常:
|
||||||
# 获取第一个表格的数据
|
requests.exceptions.RequestException: 网络请求失败
|
||||||
sheet_id = sheets[0]['sheet_id']
|
ValueError: API返回错误
|
||||||
data = client.get_sheet_data(sheet_id, 'A1:C5')
|
"""
|
||||||
print(f"获取到表格数据,版本: {data.get('revision', '未知')}")
|
# 确保token有效
|
||||||
else:
|
if not self._ensure_valid_token():
|
||||||
print("飞书连接测试失败")
|
raise FeishuClientError("无法获取有效的飞书token")
|
||||||
|
|
||||||
|
url = f'{self.base_url}/spreadsheets/{self.spreadsheet_token}/sheets/query'
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, timeout=config.REQUEST_TIMEOUT)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if data.get('code') != 0:
|
||||||
|
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
sheets = data.get('data', {}).get('sheets', [])
|
||||||
|
result = []
|
||||||
|
for sheet in sheets:
|
||||||
|
result.append({
|
||||||
|
'sheet_id': sheet.get('sheet_id'),
|
||||||
|
'title': sheet.get('title')
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"获取到 {len(result)} 个表格")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"获取表格信息失败: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"解析表格信息失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_sheet_data(self, sheet_id: str, range_: Optional[str] = None) -> Dict:
|
||||||
|
"""
|
||||||
|
获取指定表格的数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
sheet_id: 表格ID
|
||||||
|
range_: 数据范围,如果为None则使用配置
|
||||||
|
|
||||||
|
返回:
|
||||||
|
飞书API返回的原始数据,包含revision版本号
|
||||||
|
|
||||||
|
异常:
|
||||||
|
requests.exceptions.RequestException: 网络请求失败
|
||||||
|
ValueError: API返回错误
|
||||||
|
"""
|
||||||
|
# 确保token有效
|
||||||
|
if not self._ensure_valid_token():
|
||||||
|
raise FeishuClientError("无法获取有效的飞书token")
|
||||||
|
|
||||||
|
if range_ is None:
|
||||||
|
range_ = config.SHEET_RANGE
|
||||||
|
|
||||||
|
# 注意:获取表格数据使用 v2 API,而不是 v3
|
||||||
|
url = f'{self.base_url.replace("/v3", "/v2")}/spreadsheets/{self.spreadsheet_token}/values/{sheet_id}!{range_}'
|
||||||
|
params = {
|
||||||
|
'valueRenderOption': 'ToString',
|
||||||
|
'dateTimeRenderOption': 'FormattedString'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, params=params, timeout=config.REQUEST_TIMEOUT)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if data.get('code') != 0:
|
||||||
|
error_msg = f"飞书API错误: {data.get('msg')}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
logger.debug(f"获取表格数据成功: {sheet_id}, 范围: {range_}")
|
||||||
|
return data.get('data', {})
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"获取表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"解析表格数据失败: {e}, sheet_id: {sheet_id}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_token_info(self) -> Dict[str, any]:
|
||||||
|
"""
|
||||||
|
获取当前token信息
|
||||||
|
|
||||||
|
返回:
|
||||||
|
token信息字典
|
||||||
|
"""
|
||||||
|
current_time = time.time()
|
||||||
|
time_remaining = self._token_expire_time - current_time if self._token_expire_time > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'has_token': bool(self._token),
|
||||||
|
'token_preview': self._token[:20] + '...' if self._token and len(self._token) > 20 else self._token,
|
||||||
|
'token_obtained_time': self._token_obtained_time,
|
||||||
|
'token_expire_time': self._token_expire_time,
|
||||||
|
'time_remaining': max(0, time_remaining),
|
||||||
|
'using_app_credentials': bool(self.app_id and self.app_secret),
|
||||||
|
'using_manual_token': bool(config.FEISHU_TOKEN and self._token == config.FEISHU_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
测试飞书连接是否正常
|
||||||
|
|
||||||
|
返回:
|
||||||
|
连接是否正常
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 首先测试token获取
|
||||||
|
if not self._ensure_valid_token():
|
||||||
|
logger.error("无法获取有效的飞书token")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 然后测试表格访问
|
||||||
|
sheets = self.get_sheets_info()
|
||||||
|
if sheets:
|
||||||
|
logger.info(f"飞书连接测试成功,找到 {len(sheets)} 个表格")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning("飞书连接测试成功,但未找到表格")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"飞书连接测试失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def refresh_token(self) -> bool:
|
||||||
|
"""
|
||||||
|
强制刷新token
|
||||||
|
|
||||||
|
返回:
|
||||||
|
刷新是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("强制刷新token...")
|
||||||
|
current_time = time.time()
|
||||||
|
token, expire = self._get_tenant_access_token()
|
||||||
|
self._token = token
|
||||||
|
self._token_obtained_time = current_time
|
||||||
|
self._token_expire_time = current_time + expire
|
||||||
|
self._update_session_headers()
|
||||||
|
logger.info(f"token刷新成功,将在 {expire} 秒后过期")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"强制刷新token失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试代码
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 设置日志级别
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# 测试连接
|
||||||
|
client = FeishuSheetsClient()
|
||||||
|
|
||||||
|
# 显示token信息
|
||||||
|
token_info = client.get_token_info()
|
||||||
|
print("当前token信息:")
|
||||||
|
print(f" 是否有token: {token_info['has_token']}")
|
||||||
|
print(f" token预览: {token_info['token_preview']}")
|
||||||
|
print(f" 剩余时间: {int(token_info['time_remaining'])}秒")
|
||||||
|
print(f" 使用应用凭证: {token_info['using_app_credentials']}")
|
||||||
|
print(f" 使用手动token: {token_info['using_manual_token']}")
|
||||||
|
|
||||||
|
if client.test_connection():
|
||||||
|
print("\n飞书连接测试成功")
|
||||||
|
|
||||||
|
# 获取表格信息
|
||||||
|
sheets = client.get_sheets_info()
|
||||||
|
for sheet in sheets[:3]: # 只显示前3个
|
||||||
|
print(f"表格: {sheet['title']} (ID: {sheet['sheet_id']})")
|
||||||
|
|
||||||
|
if sheets:
|
||||||
|
# 获取第一个表格的数据
|
||||||
|
sheet_id = sheets[0]['sheet_id']
|
||||||
|
data = client.get_sheet_data(sheet_id, 'A1:C5')
|
||||||
|
print(f"获取到表格数据,版本: {data.get('revision', '未知')}")
|
||||||
|
|
||||||
|
# 再次显示token信息
|
||||||
|
token_info = client.get_token_info()
|
||||||
|
print(f"\n测试后token剩余时间: {int(token_info['time_remaining'])}秒")
|
||||||
|
else:
|
||||||
|
print("\n飞书连接测试失败")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1,323 +1,316 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
飞书排班管理器模块
|
飞书排班管理器模块
|
||||||
统一入口,使用数据库存储和缓存
|
统一入口,使用数据库存储和缓存
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
from src.feishu.client import FeishuSheetsClient
|
from src.feishu.client import FeishuSheetsClient
|
||||||
from src.feishu.parser import ScheduleDataParser
|
from src.feishu.parser import ScheduleDataParser
|
||||||
from src.database.schedules import ScheduleDatabase
|
from src.database.schedules import ScheduleDatabase
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FeishuScheduleManager:
|
class FeishuScheduleManager:
|
||||||
"""飞书排班管理器(统一入口)"""
|
"""飞书排班管理器(统一入口)"""
|
||||||
|
|
||||||
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
||||||
spreadsheet_token: Optional[str] = None, db_path: Optional[str] = None):
|
spreadsheet_token: Optional[str] = None, db_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
初始化管理器
|
初始化管理器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
base_url: 飞书API基础URL,如果为None则使用配置
|
base_url: 飞书API基础URL,如果为None则使用配置
|
||||||
token: 飞书API令牌,如果为None则使用配置
|
token: 飞书API令牌,如果为None则使用配置
|
||||||
spreadsheet_token: 表格token,如果为None则使用配置
|
spreadsheet_token: 表格token,如果为None则使用配置
|
||||||
db_path: 数据库路径,如果为None则使用配置
|
db_path: 数据库路径,如果为None则使用配置
|
||||||
"""
|
"""
|
||||||
# 检查配置是否完整
|
# 检查配置是否完整
|
||||||
self._check_config(token, spreadsheet_token)
|
self._check_config(token, spreadsheet_token)
|
||||||
|
|
||||||
# 初始化组件
|
# 初始化组件
|
||||||
self.client = FeishuSheetsClient(base_url, token, spreadsheet_token)
|
self.client = FeishuSheetsClient(base_url, token, spreadsheet_token)
|
||||||
self.parser = ScheduleDataParser()
|
self.parser = ScheduleDataParser()
|
||||||
self.db = ScheduleDatabase(db_path)
|
self.db = ScheduleDatabase(db_path)
|
||||||
|
|
||||||
logger.info("飞书排班管理器初始化完成")
|
logger.info("飞书排班管理器初始化完成")
|
||||||
|
|
||||||
def _check_config(self, token: Optional[str], spreadsheet_token: Optional[str]) -> None:
|
def _check_config(self, token: Optional[str], spreadsheet_token: Optional[str]) -> None:
|
||||||
"""检查必要配置"""
|
"""检查必要配置"""
|
||||||
if not token and not config.FEISHU_TOKEN:
|
# 检查是否有任何可用的认证方式
|
||||||
logger.warning("飞书令牌未配置,排班功能将不可用")
|
has_token = bool(token or config.FEISHU_TOKEN)
|
||||||
|
has_app_credentials = bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET)
|
||||||
if not spreadsheet_token and not config.FEISHU_SPREADSHEET_TOKEN:
|
|
||||||
logger.warning("飞书表格令牌未配置,排班功能将不可用")
|
if not has_token and not has_app_credentials:
|
||||||
|
logger.warning("飞书认证未配置,排班功能将不可用")
|
||||||
def _select_sheet_for_date(self, sheets: List[Dict[str, str]], target_year_month: str) -> Optional[Dict[str, str]]:
|
logger.warning("请配置 FEISHU_TOKEN 或 FEISHU_APP_ID + FEISHU_APP_SECRET")
|
||||||
"""
|
elif has_app_credentials:
|
||||||
为指定日期选择最合适的表格
|
logger.info("使用飞书应用凭证自动获取token")
|
||||||
|
elif has_token:
|
||||||
参数:
|
logger.info("使用手动配置的FEISHU_TOKEN")
|
||||||
sheets: 表格列表
|
|
||||||
target_year_month: 目标年月,格式 "2025-12"
|
if not spreadsheet_token and not config.FEISHU_SPREADSHEET_TOKEN:
|
||||||
|
logger.warning("飞书表格令牌未配置,排班功能将不可用")
|
||||||
返回:
|
|
||||||
选中的表格信息,未找到返回None
|
def _select_sheet_for_date(self, sheets: List[Dict[str, str]], target_year_month: str) -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
if not sheets:
|
为指定日期选择最合适的表格
|
||||||
logger.error("表格列表为空")
|
|
||||||
return None
|
参数:
|
||||||
|
sheets: 表格列表
|
||||||
# 提取年份和月份
|
target_year_month: 目标年月,格式 "2025-12"
|
||||||
try:
|
|
||||||
year = target_year_month[:4]
|
返回:
|
||||||
month = target_year_month[5:7].lstrip('0')
|
选中的表格信息,未找到返回None
|
||||||
except (IndexError, ValueError) as e:
|
"""
|
||||||
logger.error(f"解析年月失败: {target_year_month}, 错误: {e}")
|
if not sheets:
|
||||||
return None
|
logger.error("表格列表为空")
|
||||||
|
return None
|
||||||
# 对于2026年,优先使用年度表格
|
|
||||||
if year == '2026':
|
# 提取年份和月份
|
||||||
# 查找年度表格,如 "2026年排班表"
|
try:
|
||||||
year_name = f"{year}年"
|
year = target_year_month[:4]
|
||||||
for sheet in sheets:
|
month = target_year_month[5:7].lstrip('0')
|
||||||
title = sheet.get('title', '')
|
except (IndexError, ValueError) as e:
|
||||||
if year_name in title and '排班表' in title:
|
logger.error(f"解析年月失败: {target_year_month}, 错误: {e}")
|
||||||
logger.info(f"找到2026年年度表格: {title}")
|
return None
|
||||||
return sheet
|
|
||||||
|
# 对于2026年,优先使用年度表格
|
||||||
# 优先查找月份表格,如 "12月"
|
if year == '2026':
|
||||||
month_name = f"{int(month)}月"
|
# 查找年度表格,如 "2026年排班表"
|
||||||
for sheet in sheets:
|
year_name = f"{year}年"
|
||||||
title = sheet.get('title', '')
|
for sheet in sheets:
|
||||||
if month_name in title:
|
title = sheet.get('title', '')
|
||||||
logger.info(f"找到月份表格: {title}")
|
if year_name in title and '排班表' in title:
|
||||||
return sheet
|
logger.info(f"找到2026年年度表格: {title}")
|
||||||
|
return sheet
|
||||||
# 查找年度表格,如 "2026年排班表"
|
|
||||||
year_name = f"{year}年"
|
# 优先查找月份表格,如 "12月"
|
||||||
for sheet in sheets:
|
month_name = f"{int(month)}月"
|
||||||
title = sheet.get('title', '')
|
for sheet in sheets:
|
||||||
if year_name in title and '排班表' in title:
|
title = sheet.get('title', '')
|
||||||
logger.info(f"找到年度表格: {title}")
|
if month_name in title:
|
||||||
return sheet
|
logger.info(f"找到月份表格: {title}")
|
||||||
|
return sheet
|
||||||
# 如果没有找到匹配的表格,使用第一个表格
|
|
||||||
logger.warning(f"未找到 {target_year_month} 的匹配表格,使用第一个表格: {sheets[0]['title']}")
|
# 查找年度表格,如 "2026年排班表"
|
||||||
return sheets[0]
|
year_name = f"{year}年"
|
||||||
|
for sheet in sheets:
|
||||||
def get_schedule_for_date(self, date_str: str) -> Dict[str, any]:
|
title = sheet.get('title', '')
|
||||||
"""
|
if year_name in title and '排班表' in title:
|
||||||
获取指定日期的排班信息
|
logger.info(f"找到年度表格: {title}")
|
||||||
|
return sheet
|
||||||
参数:
|
|
||||||
date_str: 日期字符串,格式 "2025-12-30"
|
# 如果没有找到匹配的表格,使用第一个表格
|
||||||
|
logger.warning(f"未找到 {target_year_month} 的匹配表格,使用第一个表格: {sheets[0]['title']}")
|
||||||
返回:
|
return sheets[0]
|
||||||
排班信息字典
|
|
||||||
|
def get_schedule_for_date(self, date_str: str) -> Dict[str, any]:
|
||||||
异常:
|
"""
|
||||||
ValueError: 日期格式无效
|
获取指定日期的排班信息
|
||||||
Exception: 其他错误
|
|
||||||
"""
|
修复:每次都从飞书获取最新数据并覆盖数据库,确保日报中显示最新排班信息
|
||||||
try:
|
|
||||||
# 解析日期
|
参数:
|
||||||
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
date_str: 日期字符串,格式 "2025-12-30"
|
||||||
|
|
||||||
# 生成两种格式的日期字符串,用于匹配不同表格
|
返回:
|
||||||
target_date_mm_dd = dt.strftime('%m/%d') # "01/01" 用于月度表格
|
排班信息字典
|
||||||
target_date_chinese = f"{dt.month}月{dt.day}日" # "1月1日" 用于年度表格
|
|
||||||
target_year_month = dt.strftime('%Y-%m') # "2025-12"
|
异常:
|
||||||
|
ValueError: 日期格式无效
|
||||||
logger.info(f"获取 {date_str} 的排班信息 (格式: {target_date_mm_dd}/{target_date_chinese})")
|
Exception: 其他错误
|
||||||
|
"""
|
||||||
# 1. 首先尝试从数据库获取
|
try:
|
||||||
cached_schedule = self.db.get_schedule(date_str)
|
# 解析日期
|
||||||
if cached_schedule:
|
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
||||||
logger.info(f"从数据库获取 {date_str} 的排班信息")
|
|
||||||
return self._format_db_result(cached_schedule)
|
# 生成两种格式的日期字符串,用于匹配不同表格
|
||||||
|
target_date_mm_dd = dt.strftime('%m/%d') # "01/01" 用于月度表格
|
||||||
# 2. 数据库中没有,需要从飞书获取
|
target_date_chinese = f"{dt.month}月{dt.day}日" # "1月1日" 用于年度表格
|
||||||
logger.info(f"数据库中没有 {date_str} 的排班信息,从飞书获取")
|
target_year_month = dt.strftime('%Y-%m') # "2025-12"
|
||||||
|
|
||||||
# 获取表格信息
|
logger.info(f"获取 {date_str} 的排班信息 (格式: {target_date_mm_dd}/{target_date_chinese})")
|
||||||
sheets = self.client.get_sheets_info()
|
|
||||||
if not sheets:
|
# 1. 获取表格信息
|
||||||
logger.error("未获取到表格信息")
|
sheets = self.client.get_sheets_info()
|
||||||
return self._empty_result()
|
if not sheets:
|
||||||
|
logger.error("未获取到表格信息")
|
||||||
# 选择最合适的表格
|
return self._empty_result()
|
||||||
selected_sheet = self._select_sheet_for_date(sheets, target_year_month)
|
|
||||||
if not selected_sheet:
|
# 2. 选择最合适的表格
|
||||||
logger.error("未找到合适的表格")
|
selected_sheet = self._select_sheet_for_date(sheets, target_year_month)
|
||||||
return self._empty_result()
|
if not selected_sheet:
|
||||||
|
logger.error("未找到合适的表格")
|
||||||
sheet_id = selected_sheet['sheet_id']
|
return self._empty_result()
|
||||||
sheet_title = selected_sheet['title']
|
|
||||||
|
sheet_id = selected_sheet['sheet_id']
|
||||||
# 3. 获取表格数据
|
sheet_title = selected_sheet['title']
|
||||||
sheet_data = self.client.get_sheet_data(sheet_id)
|
|
||||||
if not sheet_data:
|
# 3. 获取表格数据
|
||||||
logger.error("未获取到表格数据")
|
sheet_data = self.client.get_sheet_data(sheet_id)
|
||||||
return self._empty_result()
|
if not sheet_data:
|
||||||
|
logger.error("未获取到表格数据")
|
||||||
values = sheet_data.get('valueRange', {}).get('values', [])
|
return self._empty_result()
|
||||||
revision = sheet_data.get('revision', 0)
|
|
||||||
|
values = sheet_data.get('valueRange', {}).get('values', [])
|
||||||
if not values:
|
|
||||||
logger.error("表格数据为空")
|
if not values:
|
||||||
return self._empty_result()
|
logger.error("表格数据为空")
|
||||||
|
return self._empty_result()
|
||||||
# 4. 检查表格是否有更新
|
|
||||||
need_update = self.db.check_sheet_update(
|
# 4. 解析数据 - 根据表格类型选择合适的日期格式
|
||||||
sheet_id, sheet_title, revision, {'values': values}
|
# 如果是年度表格,使用中文日期格式;否则使用mm/dd格式
|
||||||
)
|
if '年' in sheet_title and '排班表' in sheet_title:
|
||||||
|
target_date = target_date_chinese # "1月1日"
|
||||||
if not need_update and cached_schedule:
|
else:
|
||||||
# 表格无更新,且数据库中有缓存,直接返回
|
target_date = target_date_mm_dd # "01/01"
|
||||||
logger.info(f"表格无更新,使用数据库缓存")
|
|
||||||
return self._format_db_result(cached_schedule)
|
logger.info(f"使用日期格式: {target_date} 解析表格: {sheet_title}")
|
||||||
|
result = self.parser.parse(values, target_date, sheet_title)
|
||||||
# 5. 解析数据 - 根据表格类型选择合适的日期格式
|
|
||||||
# 如果是年度表格,使用中文日期格式;否则使用mm/dd格式
|
# 5. 每次都保存到数据库,覆盖旧数据,确保人员变动能及时更新
|
||||||
if '年' in sheet_title and '排班表' in sheet_title:
|
if result['day_shift'] or result['night_shift']:
|
||||||
target_date = target_date_chinese # "1月1日"
|
self.db.save_schedule(date_str, result, sheet_id, sheet_title)
|
||||||
else:
|
logger.info(f"已更新 {date_str} 的排班信息到数据库: 白班={result['day_shift']}, 夜班={result['night_shift']}")
|
||||||
target_date = target_date_mm_dd # "01/01"
|
else:
|
||||||
|
logger.warning(f"解析结果为空,{date_str} 未保存到数据库")
|
||||||
logger.info(f"使用日期格式: {target_date} 解析表格: {sheet_title}")
|
|
||||||
result = self.parser.parse(values, target_date, sheet_title)
|
return result
|
||||||
|
|
||||||
# 6. 保存到数据库
|
except ValueError as e:
|
||||||
if result['day_shift'] or result['night_shift']:
|
logger.error(f"日期格式无效: {date_str}, 错误: {e}")
|
||||||
self.db.save_schedule(date_str, result, sheet_id, sheet_title)
|
raise
|
||||||
logger.info(f"已保存 {date_str} 的排班信息到数据库")
|
except Exception as e:
|
||||||
|
logger.error(f"获取排班信息失败: {e}")
|
||||||
return result
|
# 降级处理:返回空值
|
||||||
|
return self._empty_result()
|
||||||
except ValueError as e:
|
|
||||||
logger.error(f"日期格式无效: {date_str}, 错误: {e}")
|
def get_schedule_for_today(self) -> Dict[str, any]:
|
||||||
raise
|
"""获取今天的排班信息"""
|
||||||
except Exception as e:
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
logger.error(f"获取排班信息失败: {e}")
|
return self.get_schedule_for_date(today)
|
||||||
# 降级处理:返回空值
|
|
||||||
return self._empty_result()
|
def get_schedule_for_tomorrow(self) -> Dict[str, any]:
|
||||||
|
"""获取明天的排班信息"""
|
||||||
def get_schedule_for_today(self) -> Dict[str, any]:
|
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
"""获取今天的排班信息"""
|
return self.get_schedule_for_date(tomorrow)
|
||||||
today = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
return self.get_schedule_for_date(today)
|
def refresh_all_schedules(self, days: Optional[int] = None):
|
||||||
|
"""
|
||||||
def get_schedule_for_tomorrow(self) -> Dict[str, any]:
|
刷新未来指定天数的排班信息
|
||||||
"""获取明天的排班信息"""
|
|
||||||
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
参数:
|
||||||
return self.get_schedule_for_date(tomorrow)
|
days: 刷新未来多少天的排班信息,如果为None则使用配置
|
||||||
|
"""
|
||||||
def refresh_all_schedules(self, days: Optional[int] = None):
|
if days is None:
|
||||||
"""
|
days = config.SCHEDULE_REFRESH_DAYS
|
||||||
刷新未来指定天数的排班信息
|
|
||||||
|
logger.info(f"开始刷新未来 {days} 天的排班信息")
|
||||||
参数:
|
|
||||||
days: 刷新未来多少天的排班信息,如果为None则使用配置
|
today = datetime.now()
|
||||||
"""
|
success_count = 0
|
||||||
if days is None:
|
error_count = 0
|
||||||
days = config.SCHEDULE_REFRESH_DAYS
|
|
||||||
|
for i in range(days):
|
||||||
logger.info(f"开始刷新未来 {days} 天的排班信息")
|
date = (today + timedelta(days=i)).strftime('%Y-%m-%d')
|
||||||
|
try:
|
||||||
today = datetime.now()
|
logger.debug(f"刷新 {date} 的排班信息...")
|
||||||
success_count = 0
|
self.get_schedule_for_date(date)
|
||||||
error_count = 0
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
for i in range(days):
|
logger.error(f"刷新 {date} 的排班信息失败: {e}")
|
||||||
date = (today + timedelta(days=i)).strftime('%Y-%m-%d')
|
error_count += 1
|
||||||
try:
|
|
||||||
logger.debug(f"刷新 {date} 的排班信息...")
|
logger.info(f"排班信息刷新完成,成功: {success_count}, 失败: {error_count}")
|
||||||
self.get_schedule_for_date(date)
|
|
||||||
success_count += 1
|
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, any]]:
|
||||||
except Exception as e:
|
"""
|
||||||
logger.error(f"刷新 {date} 的排班信息失败: {e}")
|
获取日期范围内的排班信息
|
||||||
error_count += 1
|
|
||||||
|
参数:
|
||||||
logger.info(f"排班信息刷新完成,成功: {success_count}, 失败: {error_count}")
|
start_date: 开始日期 (YYYY-MM-DD)
|
||||||
|
end_date: 结束日期 (YYYY-MM-DD)
|
||||||
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict[str, any]]:
|
|
||||||
"""
|
返回:
|
||||||
获取日期范围内的排班信息
|
排班信息列表
|
||||||
|
"""
|
||||||
参数:
|
try:
|
||||||
start_date: 开始日期 (YYYY-MM-DD)
|
# 验证日期格式
|
||||||
end_date: 结束日期 (YYYY-MM-DD)
|
datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
|
datetime.strptime(end_date, '%Y-%m-%d')
|
||||||
返回:
|
|
||||||
排班信息列表
|
return self.db.get_schedule_by_range(start_date, end_date)
|
||||||
"""
|
|
||||||
try:
|
except ValueError as e:
|
||||||
# 验证日期格式
|
logger.error(f"日期格式无效: {e}")
|
||||||
datetime.strptime(start_date, '%Y-%m-%d')
|
return []
|
||||||
datetime.strptime(end_date, '%Y-%m-%d')
|
except Exception as e:
|
||||||
|
logger.error(f"获取排班范围失败: {e}")
|
||||||
return self.db.get_schedule_by_range(start_date, end_date)
|
return []
|
||||||
|
|
||||||
except ValueError as e:
|
def test_connection(self) -> bool:
|
||||||
logger.error(f"日期格式无效: {e}")
|
"""测试飞书连接是否正常"""
|
||||||
return []
|
return self.client.test_connection()
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取排班范围失败: {e}")
|
def get_stats(self) -> Dict[str, any]:
|
||||||
return []
|
"""获取排班数据库统计信息"""
|
||||||
|
return self.db.get_stats()
|
||||||
def test_connection(self) -> bool:
|
|
||||||
"""测试飞书连接是否正常"""
|
def _empty_result(self) -> Dict[str, any]:
|
||||||
return self.client.test_connection()
|
"""返回空结果"""
|
||||||
|
return {
|
||||||
def get_stats(self) -> Dict[str, any]:
|
'day_shift': '',
|
||||||
"""获取排班数据库统计信息"""
|
'night_shift': '',
|
||||||
return self.db.get_stats()
|
'day_shift_list': [],
|
||||||
|
'night_shift_list': []
|
||||||
def _empty_result(self) -> Dict[str, any]:
|
}
|
||||||
"""返回空结果"""
|
|
||||||
return {
|
def _format_db_result(self, db_result: Dict[str, any]) -> Dict[str, any]:
|
||||||
'day_shift': '',
|
"""格式化数据库结果"""
|
||||||
'night_shift': '',
|
return {
|
||||||
'day_shift_list': [],
|
'day_shift': db_result['day_shift'],
|
||||||
'night_shift_list': []
|
'night_shift': db_result['night_shift'],
|
||||||
}
|
'day_shift_list': db_result['day_shift_list'],
|
||||||
|
'night_shift_list': db_result['night_shift_list']
|
||||||
def _format_db_result(self, db_result: Dict[str, any]) -> Dict[str, any]:
|
}
|
||||||
"""格式化数据库结果"""
|
|
||||||
return {
|
|
||||||
'day_shift': db_result['day_shift'],
|
if __name__ == '__main__':
|
||||||
'night_shift': db_result['night_shift'],
|
# 测试代码
|
||||||
'day_shift_list': db_result['day_shift_list'],
|
import sys
|
||||||
'night_shift_list': db_result['night_shift_list']
|
|
||||||
}
|
# 设置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# 初始化管理器
|
||||||
# 测试代码
|
manager = FeishuScheduleManager()
|
||||||
import sys
|
|
||||||
|
# 测试连接
|
||||||
# 设置日志
|
if not manager.test_connection():
|
||||||
logging.basicConfig(level=logging.INFO)
|
print("飞书连接测试失败")
|
||||||
|
sys.exit(1)
|
||||||
# 初始化管理器
|
|
||||||
manager = FeishuScheduleManager()
|
print("飞书连接测试成功")
|
||||||
|
|
||||||
# 测试连接
|
# 测试获取今天和明天的排班
|
||||||
if not manager.test_connection():
|
today_schedule = manager.get_schedule_for_today()
|
||||||
print("飞书连接测试失败")
|
print(f"今天排班: 白班={today_schedule['day_shift']}, 夜班={today_schedule['night_shift']}")
|
||||||
sys.exit(1)
|
|
||||||
|
tomorrow_schedule = manager.get_schedule_for_tomorrow()
|
||||||
print("飞书连接测试成功")
|
print(f"明天排班: 白班={tomorrow_schedule['day_shift']}, 夜班={tomorrow_schedule['night_shift']}")
|
||||||
|
|
||||||
# 测试获取今天和明天的排班
|
# 测试统计
|
||||||
today_schedule = manager.get_schedule_for_today()
|
stats = manager.get_stats()
|
||||||
print(f"今天排班: 白班={today_schedule['day_shift']}, 夜班={today_schedule['night_shift']}")
|
print(f"排班统计: {stats}")
|
||||||
|
|
||||||
tomorrow_schedule = manager.get_schedule_for_tomorrow()
|
# 测试范围查询(最近7天)
|
||||||
print(f"明天排班: 白班={tomorrow_schedule['day_shift']}, 夜班={tomorrow_schedule['night_shift']}")
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
|
||||||
# 测试统计
|
schedules = manager.get_schedule_by_range(start_date, end_date)
|
||||||
stats = manager.get_stats()
|
|
||||||
print(f"排班统计: {stats}")
|
|
||||||
|
|
||||||
# 测试范围查询(最近7天)
|
|
||||||
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
|
|
||||||
schedules = manager.get_schedule_by_range(start_date, end_date)
|
|
||||||
print(f"最近7天排班记录: {len(schedules)} 条")
|
print(f"最近7天排班记录: {len(schedules)} 条")
|
||||||
@@ -1,339 +1,339 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
排班数据解析器模块
|
排班数据解析器模块
|
||||||
支持月度表格和年度表格解析
|
支持月度表格和年度表格解析
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScheduleDataParser:
|
class ScheduleDataParser:
|
||||||
"""排班数据解析器(支持月度表格和年度表格)"""
|
"""排班数据解析器(支持月度表格和年度表格)"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_chinese_date(date_str: str) -> Optional[str]:
|
def _parse_chinese_date(date_str: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
解析中文日期格式
|
解析中文日期格式
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date_str: 中文日期,如 "12月30日" 或 "12/30" 或 "12月1日" 或 "1月1日"
|
date_str: 中文日期,如 "12月30日" 或 "12/30" 或 "12月1日" 或 "1月1日"
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
标准化日期字符串 "M月D日" (不补零)
|
标准化日期字符串 "M月D日" (不补零)
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
ValueError: 日期格式无效
|
ValueError: 日期格式无效
|
||||||
"""
|
"""
|
||||||
if not date_str or not isinstance(date_str, str):
|
if not date_str or not isinstance(date_str, str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
date_str = date_str.strip()
|
date_str = date_str.strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 如果是 "12/30" 格式
|
# 如果是 "12/30" 格式
|
||||||
if '/' in date_str:
|
if '/' in date_str:
|
||||||
month, day = date_str.split('/')
|
month, day = date_str.split('/')
|
||||||
# 移除可能的空格和前导零
|
# 移除可能的空格和前导零
|
||||||
month = month.strip().lstrip('0')
|
month = month.strip().lstrip('0')
|
||||||
day = day.strip().lstrip('0')
|
day = day.strip().lstrip('0')
|
||||||
if not month.isdigit() or not day.isdigit():
|
if not month.isdigit() or not day.isdigit():
|
||||||
raise ValueError(f"日期格式无效: {date_str}")
|
raise ValueError(f"日期格式无效: {date_str}")
|
||||||
return f"{int(month)}月{int(day)}日"
|
return f"{int(month)}月{int(day)}日"
|
||||||
|
|
||||||
# 如果是 "12月30日" 或 "1月1日" 格式
|
# 如果是 "12月30日" 或 "1月1日" 格式
|
||||||
if '月' in date_str and '日' in date_str:
|
if '月' in date_str and '日' in date_str:
|
||||||
# 移除前导零,如 "01月01日" -> "1月1日"
|
# 移除前导零,如 "01月01日" -> "1月1日"
|
||||||
parts = date_str.split('月')
|
parts = date_str.split('月')
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
month_part = parts[0].lstrip('0')
|
month_part = parts[0].lstrip('0')
|
||||||
day_part = parts[1].rstrip('日').lstrip('0')
|
day_part = parts[1].rstrip('日').lstrip('0')
|
||||||
if not month_part or not day_part:
|
if not month_part or not day_part:
|
||||||
raise ValueError(f"日期格式无效: {date_str}")
|
raise ValueError(f"日期格式无效: {date_str}")
|
||||||
return f"{month_part}月{day_part}日"
|
return f"{month_part}月{day_part}日"
|
||||||
return date_str
|
return date_str
|
||||||
|
|
||||||
# 如果是 "12月1日" 格式(已经包含"日"字)
|
# 如果是 "12月1日" 格式(已经包含"日"字)
|
||||||
if '月' in date_str:
|
if '月' in date_str:
|
||||||
# 检查是否已经有"日"字
|
# 检查是否已经有"日"字
|
||||||
if '日' not in date_str:
|
if '日' not in date_str:
|
||||||
return f"{date_str}日"
|
return f"{date_str}日"
|
||||||
return date_str
|
return date_str
|
||||||
|
|
||||||
# 如果是纯数字,尝试解析
|
# 如果是纯数字,尝试解析
|
||||||
if date_str.isdigit() and len(date_str) == 4:
|
if date_str.isdigit() and len(date_str) == 4:
|
||||||
# 假设是 "1230" 格式
|
# 假设是 "1230" 格式
|
||||||
month = date_str[:2].lstrip('0')
|
month = date_str[:2].lstrip('0')
|
||||||
day = date_str[2:].lstrip('0')
|
day = date_str[2:].lstrip('0')
|
||||||
return f"{month}月{day}日"
|
return f"{month}月{day}日"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
logger.warning(f"解析日期失败: {date_str}, 错误: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_date_column_index(headers: List[str], target_date: str) -> Optional[int]:
|
def _find_date_column_index(headers: List[str], target_date: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
在表头中查找目标日期对应的列索引
|
在表头中查找目标日期对应的列索引
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
headers: 表头行 ["姓名", "12月1日", "12月2日", ...]
|
headers: 表头行 ["姓名", "12月1日", "12月2日", ...]
|
||||||
target_date: 目标日期 "12月30日"
|
target_date: 目标日期 "12月30日"
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
列索引(从0开始),未找到返回None
|
列索引(从0开始),未找到返回None
|
||||||
"""
|
"""
|
||||||
if not headers or not target_date:
|
if not headers or not target_date:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 标准化目标日期
|
# 标准化目标日期
|
||||||
target_std = ScheduleDataParser._parse_chinese_date(target_date)
|
target_std = ScheduleDataParser._parse_chinese_date(target_date)
|
||||||
if not target_std:
|
if not target_std:
|
||||||
logger.warning(f"无法标准化目标日期: {target_date}")
|
logger.warning(f"无法标准化目标日期: {target_date}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 遍历表头查找匹配的日期
|
# 遍历表头查找匹配的日期
|
||||||
for i, header in enumerate(headers):
|
for i, header in enumerate(headers):
|
||||||
if not header:
|
if not header:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
header_std = ScheduleDataParser._parse_chinese_date(header)
|
header_std = ScheduleDataParser._parse_chinese_date(header)
|
||||||
if header_std == target_std:
|
if header_std == target_std:
|
||||||
logger.debug(f"找到日期列: {target_date} -> {header} (索引: {i})")
|
logger.debug(f"找到日期列: {target_date} -> {header} (索引: {i})")
|
||||||
return i
|
return i
|
||||||
|
|
||||||
logger.warning(f"未找到日期列: {target_date}, 表头: {headers}")
|
logger.warning(f"未找到日期列: {target_date}, 表头: {headers}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def parse_monthly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
def parse_monthly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
解析月度表格数据(如12月表格)
|
解析月度表格数据(如12月表格)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
values: 飞书表格返回的二维数组
|
values: 飞书表格返回的二维数组
|
||||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
排班信息字典
|
排班信息字典
|
||||||
"""
|
"""
|
||||||
if not values or len(values) < 2:
|
if not values or len(values) < 2:
|
||||||
logger.warning("表格数据为空或不足")
|
logger.warning("表格数据为空或不足")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
# 第一行是表头
|
# 第一行是表头
|
||||||
headers = values[0]
|
headers = values[0]
|
||||||
date_column_index = self._find_date_column_index(headers, target_date)
|
date_column_index = self._find_date_column_index(headers, target_date)
|
||||||
|
|
||||||
if date_column_index is None:
|
if date_column_index is None:
|
||||||
logger.warning(f"未找到日期列: {target_date}")
|
logger.warning(f"未找到日期列: {target_date}")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
# 收集白班和夜班人员
|
# 收集白班和夜班人员
|
||||||
day_shift_names = []
|
day_shift_names = []
|
||||||
night_shift_names = []
|
night_shift_names = []
|
||||||
|
|
||||||
# 从第二行开始是人员数据
|
# 从第二行开始是人员数据
|
||||||
for row_idx, row in enumerate(values[1:], start=2):
|
for row_idx, row in enumerate(values[1:], start=2):
|
||||||
if len(row) <= date_column_index:
|
if len(row) <= date_column_index:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = row[0] if row else ''
|
name = row[0] if row else ''
|
||||||
shift = row[date_column_index] if date_column_index < len(row) else ''
|
shift = row[date_column_index] if date_column_index < len(row) else ''
|
||||||
|
|
||||||
if not name or not shift:
|
if not name or not shift:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 清理班次值
|
# 清理班次值
|
||||||
shift = shift.strip()
|
shift = shift.strip()
|
||||||
if shift == '白':
|
if shift == '白':
|
||||||
day_shift_names.append(name.strip())
|
day_shift_names.append(name.strip())
|
||||||
elif shift == '夜':
|
elif shift == '夜':
|
||||||
night_shift_names.append(name.strip())
|
night_shift_names.append(name.strip())
|
||||||
elif shift: # 其他班次类型
|
elif shift: # 其他班次类型
|
||||||
logger.debug(f"忽略未知班次类型: {shift} (行: {row_idx})")
|
logger.debug(f"忽略未知班次类型: {shift} (行: {row_idx})")
|
||||||
|
|
||||||
return self._format_result(day_shift_names, night_shift_names)
|
return self._format_result(day_shift_names, night_shift_names)
|
||||||
|
|
||||||
def parse_yearly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
def parse_yearly_sheet(self, values: List[List[str]], target_date: str) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
解析年度表格数据(如2026年排班表)
|
解析年度表格数据(如2026年排班表)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
values: 飞书表格返回的二维数组
|
values: 飞书表格返回的二维数组
|
||||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
排班信息字典
|
排班信息字典
|
||||||
"""
|
"""
|
||||||
if not values:
|
if not values:
|
||||||
logger.warning("年度表格数据为空")
|
logger.warning("年度表格数据为空")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
# 查找目标月份的数据块
|
# 查找目标月份的数据块
|
||||||
target_month = target_date.split('月')[0] if '月' in target_date else ''
|
target_month = target_date.split('月')[0] if '月' in target_date else ''
|
||||||
if not target_month:
|
if not target_month:
|
||||||
logger.warning(f"无法从 {target_date} 提取月份")
|
logger.warning(f"无法从 {target_date} 提取月份")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
# 在年度表格中查找对应的月份块
|
# 在年度表格中查找对应的月份块
|
||||||
current_block_start = -1
|
current_block_start = -1
|
||||||
current_month = ''
|
current_month = ''
|
||||||
|
|
||||||
for i, row in enumerate(values):
|
for i, row in enumerate(values):
|
||||||
if not row:
|
if not row:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
first_cell = str(row[0]) if row else ''
|
first_cell = str(row[0]) if row else ''
|
||||||
|
|
||||||
# 检查是否是月份标题行,如 "福州港1月排班表"
|
# 检查是否是月份标题行,如 "福州港1月排班表"
|
||||||
if '排班表' in first_cell and '月' in first_cell:
|
if '排班表' in first_cell and '月' in first_cell:
|
||||||
# 提取月份数字
|
# 提取月份数字
|
||||||
month_match = re.search(r'(\d+)月', first_cell)
|
month_match = re.search(r'(\d+)月', first_cell)
|
||||||
if month_match:
|
if month_match:
|
||||||
current_month = month_match.group(1).lstrip('0')
|
current_month = month_match.group(1).lstrip('0')
|
||||||
current_block_start = i
|
current_block_start = i
|
||||||
logger.debug(f"找到月份块: {current_month}月 (行: {i+1})")
|
logger.debug(f"找到月份块: {current_month}月 (行: {i+1})")
|
||||||
|
|
||||||
# 如果找到目标月份,检查下一行是否是表头行
|
# 如果找到目标月份,检查下一行是否是表头行
|
||||||
if current_month == target_month and i == current_block_start + 1:
|
if current_month == target_month and i == current_block_start + 1:
|
||||||
# 当前行是表头行
|
# 当前行是表头行
|
||||||
headers = row
|
headers = row
|
||||||
date_column_index = self._find_date_column_index(headers, target_date)
|
date_column_index = self._find_date_column_index(headers, target_date)
|
||||||
|
|
||||||
if date_column_index is None:
|
if date_column_index is None:
|
||||||
logger.warning(f"在年度表格中未找到日期列: {target_date}")
|
logger.warning(f"在年度表格中未找到日期列: {target_date}")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
# 收集人员数据(从表头行的下一行开始)
|
# 收集人员数据(从表头行的下一行开始)
|
||||||
day_shift_names = []
|
day_shift_names = []
|
||||||
night_shift_names = []
|
night_shift_names = []
|
||||||
|
|
||||||
for j in range(i + 1, len(values)):
|
for j in range(i + 1, len(values)):
|
||||||
person_row = values[j]
|
person_row = values[j]
|
||||||
if not person_row:
|
if not person_row:
|
||||||
# 遇到空行,继续检查下一行
|
# 遇到空行,继续检查下一行
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查是否是下一个月份块的开始
|
# 检查是否是下一个月份块的开始
|
||||||
if person_row[0] and isinstance(person_row[0], str) and '排班表' in person_row[0] and '月' in person_row[0]:
|
if person_row[0] and isinstance(person_row[0], str) and '排班表' in person_row[0] and '月' in person_row[0]:
|
||||||
break
|
break
|
||||||
|
|
||||||
# 跳过星期行(第一列为空的行)
|
# 跳过星期行(第一列为空的行)
|
||||||
if not person_row[0]:
|
if not person_row[0]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(person_row) <= date_column_index:
|
if len(person_row) <= date_column_index:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = person_row[0] if person_row else ''
|
name = person_row[0] if person_row else ''
|
||||||
shift = person_row[date_column_index] if date_column_index < len(person_row) else ''
|
shift = person_row[date_column_index] if date_column_index < len(person_row) else ''
|
||||||
|
|
||||||
if not name or not shift:
|
if not name or not shift:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 清理班次值
|
# 清理班次值
|
||||||
shift = shift.strip()
|
shift = shift.strip()
|
||||||
if shift == '白':
|
if shift == '白':
|
||||||
day_shift_names.append(name.strip())
|
day_shift_names.append(name.strip())
|
||||||
elif shift == '夜':
|
elif shift == '夜':
|
||||||
night_shift_names.append(name.strip())
|
night_shift_names.append(name.strip())
|
||||||
|
|
||||||
return self._format_result(day_shift_names, night_shift_names)
|
return self._format_result(day_shift_names, night_shift_names)
|
||||||
|
|
||||||
logger.warning(f"在年度表格中未找到 {target_month}月 的数据块")
|
logger.warning(f"在年度表格中未找到 {target_month}月 的数据块")
|
||||||
return self._empty_result()
|
return self._empty_result()
|
||||||
|
|
||||||
def parse(self, values: List[List[str]], target_date: str, sheet_title: str = '') -> Dict[str, any]:
|
def parse(self, values: List[List[str]], target_date: str, sheet_title: str = '') -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
解析排班数据,自动判断表格类型
|
解析排班数据,自动判断表格类型
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
values: 飞书表格返回的二维数组
|
values: 飞书表格返回的二维数组
|
||||||
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
target_date: 目标日期(格式: "12月30日" 或 "12/30")
|
||||||
sheet_title: 表格标题,用于判断表格类型
|
sheet_title: 表格标题,用于判断表格类型
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
排班信息字典
|
排班信息字典
|
||||||
"""
|
"""
|
||||||
# 根据表格标题判断表格类型
|
# 根据表格标题判断表格类型
|
||||||
if '年' in sheet_title and '排班表' in sheet_title:
|
if '年' in sheet_title and '排班表' in sheet_title:
|
||||||
# 年度表格
|
# 年度表格
|
||||||
logger.info(f"使用年度表格解析器: {sheet_title}")
|
logger.info(f"使用年度表格解析器: {sheet_title}")
|
||||||
return self.parse_yearly_sheet(values, target_date)
|
return self.parse_yearly_sheet(values, target_date)
|
||||||
else:
|
else:
|
||||||
# 月度表格
|
# 月度表格
|
||||||
logger.info(f"使用月度表格解析器: {sheet_title}")
|
logger.info(f"使用月度表格解析器: {sheet_title}")
|
||||||
return self.parse_monthly_sheet(values, target_date)
|
return self.parse_monthly_sheet(values, target_date)
|
||||||
|
|
||||||
def _empty_result(self) -> Dict[str, any]:
|
def _empty_result(self) -> Dict[str, any]:
|
||||||
"""返回空结果"""
|
"""返回空结果"""
|
||||||
return {
|
return {
|
||||||
'day_shift': '',
|
'day_shift': '',
|
||||||
'night_shift': '',
|
'night_shift': '',
|
||||||
'day_shift_list': [],
|
'day_shift_list': [],
|
||||||
'night_shift_list': []
|
'night_shift_list': []
|
||||||
}
|
}
|
||||||
|
|
||||||
def _format_result(self, day_shift_names: List[str], night_shift_names: List[str]) -> Dict[str, any]:
|
def _format_result(self, day_shift_names: List[str], night_shift_names: List[str]) -> Dict[str, any]:
|
||||||
"""格式化结果"""
|
"""格式化结果"""
|
||||||
# 去重并排序
|
# 去重并排序
|
||||||
day_shift_names = sorted(set(day_shift_names))
|
day_shift_names = sorted(set(day_shift_names))
|
||||||
night_shift_names = sorted(set(night_shift_names))
|
night_shift_names = sorted(set(night_shift_names))
|
||||||
|
|
||||||
# 格式化输出
|
# 格式化输出
|
||||||
day_shift_str = '、'.join(day_shift_names) if day_shift_names else ''
|
day_shift_str = '、'.join(day_shift_names) if day_shift_names else ''
|
||||||
night_shift_str = '、'.join(night_shift_names) if night_shift_names else ''
|
night_shift_str = '、'.join(night_shift_names) if night_shift_names else ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'day_shift': day_shift_str,
|
'day_shift': day_shift_str,
|
||||||
'night_shift': night_shift_str,
|
'night_shift': night_shift_str,
|
||||||
'day_shift_list': day_shift_names,
|
'day_shift_list': day_shift_names,
|
||||||
'night_shift_list': night_shift_names
|
'night_shift_list': night_shift_names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
parser = ScheduleDataParser()
|
parser = ScheduleDataParser()
|
||||||
|
|
||||||
# 测试日期解析
|
# 测试日期解析
|
||||||
test_dates = ["12/30", "12月30日", "1月1日", "01/01", "1230", "无效日期"]
|
test_dates = ["12/30", "12月30日", "1月1日", "01/01", "1230", "无效日期"]
|
||||||
for date in test_dates:
|
for date in test_dates:
|
||||||
parsed = parser._parse_chinese_date(date)
|
parsed = parser._parse_chinese_date(date)
|
||||||
print(f"解析 '{date}' -> '{parsed}'")
|
print(f"解析 '{date}' -> '{parsed}'")
|
||||||
|
|
||||||
# 测试月度表格解析
|
# 测试月度表格解析
|
||||||
monthly_values = [
|
monthly_values = [
|
||||||
["姓名", "12月1日", "12月2日", "12月3日"],
|
["姓名", "12月1日", "12月2日", "12月3日"],
|
||||||
["张三", "白", "夜", ""],
|
["张三", "白", "夜", ""],
|
||||||
["李四", "夜", "白", "白"],
|
["李四", "夜", "白", "白"],
|
||||||
["王五", "", "白", "夜"]
|
["王五", "", "白", "夜"]
|
||||||
]
|
]
|
||||||
|
|
||||||
result = parser.parse_monthly_sheet(monthly_values, "12月2日")
|
result = parser.parse_monthly_sheet(monthly_values, "12月2日")
|
||||||
print(f"\n月度表格解析结果: {result}")
|
print(f"\n月度表格解析结果: {result}")
|
||||||
|
|
||||||
# 测试年度表格解析
|
# 测试年度表格解析
|
||||||
yearly_values = [
|
yearly_values = [
|
||||||
["福州港2026年排班表"],
|
["福州港2026年排班表"],
|
||||||
["姓名", "1月1日", "1月2日", "1月3日"],
|
["姓名", "1月1日", "1月2日", "1月3日"],
|
||||||
["张三", "白", "夜", ""],
|
["张三", "白", "夜", ""],
|
||||||
["李四", "夜", "白", "白"],
|
["李四", "夜", "白", "白"],
|
||||||
["福州港2月排班表"],
|
["福州港2月排班表"],
|
||||||
["姓名", "2月1日", "2月2日"],
|
["姓名", "2月1日", "2月2日"],
|
||||||
["王五", "白", "夜"]
|
["王五", "白", "夜"]
|
||||||
]
|
]
|
||||||
|
|
||||||
result = parser.parse_yearly_sheet(yearly_values, "1月2日")
|
result = parser.parse_yearly_sheet(yearly_values, "1月2日")
|
||||||
print(f"年度表格解析结果: {result}")
|
print(f"年度表格解析结果: {result}")
|
||||||
2514
src/gui.py
2514
src/gui.py
File diff suppressed because it is too large
Load Diff
@@ -1,144 +1,172 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
统一日志配置模块
|
统一日志配置模块
|
||||||
提供统一的日志配置,避免各模块自行配置
|
提供统一的日志配置,避免各模块自行配置
|
||||||
"""
|
支持按日期分片存储日志
|
||||||
import os
|
"""
|
||||||
import logging
|
import os
|
||||||
import sys
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
import sys
|
||||||
from typing import Optional
|
from datetime import datetime
|
||||||
|
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||||
from src.config import config
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.config import config
|
||||||
def setup_logging(
|
|
||||||
log_file: Optional[str] = None,
|
|
||||||
console_level: int = logging.INFO,
|
def setup_logging(
|
||||||
file_level: int = logging.DEBUG,
|
log_file: Optional[str] = None,
|
||||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
console_level: int = logging.INFO,
|
||||||
backup_count: int = 5
|
file_level: int = logging.DEBUG,
|
||||||
) -> logging.Logger:
|
use_date_split: bool = True,
|
||||||
"""
|
date_folder_format: str = "%Y-%m", # 按月份分文件夹
|
||||||
配置统一的日志系统
|
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||||
|
backup_count: int = 5
|
||||||
参数:
|
) -> logging.Logger:
|
||||||
log_file: 日志文件路径,如果为None则使用默认路径
|
"""
|
||||||
console_level: 控制台日志级别
|
配置统一的日志系统
|
||||||
file_level: 文件日志级别
|
|
||||||
max_bytes: 单个日志文件最大大小
|
参数:
|
||||||
backup_count: 备份文件数量
|
log_file: 日志文件路径,如果为None则使用默认路径
|
||||||
|
console_level: 控制台日志级别
|
||||||
返回:
|
file_level: 文件日志级别
|
||||||
配置好的根日志器
|
use_date_split: 是否使用日期分片
|
||||||
"""
|
date_folder_format: 日期文件夹格式(默认按月份,如 logs/2025-12/)
|
||||||
# 创建日志目录
|
max_bytes: 单个日志文件最大大小
|
||||||
if log_file is None:
|
backup_count: 备份文件数量
|
||||||
log_dir = 'logs'
|
|
||||||
log_file = os.path.join(log_dir, 'app.log')
|
返回:
|
||||||
else:
|
配置好的根日志器
|
||||||
log_dir = os.path.dirname(log_file)
|
"""
|
||||||
|
# 获取当前日期用于构建路径
|
||||||
if log_dir and not os.path.exists(log_dir):
|
now = datetime.now()
|
||||||
os.makedirs(log_dir)
|
|
||||||
|
if log_file is None:
|
||||||
# 获取根日志器
|
log_dir = 'logs'
|
||||||
logger = logging.getLogger()
|
if use_date_split:
|
||||||
logger.setLevel(logging.DEBUG) # 根日志器设置为最低级别
|
# 按日期分片:logs/2025-12/2025-12-30.log
|
||||||
|
date_folder = now.strftime(date_folder_format)
|
||||||
# 清除现有handler,避免重复添加
|
log_dir = os.path.join('logs', date_folder)
|
||||||
logger.handlers.clear()
|
log_file = os.path.join(log_dir, now.strftime('%Y-%m-%d.log'))
|
||||||
|
else:
|
||||||
# 控制台handler
|
log_file = os.path.join(log_dir, 'app.log')
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
else:
|
||||||
console_handler.setLevel(console_level)
|
log_dir = os.path.dirname(log_file)
|
||||||
console_formatter = logging.Formatter(
|
|
||||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
if log_dir and not os.path.exists(log_dir):
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
)
|
|
||||||
console_handler.setFormatter(console_formatter)
|
# 获取根日志器
|
||||||
logger.addHandler(console_handler)
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG) # 根日志器设置为最低级别
|
||||||
# 文件handler(轮转)
|
|
||||||
file_handler = RotatingFileHandler(
|
# 清除现有handler,避免重复添加
|
||||||
log_file,
|
logger.handlers.clear()
|
||||||
maxBytes=max_bytes,
|
|
||||||
backupCount=backup_count,
|
# 控制台handler
|
||||||
encoding='utf-8'
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
)
|
console_handler.setLevel(console_level)
|
||||||
file_handler.setLevel(file_level)
|
console_formatter = logging.Formatter(
|
||||||
file_formatter = logging.Formatter(
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
)
|
||||||
)
|
console_handler.setFormatter(console_formatter)
|
||||||
file_handler.setFormatter(file_formatter)
|
logger.addHandler(console_handler)
|
||||||
logger.addHandler(file_handler)
|
|
||||||
|
# 文件handler(日期分片或大小轮转)
|
||||||
# 设置第三方库的日志级别
|
if use_date_split:
|
||||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
# 使用TimedRotatingFileHandler,每天午夜轮转
|
||||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
file_handler = TimedRotatingFileHandler(
|
||||||
|
log_file,
|
||||||
logger.info(f"日志系统已初始化,日志文件: {log_file}")
|
when='midnight',
|
||||||
logger.info(f"控制台日志级别: {logging.getLevelName(console_level)}")
|
interval=1,
|
||||||
logger.info(f"文件日志级别: {logging.getLevelName(file_level)}")
|
backupCount=backup_count,
|
||||||
|
encoding='utf-8',
|
||||||
return logger
|
atTime=datetime.strptime('00:00:00', '%H:%M:%S')
|
||||||
|
)
|
||||||
|
logger.info(f"日志系统已初始化,使用日期分片: {log_file}")
|
||||||
def get_logger(name: str) -> logging.Logger:
|
else:
|
||||||
"""
|
# 使用RotatingFileHandler,按大小轮转
|
||||||
获取指定名称的日志器
|
file_handler = RotatingFileHandler(
|
||||||
|
log_file,
|
||||||
参数:
|
maxBytes=max_bytes,
|
||||||
name: 日志器名称,通常使用 __name__
|
backupCount=backup_count,
|
||||||
|
encoding='utf-8'
|
||||||
返回:
|
)
|
||||||
配置好的日志器
|
logger.info(f"日志系统已初始化,使用大小轮转: {log_file}")
|
||||||
"""
|
|
||||||
return logging.getLogger(name)
|
file_handler.setLevel(file_level)
|
||||||
|
file_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
|
||||||
# 自动初始化日志系统
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
if not logging.getLogger().handlers:
|
)
|
||||||
# 只有在没有handler时才初始化,避免重复初始化
|
file_handler.setFormatter(file_formatter)
|
||||||
setup_logging()
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 设置第三方库的日志级别
|
||||||
# 便捷函数
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||||
def info(msg: str, *args, **kwargs):
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
"""记录INFO级别日志"""
|
|
||||||
logging.info(msg, *args, **kwargs)
|
logger.info(f"控制台日志级别: {logging.getLevelName(console_level)}")
|
||||||
|
logger.info(f"文件日志级别: {logging.getLevelName(file_level)}")
|
||||||
|
|
||||||
def warning(msg: str, *args, **kwargs):
|
return logger
|
||||||
"""记录WARNING级别日志"""
|
|
||||||
logging.warning(msg, *args, **kwargs)
|
|
||||||
|
def get_logger(name: str) -> logging.Logger:
|
||||||
|
"""
|
||||||
def error(msg: str, *args, **kwargs):
|
获取指定名称的日志器
|
||||||
"""记录ERROR级别日志"""
|
|
||||||
logging.error(msg, *args, **kwargs)
|
参数:
|
||||||
|
name: 日志器名称,通常使用 __name__
|
||||||
|
|
||||||
def debug(msg: str, *args, **kwargs):
|
返回:
|
||||||
"""记录DEBUG级别日志"""
|
配置好的日志器
|
||||||
logging.debug(msg, *args, **kwargs)
|
"""
|
||||||
|
return logging.getLogger(name)
|
||||||
|
|
||||||
def exception(msg: str, *args, **kwargs):
|
|
||||||
"""记录异常日志"""
|
# 自动初始化日志系统
|
||||||
logging.exception(msg, *args, **kwargs)
|
if not logging.getLogger().handlers:
|
||||||
|
# 只有在没有handler时才初始化,避免重复初始化
|
||||||
|
setup_logging()
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试日志配置
|
|
||||||
logger = get_logger(__name__)
|
# 便捷函数
|
||||||
logger.info("测试INFO日志")
|
def info(msg: str, *args, **kwargs):
|
||||||
logger.warning("测试WARNING日志")
|
"""记录INFO级别日志"""
|
||||||
logger.error("测试ERROR日志")
|
logging.info(msg, *args, **kwargs)
|
||||||
logger.debug("测试DEBUG日志")
|
|
||||||
|
|
||||||
try:
|
def warning(msg: str, *args, **kwargs):
|
||||||
raise ValueError("测试异常")
|
"""记录WARNING级别日志"""
|
||||||
except ValueError as e:
|
logging.warning(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg: str, *args, **kwargs):
|
||||||
|
"""记录ERROR级别日志"""
|
||||||
|
logging.error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg: str, *args, **kwargs):
|
||||||
|
"""记录DEBUG级别日志"""
|
||||||
|
logging.debug(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def exception(msg: str, *args, **kwargs):
|
||||||
|
"""记录异常日志"""
|
||||||
|
logging.exception(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试日志配置
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
logger.info("测试INFO日志")
|
||||||
|
logger.warning("测试WARNING日志")
|
||||||
|
logger.error("测试ERROR日志")
|
||||||
|
logger.debug("测试DEBUG日志")
|
||||||
|
|
||||||
|
try:
|
||||||
|
raise ValueError("测试异常")
|
||||||
|
except ValueError as e:
|
||||||
logger.exception("捕获到异常: %s", e)
|
logger.exception("捕获到异常: %s", e)
|
||||||
1980
src/logs/2026-01/2026-01-27.log
Normal file
1980
src/logs/2026-01/2026-01-27.log
Normal file
File diff suppressed because it is too large
Load Diff
265
src/logs/2026-01/2026-01-28.log
Normal file
265
src/logs/2026-01/2026-01-28.log
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
2026-01-28 08:37:33 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-28 08:37:33 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-28 08:37:33 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||||
|
2026-01-28 08:37:33 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-28 08:37:33 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||||
|
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-28 08:37:33 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-28 08:37:33 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-28 08:37:33 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:33 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-28 08:37:33 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-28 08:37:33 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-28 的排班信息...
|
||||||
|
2026-01-28 08:37:33 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||||
|
2026-01-28 08:37:33 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-28 08:37:35 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||||
|
2026-01-28 08:37:35 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||||
|
2026-01-28 08:37:36 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:36 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:36 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:37 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:37 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:37 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||||
|
2026-01-28 08:37:37 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:37 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:37 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||||
|
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||||
|
2026-01-28 08:37:37 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-29 的排班信息...
|
||||||
|
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||||
|
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-28 08:37:37 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:37 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:37 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||||
|
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:38 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:38 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||||
|
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:38 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:38 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:38 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:38 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-28 08:37:39 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:39 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:39 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||||
|
2026-01-28 08:37:39 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||||
|
2026-01-28 08:37:39 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||||
|
2026-01-28 08:37:40 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||||
|
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:40 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-28 08:37:40 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||||
|
2026-01-28 08:37:40 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:40 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:40 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||||
|
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-28 08:37:40 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||||
|
2026-01-28 08:37:40 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||||
|
2026-01-28 08:37:40 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7192秒
|
||||||
|
2026-01-28 08:37:41 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:41 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:41 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7191秒
|
||||||
|
2026-01-28 08:37:42 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:42 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-28 08:37:42 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:42 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||||
|
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-28 08:37:42 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-28 08:37:42 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||||
|
2026-01-28 08:37:42 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:42 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:42 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:42 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-28 08:37:42 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-28 08:37:42 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-28 08:37:43 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-28 08:37:43 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 78987 字符
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:757 - 获取成功,共 78987 字符
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||||
|
2026-01-28 08:37:43 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 78987 字符
|
||||||
|
2026-01-28 08:37:43 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 17658 字符
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||||
|
2026-01-28 08:37:43 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-28 08:37:43 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 144 条记录
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 144/144 条记录
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:778 - 已保存 144 条新记录
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||||
|
2026-01-28 08:37:43 - __main__ - INFO - gui.py:650 - 生成 2026-01-27 的日报...
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-28 08:37:43 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-28 08:37:43 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:43 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-28 08:37:43 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-28 08:37:43 - src.report - INFO - report.py:266 - 获取 2026-01-27 日报的班次人员,对应排班表日期: 2026-01-28
|
||||||
|
2026-01-28 08:37:43 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||||
|
2026-01-28 08:37:43 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-28 08:37:44 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7191秒
|
||||||
|
2026-01-28 08:37:44 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7191 秒后过期
|
||||||
|
2026-01-28 08:37:46 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:37:46 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:46 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7188秒
|
||||||
|
2026-01-28 08:37:47 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:37:47 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:37:47 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:37:47 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:37:47 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||||
|
2026-01-28 08:37:47 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:37:47 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:37:47 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||||
|
2026-01-28 08:37:47 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||||
|
2026-01-28 08:37:47 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-27
|
||||||
|
2026-01-28 08:37:47 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-27
|
||||||
|
2026-01-28 08:37:47 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:331 - 开始获取数据...
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:360 - 使用页面ID映射: 2026-01-27 -> 159049182
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:365 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-28 08:39:12 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-28 08:39:12 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-28 08:39:12 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-28 08:39:12 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 78990 字符
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:375 - 获取成功,共 78990 字符
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:379 - 正在提取布局文本...
|
||||||
|
2026-01-28 08:39:12 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 78990 字符
|
||||||
|
2026-01-28 08:39:12 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 17661 字符
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:383 - 提取完成,共 17660 字符
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:387 - 正在解析日志数据...
|
||||||
|
2026-01-28 08:39:12 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-28 08:39:12 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 144 条记录
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:391 - 解析到 144 条记录
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:396 - 正在保存到数据库...
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 144/144 条记录
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:400 - 已保存 144 条记录
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:405 - 数据库总计: 275 条记录, 53 艘船
|
||||||
|
2026-01-28 08:39:12 - __main__ - INFO - gui.py:650 - 生成 2026-01-27 的日报...
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:13 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-28 08:39:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-28 08:39:13 - src.report - INFO - report.py:266 - 获取 2026-01-27 日报的班次人员,对应排班表日期: 2026-01-28
|
||||||
|
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-28 的排班信息 (格式: 01/28/1月28日)
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7102秒
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7102 秒后过期
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-28 08:39:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-28 08:39:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7101秒
|
||||||
|
2026-01-28 08:39:14 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-28 08:39:14 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月28日 解析表格: 2026年排班表
|
||||||
|
2026-01-28 08:39:14 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-28 08:39:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-28 08:39:14 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月28日 -> 1月28日 (索引: 28)
|
||||||
|
2026-01-28 08:39:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-28 08:39:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-28 08:39:14 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-28
|
||||||
|
2026-01-28 08:39:14 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-28 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=冯栋、刘炜彬、杨俊豪
|
||||||
|
2026-01-28 08:39:14 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-27
|
||||||
|
2026-01-28 08:39:14 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-27
|
||||||
|
2026-01-28 08:39:14 - __main__ - INFO - gui.py:414 - 数据获取完成
|
||||||
233
src/logs/2026-01/2026-01-29.log
Normal file
233
src/logs/2026-01/2026-01-29.log
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
2026-01-29 08:23:17 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-29 08:23:17 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-29 08:23:18 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||||
|
2026-01-29 08:23:18 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-29 08:23:18 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||||
|
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-29 08:23:18 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-29 08:23:18 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-29 08:23:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:18 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-29 08:23:18 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-29 的排班信息...
|
||||||
|
2026-01-29 08:23:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||||
|
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||||
|
2026-01-29 08:23:18 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||||
|
2026-01-29 08:23:19 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7199秒
|
||||||
|
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:19 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:19 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:19 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||||
|
2026-01-29 08:23:19 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:19 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:19 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||||
|
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-29 08:23:19 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||||
|
2026-01-29 08:23:19 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||||
|
2026-01-29 08:23:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||||
|
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||||
|
2026-01-29 08:23:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||||
|
2026-01-29 08:23:20 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:21 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-29 08:23:21 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||||
|
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:22 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||||
|
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:22 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:22 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:22 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:22 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:23 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-29 08:23:23 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-29 08:23:23 - __main__ - INFO - gui.py:650 - 生成 2026-01-29 的日报...
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:23 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:23 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-29 08:23:23 - src.report - INFO - report.py:266 - 获取 2026-01-29 日报的班次人员,对应排班表日期: 2026-01-30
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:23 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:23 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:24 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:24 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:24 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||||
|
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-29 08:23:24 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-29
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-29
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-29 08:23:24 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-29 08:23:24 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-29 08:23:24 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-29 08:23:24 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 81156 字符
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:757 - 获取成功,共 81156 字符
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||||
|
2026-01-29 08:23:24 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 81156 字符
|
||||||
|
2026-01-29 08:23:24 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 18253 字符
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||||
|
2026-01-29 08:23:24 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-29 08:23:24 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 148 条记录
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 148/148 条记录
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:778 - 已保存 148 条新记录
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||||
|
2026-01-29 08:23:24 - __main__ - INFO - gui.py:650 - 生成 2026-01-28 的日报...
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:24 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-29 08:23:24 - src.report - INFO - report.py:266 - 获取 2026-01-28 日报的班次人员,对应排班表日期: 2026-01-29
|
||||||
|
2026-01-29 08:23:24 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-29 的排班信息 (格式: 01/29/1月29日)
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7194秒
|
||||||
|
2026-01-29 08:23:24 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7194 秒后过期
|
||||||
|
2026-01-29 08:23:25 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:25 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||||
|
2026-01-29 08:23:25 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月29日 解析表格: 2026年排班表
|
||||||
|
2026-01-29 08:23:25 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-29 08:23:25 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-29 08:23:25 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月29日 -> 1月29日 (索引: 29)
|
||||||
|
2026-01-29 08:23:25 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-29 08:23:25 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-29 08:23:25 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-29
|
||||||
|
2026-01-29 08:23:25 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-29 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-29 08:23:25 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-28
|
||||||
|
2026-01-29 08:23:25 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-28
|
||||||
|
2026-01-29 08:23:25 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||||
234
src/logs/2026-01/2026-01-30.log
Normal file
234
src/logs/2026-01/2026-01-30.log
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
2026-01-30 08:29:07 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-30 08:29:07 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-30 08:29:07 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||||
|
2026-01-30 08:29:09 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-30 08:29:09 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||||
|
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-30 08:29:09 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-30 08:29:09 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-30 08:29:09 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:09 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:09 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-30 08:29:09 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-30 的排班信息...
|
||||||
|
2026-01-30 08:29:09 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||||
|
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||||
|
2026-01-30 08:29:09 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:10 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:10 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:10 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||||
|
2026-01-30 08:29:10 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:10 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:10 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:10 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:10 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-30 08:29:11 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:11 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:11 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - WARNING - manager.py:182 - 解析结果为空,2026-02-01 未保存到数据库
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||||
|
2026-01-30 08:29:11 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||||
|
2026-01-30 08:29:11 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||||
|
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:12 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||||
|
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:12 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:12 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||||
|
2026-01-30 08:29:12 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||||
|
2026-01-30 08:29:12 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:13 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-30 08:29:13 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||||
|
2026-01-30 08:29:13 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:13 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:13 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-05 的排班信息...
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-05 的排班信息 (格式: 02/05/2月5日)
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:13 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:13 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月5日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:14 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-30 08:29:14 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月5日 -> 2月5日 (索引: 5)
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-05
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-05 的排班信息到数据库: 白班=汪钦良, 夜班=
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-30 08:29:14 - __main__ - INFO - gui.py:650 - 生成 2026-01-30 的日报...
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:14 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-30 08:29:14 - src.report - INFO - report.py:266 - 获取 2026-01-30 日报的班次人员,对应排班表日期: 2026-01-31
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:14 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:14 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-30 08:29:15 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-30
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-30
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-30 08:29:15 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-30 08:29:15 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-30 08:29:15 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-30 08:29:15 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 84056 字符
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:757 - 获取成功,共 84056 字符
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||||
|
2026-01-30 08:29:15 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 84056 字符
|
||||||
|
2026-01-30 08:29:15 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 18944 字符
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||||
|
2026-01-30 08:29:15 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-30 08:29:15 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 154 条记录
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 154/154 条记录
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:778 - 已保存 154 条新记录
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||||
|
2026-01-30 08:29:15 - __main__ - INFO - gui.py:650 - 生成 2026-01-29 的日报...
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-30 08:29:15 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:15 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-30 08:29:15 - src.report - INFO - report.py:266 - 获取 2026-01-29 日报的班次人员,对应排班表日期: 2026-01-30
|
||||||
|
2026-01-30 08:29:15 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-30 的排班信息 (格式: 01/30/1月30日)
|
||||||
|
2026-01-30 08:29:15 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7193秒
|
||||||
|
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7193 秒后过期
|
||||||
|
2026-01-30 08:29:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7192秒
|
||||||
|
2026-01-30 08:29:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月30日 解析表格: 2026年排班表
|
||||||
|
2026-01-30 08:29:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-30 08:29:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-30 08:29:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月30日 -> 1月30日 (索引: 30)
|
||||||
|
2026-01-30 08:29:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-30 08:29:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-30 08:29:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-30
|
||||||
|
2026-01-30 08:29:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-30 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-30 08:29:16 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-29
|
||||||
|
2026-01-30 08:29:16 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-29
|
||||||
|
2026-01-30 08:29:16 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||||
239
src/logs/2026-01/2026-01-31.log
Normal file
239
src/logs/2026-01/2026-01-31.log
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
2026-01-31 08:25:13 - root - INFO - logging_config.py:110 - 控制台日志级别: INFO
|
||||||
|
2026-01-31 08:25:13 - root - INFO - logging_config.py:111 - 文件日志级别: DEBUG
|
||||||
|
2026-01-31 08:25:13 - __main__ - INFO - gui.py:81 - 使用 iconphoto 设置图标成功: /home/admin1/文档/Orbitin/icons/container.png
|
||||||
|
2026-01-31 08:25:14 - __main__ - INFO - gui.py:692 - GUI启动,开始自动获取新数据...
|
||||||
|
2026-01-31 08:25:14 - __main__ - INFO - gui.py:705 - 正在刷新排班信息...
|
||||||
|
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-31 08:25:14 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-31 08:25:14 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-31 08:25:14 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:14 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:14 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:214 - 开始刷新未来 7 天的排班信息
|
||||||
|
2026-01-31 08:25:14 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-01-31 的排班信息...
|
||||||
|
2026-01-31 08:25:14 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7200秒
|
||||||
|
2026-01-31 08:25:14 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7200 秒后过期
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7199秒
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:15 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:15 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:15 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-31 08:25:15 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:15 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:15 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-01 的排班信息...
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:15 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:15 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||||
|
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-01
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-01 的排班信息到数据库: 白班=梁启迟、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-02 的排班信息...
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-02 的排班信息 (格式: 02/02/2月2日)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7198秒
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月2日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月2日 -> 2月2日 (索引: 2)
|
||||||
|
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:16 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:16 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-02
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-02 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬、杨俊豪
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-03 的排班信息...
|
||||||
|
2026-01-31 08:25:16 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-03 的排班信息 (格式: 02/03/2月3日)
|
||||||
|
2026-01-31 08:25:16 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7197秒
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月3日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月3日 -> 2月3日 (索引: 3)
|
||||||
|
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:17 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-03
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-03 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=刘炜彬、杨俊豪
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-04 的排班信息...
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-04 的排班信息 (格式: 02/04/2月4日)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月4日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月4日 -> 2月4日 (索引: 4)
|
||||||
|
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:17 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:17 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-04
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-04 的排班信息到数据库: 白班=梁启迟、汪钦良, 夜班=刘炜彬、杨俊豪
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-05 的排班信息...
|
||||||
|
2026-01-31 08:25:17 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-05 的排班信息 (格式: 02/05/2月5日)
|
||||||
|
2026-01-31 08:25:17 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7196秒
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月5日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月5日 -> 2月5日 (索引: 5)
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-05
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-05 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - DEBUG - manager.py:223 - 刷新 2026-02-06 的排班信息...
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-06 的排班信息 (格式: 02/06/2月6日)
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7195秒
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月6日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:18 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月6日 -> 2月6日 (索引: 6)
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-06
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-06 的排班信息到数据库: 白班=汪钦良、牛晨, 夜班=冯栋、杨俊豪
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:230 - 排班信息刷新完成,成功: 7, 失败: 0
|
||||||
|
2026-01-31 08:25:18 - __main__ - INFO - gui.py:650 - 生成 2026-01-31 的日报...
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-31 08:25:18 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:18 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:18 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-31 08:25:18 - src.report - INFO - report.py:266 - 获取 2026-01-31 日报的班次人员,对应排班表日期: 2026-02-01
|
||||||
|
2026-01-31 08:25:18 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-02-01 的排班信息 (格式: 02/01/2月1日)
|
||||||
|
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7195秒
|
||||||
|
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7195 秒后过期
|
||||||
|
2026-01-31 08:25:19 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:19 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:19 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7194秒
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 2月1日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:20 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 2月 (行: 14)
|
||||||
|
2026-01-31 08:25:20 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 2月1日 -> 2月1日 (索引: 1)
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-02-01
|
||||||
|
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-02-01 的排班信息到数据库: 白班=梁启迟、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-31 08:25:20 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-31
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-31
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:710 - 排班信息刷新完成
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:726 - 正在尝试获取最新作业数据...
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:751 - 正在从 Confluence 获取 HTML...
|
||||||
|
2026-01-31 08:25:20 - src.confluence.client - DEBUG - client.py:50 - Confluence客户端初始化完成,基础URL: https://confluence.westwell-lab.com/rest/api
|
||||||
|
2026-01-31 08:25:20 - src.confluence.client - DEBUG - client.py:76 - 获取Confluence内容: 159049182
|
||||||
|
2026-01-31 08:25:20 - src.confluence.client - INFO - client.py:81 - 成功获取Confluence内容: 159049182
|
||||||
|
2026-01-31 08:25:20 - src.confluence.client - INFO - client.py:122 - 获取到Confluence HTML内容,长度: 86366 字符
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:757 - 获取成功,共 86366 字符
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:761 - 正在提取布局文本...
|
||||||
|
2026-01-31 08:25:20 - src.confluence.text - DEBUG - text.py:60 - 开始解析HTML,长度: 86366 字符
|
||||||
|
2026-01-31 08:25:20 - src.confluence.text - INFO - text.py:83 - HTML提取完成,输出长度: 19525 字符
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:767 - 正在解析日志数据...
|
||||||
|
2026-01-31 08:25:20 - src.confluence.log_parser - INFO - log_parser.py:390 - 解析转堆作业: 2026-01-02 白班 2TEU
|
||||||
|
2026-01-31 08:25:20 - src.confluence.log_parser - INFO - log_parser.py:209 - 日志解析完成,共 159 条记录
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:774 - 正在保存到数据库...
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.daily_logs - INFO - daily_logs.py:237 - 批量插入完成,成功 159/159 条记录
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:778 - 已保存 159 条新记录
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:806 - 正在生成今日日报...
|
||||||
|
2026-01-31 08:25:20 - __main__ - INFO - gui.py:650 - 生成 2026-01-30 的日报...
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.daily_logs - DEBUG - daily_logs.py:160 - 数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.report - INFO - report.py:34 - 日报生成器初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:53 - 使用飞书应用凭证自动获取token
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:56 - 飞书客户端初始化完成,基础URL: https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - DEBUG - client.py:57 - 使用应用ID: cli_a9d9... 如果配置
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:20 - src.database.schedules - DEBUG - schedules.py:71 - 排班数据库表结构初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:41 - 飞书排班管理器初始化完成
|
||||||
|
2026-01-31 08:25:20 - src.report - INFO - report.py:266 - 获取 2026-01-30 日报的班次人员,对应排班表日期: 2026-01-31
|
||||||
|
2026-01-31 08:25:20 - src.feishu.manager - INFO - manager.py:138 - 获取 2026-01-31 的排班信息 (格式: 01/31/1月31日)
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:98 - 正在获取tenant_access_token,应用ID: cli_a9d9...
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:114 - 成功获取tenant_access_token,有效期: 7194秒
|
||||||
|
2026-01-31 08:25:20 - src.feishu.client - INFO - client.py:156 - token获取成功,将在 7194 秒后过期
|
||||||
|
2026-01-31 08:25:21 - src.feishu.client - INFO - client.py:203 - 获取到 7 个表格
|
||||||
|
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:90 - 找到2026年年度表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:21 - src.feishu.client - DEBUG - client.py:142 - token仍然有效,剩余时间: 7193秒
|
||||||
|
2026-01-31 08:25:21 - src.feishu.client - DEBUG - client.py:252 - 获取表格数据成功: R35cIj, 范围: A:AF
|
||||||
|
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:174 - 使用日期格式: 1月31日 解析表格: 2026年排班表
|
||||||
|
2026-01-31 08:25:21 - src.feishu.parser - INFO - parser.py:267 - 使用年度表格解析器: 2026年排班表
|
||||||
|
2026-01-31 08:25:21 - src.feishu.parser - DEBUG - parser.py:201 - 找到月份块: 1月 (行: 1)
|
||||||
|
2026-01-31 08:25:21 - src.feishu.parser - DEBUG - parser.py:108 - 找到日期列: 1月31日 -> 1月31日 (索引: 31)
|
||||||
|
2026-01-31 08:25:21 - src.database.base - DEBUG - base.py:57 - 数据库连接已建立: data/daily_logs.db
|
||||||
|
2026-01-31 08:25:21 - src.database.base - DEBUG - base.py:87 - 数据库连接已关闭
|
||||||
|
2026-01-31 08:25:21 - src.database.schedules - DEBUG - schedules.py:182 - 保存排班信息: 2026-01-31
|
||||||
|
2026-01-31 08:25:21 - src.feishu.manager - INFO - manager.py:180 - 已更新 2026-01-31 的排班信息到数据库: 白班=梁启迟、汪钦良、牛晨, 夜班=冯栋、刘炜彬
|
||||||
|
2026-01-31 08:25:21 - src.report - INFO - report.py:371 - 日报生成完成: 2026-01-30
|
||||||
|
2026-01-31 08:25:21 - __main__ - INFO - gui.py:667 - 日报生成完成: 2026-01-30
|
||||||
|
2026-01-31 08:25:21 - __main__ - INFO - gui.py:811 - 自动获取完成,GUI已就绪
|
||||||
1661
src/logs/2026-02/2026-02-01.log
Normal file
1661
src/logs/2026-02/2026-02-01.log
Normal file
File diff suppressed because it is too large
Load Diff
858
src/report.py
858
src/report.py
@@ -1,381 +1,477 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
日报生成模块
|
日报生成模块
|
||||||
更新依赖,使用新的配置和数据库模块
|
更新依赖,使用新的配置和数据库模块
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.logging_config import get_logger
|
from src.logging_config import get_logger
|
||||||
from src.database.daily_logs import DailyLogsDatabase
|
from src.database.daily_logs import DailyLogsDatabase
|
||||||
from src.feishu.manager import FeishuScheduleManager
|
from src.feishu.manager import FeishuScheduleManager
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ReportGeneratorError(Exception):
|
class ReportGeneratorError(Exception):
|
||||||
"""日报生成错误"""
|
"""日报生成错误"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DailyReportGenerator:
|
class DailyReportGenerator:
|
||||||
"""每日作业报告生成器"""
|
"""每日作业报告生成器"""
|
||||||
|
|
||||||
def __init__(self, db_path: Optional[str] = None):
|
def __init__(self, db_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
初始化日报生成器
|
初始化日报生成器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
db_path: 数据库文件路径,如果为None则使用配置
|
db_path: 数据库文件路径,如果为None则使用配置
|
||||||
"""
|
"""
|
||||||
self.db = DailyLogsDatabase(db_path)
|
self.db = DailyLogsDatabase(db_path)
|
||||||
logger.info("日报生成器初始化完成")
|
logger.info("日报生成器初始化完成")
|
||||||
|
|
||||||
def get_latest_date(self) -> str:
|
def get_latest_date(self) -> str:
|
||||||
"""
|
"""
|
||||||
获取数据库中最新的日期
|
获取数据库中最新的日期
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
最新日期字符串,格式 "YYYY-MM-DD"
|
最新日期字符串,格式 "YYYY-MM-DD"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logs = self.db.query_all(limit=1)
|
logs = self.db.query_all(limit=1)
|
||||||
if logs:
|
if logs:
|
||||||
return logs[0]['date']
|
return logs[0]['date']
|
||||||
return datetime.now().strftime('%Y-%m-%d')
|
return datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取最新日期失败: {e}")
|
logger.error(f"获取最新日期失败: {e}")
|
||||||
return datetime.now().strftime('%Y-%m-%d')
|
return datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
def get_daily_data(self, date: str) -> Dict[str, Any]:
|
def get_daily_data(self, date: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取指定日期的数据
|
获取指定日期的数据(包含手动调整)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
每日数据字典
|
每日数据字典
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logs = self.db.query_by_date(date)
|
# 使用数据库的新方法获取包含调整的数据
|
||||||
|
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||||
# 按船名汇总
|
return self.db.get_daily_data_with_adjustments(date)
|
||||||
ships: Dict[str, int] = {}
|
|
||||||
for log in logs:
|
# 降级处理:如果没有新方法,使用原始逻辑
|
||||||
ship = log['ship_name']
|
logs = self.db.query_by_date(date)
|
||||||
if ship not in ships:
|
|
||||||
ships[ship] = 0
|
# 按船名汇总TEU和尺寸箱量
|
||||||
if log.get('teu'):
|
ships: Dict[str, Dict[str, Any]] = {}
|
||||||
ships[ship] += log['teu']
|
for log in logs:
|
||||||
|
ship = log['ship_name']
|
||||||
return {
|
if ship not in ships:
|
||||||
'date': date,
|
ships[ship] = {
|
||||||
'ships': ships,
|
'teu': 0,
|
||||||
'total_teu': sum(ships.values()),
|
'twenty_feet': 0,
|
||||||
'ship_count': len(ships)
|
'forty_feet': 0
|
||||||
}
|
}
|
||||||
|
if log.get('teu'):
|
||||||
except Exception as e:
|
ships[ship]['teu'] += log['teu']
|
||||||
logger.error(f"获取每日数据失败: {date}, 错误: {e}")
|
if log.get('twenty_feet'):
|
||||||
return {
|
ships[ship]['twenty_feet'] += log['twenty_feet']
|
||||||
'date': date,
|
if log.get('forty_feet'):
|
||||||
'ships': {},
|
ships[ship]['forty_feet'] += log['forty_feet']
|
||||||
'total_teu': 0,
|
|
||||||
'ship_count': 0
|
total_teu = sum(ship_data['teu'] for ship_data in ships.values())
|
||||||
}
|
|
||||||
|
return {
|
||||||
def get_monthly_stats(self, date: str) -> Dict[str, Any]:
|
'date': date,
|
||||||
"""
|
'ships': ships,
|
||||||
获取月度统计(截止到指定日期)
|
'total_teu': total_teu,
|
||||||
|
'ship_count': len(ships),
|
||||||
参数:
|
'adjustments': [],
|
||||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
'total_adjustment_teu': 0
|
||||||
|
}
|
||||||
返回:
|
|
||||||
月度统计字典
|
except Exception as e:
|
||||||
"""
|
logger.error(f"获取每日数据失败: {date}, 错误: {e}")
|
||||||
try:
|
return {
|
||||||
year_month = date[:7] # YYYY-MM
|
'date': date,
|
||||||
target_date = datetime.strptime(date, '%Y-%m-%d').date()
|
'ships': {},
|
||||||
|
'total_teu': 0,
|
||||||
logs = self.db.query_all(limit=10000)
|
'ship_count': 0,
|
||||||
|
'adjustments': [],
|
||||||
# 只统计当月且在指定日期之前的数据
|
'total_adjustment_teu': 0
|
||||||
monthly_logs = [
|
}
|
||||||
log for log in logs
|
|
||||||
if log['date'].startswith(year_month)
|
def get_monthly_stats(self, date: str) -> Dict[str, Any]:
|
||||||
and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date
|
"""
|
||||||
]
|
获取月度统计(截止到指定日期)
|
||||||
|
|
||||||
# 按日期汇总
|
参数:
|
||||||
daily_totals: Dict[str, int] = {}
|
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||||
for log in monthly_logs:
|
|
||||||
d = log['date']
|
返回:
|
||||||
if d not in daily_totals:
|
月度统计字典
|
||||||
daily_totals[d] = 0
|
"""
|
||||||
if log.get('teu'):
|
try:
|
||||||
daily_totals[d] += log['teu']
|
year_month = date[:7] # YYYY-MM
|
||||||
|
target_date = datetime.strptime(date, '%Y-%m-%d').date()
|
||||||
# 计算当月天数(已过的天数)
|
|
||||||
current_date = datetime.strptime(date, '%Y-%m-%d')
|
logs = self.db.query_all(limit=10000)
|
||||||
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
|
||||||
days_passed = 1
|
# 只统计当月且在指定日期之前的数据
|
||||||
else:
|
monthly_logs = [
|
||||||
days_passed = current_date.day
|
log for log in logs
|
||||||
|
if log['date'].startswith(year_month)
|
||||||
# 获取未统计数据
|
and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date
|
||||||
unaccounted = self.db.get_unaccounted(year_month)
|
]
|
||||||
|
|
||||||
planned = days_passed * config.DAILY_TARGET_TEU
|
# 按日期汇总原始数据
|
||||||
actual = sum(daily_totals.values()) + unaccounted
|
daily_totals: Dict[str, int] = {}
|
||||||
|
for log in monthly_logs:
|
||||||
completion = round(actual / planned * 100, 2) if planned > 0 else 0
|
d = log['date']
|
||||||
|
if d not in daily_totals:
|
||||||
return {
|
daily_totals[d] = 0
|
||||||
'year_month': year_month,
|
if log.get('teu'):
|
||||||
'days_passed': days_passed,
|
daily_totals[d] += log['teu']
|
||||||
'planned': planned,
|
|
||||||
'actual': actual,
|
# 计算当月天数(已过的天数)
|
||||||
'unaccounted': unaccounted,
|
current_date = datetime.strptime(date, '%Y-%m-%d')
|
||||||
'completion': completion,
|
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
||||||
'daily_totals': daily_totals
|
days_passed = 1
|
||||||
}
|
else:
|
||||||
|
days_passed = current_date.day
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取月度统计失败: {date}, 错误: {e}")
|
# 获取当月所有日期的调整数据
|
||||||
return {
|
total_adjustment_teu = 0
|
||||||
'year_month': date[:7],
|
adjustment_details: Dict[str, Dict[str, int]] = {}
|
||||||
'days_passed': 0,
|
|
||||||
'planned': 0,
|
# 直接获取整个月份的所有调整记录
|
||||||
'actual': 0,
|
# 这样可以确保即使在生成日报之后执行的剔除操作也能影响当月实际作业量
|
||||||
'unaccounted': 0,
|
if hasattr(self.db, 'get_monthly_adjustments'):
|
||||||
'completion': 0,
|
monthly_adjustments = self.db.get_monthly_adjustments(year_month)
|
||||||
'daily_totals': {}
|
|
||||||
}
|
# 按日期汇总调整数据
|
||||||
|
date_adjustments: Dict[str, int] = {}
|
||||||
def get_shift_personnel(self, date: str) -> Dict[str, str]:
|
for adj in monthly_adjustments:
|
||||||
"""
|
adj_date = adj['date']
|
||||||
获取班次人员(从飞书排班表获取)
|
if adj_date not in date_adjustments:
|
||||||
|
date_adjustments[adj_date] = 0
|
||||||
注意:日报中显示的是次日的班次人员,所以需要获取 date+1 的排班
|
if adj['adjustment_type'] == 'add':
|
||||||
例如:生成 12/29 的日报,显示的是 12/30 的人员
|
date_adjustments[adj_date] += adj['teu']
|
||||||
|
elif adj['adjustment_type'] == 'exclude':
|
||||||
参数:
|
date_adjustments[adj_date] -= adj['teu']
|
||||||
date: 日期字符串,格式 "YYYY-MM-DD"
|
|
||||||
|
# 计算总调整量并构建调整详情
|
||||||
返回:
|
for adj_date, adj_teu in date_adjustments.items():
|
||||||
班次人员字典
|
if adj_teu != 0:
|
||||||
"""
|
total_adjustment_teu += adj_teu
|
||||||
try:
|
# 获取该日期的详细数据
|
||||||
# 检查飞书配置
|
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||||
if not config.FEISHU_TOKEN or not config.FEISHU_SPREADSHEET_TOKEN:
|
daily_data = self.db.get_daily_data_with_adjustments(adj_date)
|
||||||
logger.warning("飞书配置不完整,跳过排班信息获取")
|
adjustment_details[adj_date] = {
|
||||||
return self._empty_personnel()
|
'adjustment_teu': adj_teu,
|
||||||
|
'total_teu': daily_data.get('total_teu', 0)
|
||||||
# 初始化飞书排班管理器
|
}
|
||||||
manager = FeishuScheduleManager()
|
else:
|
||||||
|
# 降级处理:如果没有新方法,使用按天循环查询
|
||||||
# 计算次日日期(日报中显示的是次日班次)
|
days_in_month = target_date.day
|
||||||
parsed_date = datetime.strptime(date, '%Y-%m-%d')
|
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
|
||||||
tomorrow = (parsed_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
days_in_month = 1
|
||||||
|
|
||||||
logger.info(f"获取 {date} 日报的班次人员,对应排班表日期: {tomorrow}")
|
for day in range(1, days_in_month + 1):
|
||||||
|
day_str = f"{year_month}-{day:02d}"
|
||||||
# 获取次日的排班信息
|
# 获取该日期的调整数据
|
||||||
schedule = manager.get_schedule_for_date(tomorrow)
|
if hasattr(self.db, 'get_daily_data_with_adjustments'):
|
||||||
|
daily_data = self.db.get_daily_data_with_adjustments(day_str)
|
||||||
# 如果从飞书获取到数据,使用飞书数据
|
adjustment_teu = daily_data.get('total_adjustment_teu', 0)
|
||||||
if schedule.get('day_shift') or schedule.get('night_shift'):
|
if adjustment_teu != 0:
|
||||||
return {
|
total_adjustment_teu += adjustment_teu
|
||||||
'day_shift': schedule.get('day_shift', ''),
|
adjustment_details[day_str] = {
|
||||||
'night_shift': schedule.get('night_shift', ''),
|
'adjustment_teu': adjustment_teu,
|
||||||
'duty_phone': config.DUTY_PHONE
|
'total_teu': daily_data.get('total_teu', 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果飞书数据为空,返回空值
|
# 获取未统计数据
|
||||||
logger.warning(f"无法从飞书获取 {tomorrow} 的排班信息")
|
unaccounted = self.db.get_unaccounted(year_month)
|
||||||
return self._empty_personnel()
|
|
||||||
|
planned = days_passed * config.DAILY_TARGET_TEU
|
||||||
except Exception as e:
|
# 实际作业量 = 原始数据总计 + 未统计数据 + 调整数据总计
|
||||||
logger.error(f"获取排班信息失败: {e}")
|
actual = sum(daily_totals.values()) + unaccounted + total_adjustment_teu
|
||||||
# 降级处理:返回空值
|
|
||||||
return self._empty_personnel()
|
completion = round(actual / planned * 100, 2) if planned > 0 else 0
|
||||||
|
|
||||||
def generate_report(self, date: Optional[str] = None) -> str:
|
return {
|
||||||
"""
|
'year_month': year_month,
|
||||||
生成日报
|
'days_passed': days_passed,
|
||||||
|
'planned': planned,
|
||||||
参数:
|
'actual': actual,
|
||||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
'unaccounted': unaccounted,
|
||||||
|
'adjustment_total': total_adjustment_teu,
|
||||||
返回:
|
'completion': completion,
|
||||||
日报文本
|
'daily_totals': daily_totals,
|
||||||
|
'adjustment_details': adjustment_details
|
||||||
异常:
|
}
|
||||||
ReportGeneratorError: 生成失败
|
|
||||||
"""
|
except Exception as e:
|
||||||
try:
|
logger.error(f"获取月度统计失败: {date}, 错误: {e}")
|
||||||
if not date:
|
return {
|
||||||
date = self.get_latest_date()
|
'year_month': date[:7],
|
||||||
|
'days_passed': 0,
|
||||||
# 验证日期格式
|
'planned': 0,
|
||||||
try:
|
'actual': 0,
|
||||||
parsed = datetime.strptime(date, '%Y-%m-%d')
|
'unaccounted': 0,
|
||||||
display_date = parsed.strftime('%m/%d')
|
'adjustment_total': 0,
|
||||||
query_date = parsed.strftime('%Y-%m-%d')
|
'completion': 0,
|
||||||
except ValueError as e:
|
'daily_totals': {},
|
||||||
error_msg = f"日期格式无效: {date}, 错误: {e}"
|
'adjustment_details': {}
|
||||||
logger.error(error_msg)
|
}
|
||||||
raise ReportGeneratorError(error_msg) from e
|
|
||||||
|
def get_shift_personnel(self, date: str) -> Dict[str, str]:
|
||||||
# 获取数据
|
"""
|
||||||
daily_data = self.get_daily_data(query_date)
|
获取班次人员(从飞书排班表获取)
|
||||||
monthly_data = self.get_monthly_stats(query_date)
|
|
||||||
personnel = self.get_shift_personnel(query_date)
|
注意:日报中显示的是次日的班次人员,所以需要获取 date+1 的排班
|
||||||
|
例如:生成 12/29 的日报,显示的是 12/30 的人员
|
||||||
# 生成日报
|
|
||||||
lines: List[str] = []
|
参数:
|
||||||
lines.append(f"日期:{display_date}")
|
date: 日期字符串,格式 "YYYY-MM-DD"
|
||||||
lines.append("")
|
|
||||||
|
返回:
|
||||||
# 船次信息
|
班次人员字典
|
||||||
if daily_data['ships']:
|
"""
|
||||||
ship_lines: List[str] = []
|
try:
|
||||||
for ship, teu in sorted(daily_data['ships'].items(), key=lambda x: -x[1]):
|
# 检查飞书配置(支持应用凭证和手动token两种方式)
|
||||||
ship_lines.append(f"船名:{ship}")
|
has_feishu_config = bool(config.FEISHU_SPREADSHEET_TOKEN) and (
|
||||||
ship_lines.append(f"作业量:{teu}TEU")
|
bool(config.FEISHU_APP_ID and config.FEISHU_APP_SECRET) or
|
||||||
lines.extend(ship_lines)
|
bool(config.FEISHU_TOKEN)
|
||||||
lines.append("")
|
)
|
||||||
|
|
||||||
# 当日实际作业量
|
if not has_feishu_config:
|
||||||
lines.append(f"当日实际作业量:{daily_data['total_teu']}TEU")
|
logger.warning("飞书配置不完整,跳过排班信息获取")
|
||||||
|
logger.warning("需要配置 FEISHU_SPREADSHEET_TOKEN 和 (FEISHU_APP_ID+FEISHU_APP_SECRET 或 FEISHU_TOKEN)")
|
||||||
# 月度统计
|
return self._empty_personnel()
|
||||||
lines.append(f"当月计划作业量:{monthly_data['planned']}TEU (用天数*{config.DAILY_TARGET_TEU}TEU)")
|
|
||||||
lines.append(f"当月实际作业量:{monthly_data['actual']}TEU")
|
# 初始化飞书排班管理器
|
||||||
lines.append(f"当月完成比例:{monthly_data['completion']}%")
|
manager = FeishuScheduleManager()
|
||||||
lines.append("")
|
|
||||||
|
# 计算次日日期(日报中显示的是次日班次)
|
||||||
# 人员信息
|
parsed_date = datetime.strptime(date, '%Y-%m-%d')
|
||||||
day_personnel = personnel['day_shift']
|
tomorrow = (parsed_date + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
night_personnel = personnel['night_shift']
|
|
||||||
duty_phone = personnel['duty_phone']
|
logger.info(f"获取 {date} 日报的班次人员,对应排班表日期: {tomorrow}")
|
||||||
|
|
||||||
# 班次日期使用次日
|
# 获取次日的排班信息
|
||||||
next_day = (parsed + timedelta(days=1)).strftime('%m/%d')
|
schedule = manager.get_schedule_for_date(tomorrow)
|
||||||
lines.append(f"{next_day} 白班人员:{day_personnel}")
|
|
||||||
lines.append(f"{next_day} 夜班人员:{night_personnel}")
|
# 如果从飞书获取到数据,使用飞书数据
|
||||||
lines.append(f"24小时值班手机:{duty_phone}")
|
if schedule.get('day_shift') or schedule.get('night_shift'):
|
||||||
|
return {
|
||||||
report = "\n".join(lines)
|
'day_shift': schedule.get('day_shift', ''),
|
||||||
logger.info(f"日报生成完成: {date}")
|
'night_shift': schedule.get('night_shift', ''),
|
||||||
return report
|
'duty_phone': config.DUTY_PHONE
|
||||||
|
}
|
||||||
except ReportGeneratorError:
|
|
||||||
raise
|
# 如果飞书数据为空,返回空值
|
||||||
except Exception as e:
|
logger.warning(f"无法从飞书获取 {tomorrow} 的排班信息")
|
||||||
error_msg = f"生成日报失败: {e}"
|
return self._empty_personnel()
|
||||||
logger.error(error_msg)
|
|
||||||
raise ReportGeneratorError(error_msg) from e
|
except Exception as e:
|
||||||
|
logger.error(f"获取排班信息失败: {e}")
|
||||||
def print_report(self, date: Optional[str] = None) -> str:
|
# 降级处理:返回空值
|
||||||
"""
|
return self._empty_personnel()
|
||||||
打印日报
|
|
||||||
|
def generate_report(self, date: Optional[str] = None) -> str:
|
||||||
参数:
|
"""
|
||||||
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
生成日报
|
||||||
|
|
||||||
返回:
|
参数:
|
||||||
日报文本
|
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||||
"""
|
|
||||||
try:
|
返回:
|
||||||
report = self.generate_report(date)
|
日报文本
|
||||||
print(report)
|
|
||||||
return report
|
异常:
|
||||||
|
ReportGeneratorError: 生成失败
|
||||||
except ReportGeneratorError as e:
|
"""
|
||||||
print(f"生成日报失败: {e}")
|
try:
|
||||||
return ""
|
if not date:
|
||||||
|
date = self.get_latest_date()
|
||||||
def save_report_to_file(self, date: Optional[str] = None, filepath: Optional[str] = None) -> bool:
|
|
||||||
"""
|
# 验证日期格式
|
||||||
保存日报到文件
|
try:
|
||||||
|
parsed = datetime.strptime(date, '%Y-%m-%d')
|
||||||
参数:
|
display_date = parsed.strftime('%m/%d')
|
||||||
date: 日期字符串,如果为None则使用最新日期
|
query_date = parsed.strftime('%Y-%m-%d')
|
||||||
filepath: 文件路径,如果为None则使用默认路径
|
except ValueError as e:
|
||||||
|
error_msg = f"日期格式无效: {date}, 错误: {e}"
|
||||||
返回:
|
logger.error(error_msg)
|
||||||
是否成功
|
raise ReportGeneratorError(error_msg) from e
|
||||||
"""
|
|
||||||
try:
|
# 获取数据
|
||||||
report = self.generate_report(date)
|
daily_data = self.get_daily_data(query_date)
|
||||||
|
monthly_data = self.get_monthly_stats(query_date)
|
||||||
if filepath is None:
|
personnel = self.get_shift_personnel(query_date)
|
||||||
# 使用默认路径
|
|
||||||
import os
|
# 生成日报
|
||||||
report_dir = "reports"
|
lines: List[str] = []
|
||||||
os.makedirs(report_dir, exist_ok=True)
|
lines.append(f"日期:{display_date}")
|
||||||
|
lines.append("")
|
||||||
if date is None:
|
|
||||||
date = self.get_latest_date()
|
# 船次信息
|
||||||
filename = f"daily_report_{date}.txt"
|
if daily_data['ships']:
|
||||||
filepath = os.path.join(report_dir, filename)
|
ship_lines: List[str] = []
|
||||||
|
for ship, ship_data in sorted(daily_data['ships'].items(), key=lambda x: -x[1]['teu']):
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
ship_lines.append(f"船名:{ship}")
|
||||||
f.write(report)
|
teu = ship_data['teu']
|
||||||
|
twenty_feet = ship_data.get('twenty_feet', 0)
|
||||||
logger.info(f"日报已保存到文件: {filepath}")
|
forty_feet = ship_data.get('forty_feet', 0)
|
||||||
return True
|
|
||||||
|
# 构建尺寸箱量字符串
|
||||||
except Exception as e:
|
size_parts = []
|
||||||
logger.error(f"保存日报到文件失败: {e}")
|
if twenty_feet > 0:
|
||||||
return False
|
size_parts.append(f"20尺*{twenty_feet}")
|
||||||
|
if forty_feet > 0:
|
||||||
def _empty_personnel(self) -> Dict[str, str]:
|
size_parts.append(f"40尺*{forty_feet}")
|
||||||
"""返回空的人员信息"""
|
|
||||||
return {
|
if size_parts:
|
||||||
'day_shift': '',
|
size_str = " ".join(size_parts)
|
||||||
'night_shift': '',
|
ship_lines.append(f"作业量:{teu}TEU({size_str})")
|
||||||
'duty_phone': config.DUTY_PHONE
|
else:
|
||||||
}
|
ship_lines.append(f"作业量:{teu}TEU")
|
||||||
|
|
||||||
def close(self):
|
lines.extend(ship_lines)
|
||||||
"""关闭数据库连接"""
|
lines.append("")
|
||||||
self.db.close()
|
|
||||||
|
# 当日实际作业量
|
||||||
|
lines.append(f"当日实际作业量:{daily_data['total_teu']}TEU")
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试代码
|
# 月度统计
|
||||||
import sys
|
lines.append(f"当月计划作业量:{monthly_data['planned']}TEU (用天数*{config.DAILY_TARGET_TEU}TEU)")
|
||||||
|
lines.append(f"当月实际作业量:{monthly_data['actual']}TEU")
|
||||||
# 设置日志
|
lines.append(f"当月完成比例:{monthly_data['completion']}%")
|
||||||
logging.basicConfig(level=logging.INFO)
|
lines.append("")
|
||||||
|
|
||||||
generator = DailyReportGenerator()
|
# 人员信息
|
||||||
|
day_personnel = personnel['day_shift']
|
||||||
try:
|
night_personnel = personnel['night_shift']
|
||||||
# 测试获取最新日期
|
duty_phone = personnel['duty_phone']
|
||||||
latest_date = generator.get_latest_date()
|
|
||||||
print(f"最新日期: {latest_date}")
|
# 班次日期使用次日
|
||||||
|
next_day = (parsed + timedelta(days=1)).strftime('%m/%d')
|
||||||
# 测试生成日报
|
lines.append(f"{next_day} 白班人员:{day_personnel}")
|
||||||
report = generator.generate_report(latest_date)
|
lines.append(f"{next_day} 夜班人员:{night_personnel}")
|
||||||
print(f"\n日报内容:\n{report}")
|
lines.append(f"24小时值班手机:{duty_phone}")
|
||||||
|
|
||||||
# 测试保存到文件
|
report = "\n".join(lines)
|
||||||
success = generator.save_report_to_file(latest_date)
|
logger.info(f"日报生成完成: {date}")
|
||||||
print(f"\n保存到文件: {'成功' if success else '失败'}")
|
return report
|
||||||
|
|
||||||
except ReportGeneratorError as e:
|
except ReportGeneratorError:
|
||||||
print(f"日报生成错误: {e}")
|
raise
|
||||||
sys.exit(1)
|
except Exception as e:
|
||||||
except Exception as e:
|
error_msg = f"生成日报失败: {e}"
|
||||||
print(f"未知错误: {e}")
|
logger.error(error_msg)
|
||||||
sys.exit(1)
|
raise ReportGeneratorError(error_msg) from e
|
||||||
finally:
|
|
||||||
generator.close()
|
def print_report(self, date: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
打印日报
|
||||||
|
|
||||||
|
参数:
|
||||||
|
date: 日期字符串,格式 "YYYY-MM-DD",如果为None则使用最新日期
|
||||||
|
|
||||||
|
返回:
|
||||||
|
日报文本
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
report = self.generate_report(date)
|
||||||
|
print(report)
|
||||||
|
return report
|
||||||
|
|
||||||
|
except ReportGeneratorError as e:
|
||||||
|
print(f"生成日报失败: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def save_report_to_file(self, date: Optional[str] = None, filepath: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
保存日报到文件
|
||||||
|
|
||||||
|
参数:
|
||||||
|
date: 日期字符串,如果为None则使用最新日期
|
||||||
|
filepath: 文件路径,如果为None则使用默认路径
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
report = self.generate_report(date)
|
||||||
|
|
||||||
|
if filepath is None:
|
||||||
|
# 使用默认路径
|
||||||
|
import os
|
||||||
|
report_dir = "reports"
|
||||||
|
os.makedirs(report_dir, exist_ok=True)
|
||||||
|
|
||||||
|
if date is None:
|
||||||
|
date = self.get_latest_date()
|
||||||
|
filename = f"daily_report_{date}.txt"
|
||||||
|
filepath = os.path.join(report_dir, filename)
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(report)
|
||||||
|
|
||||||
|
logger.info(f"日报已保存到文件: {filepath}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存日报到文件失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _empty_personnel(self) -> Dict[str, str]:
|
||||||
|
"""返回空的人员信息"""
|
||||||
|
return {
|
||||||
|
'day_shift': '',
|
||||||
|
'night_shift': '',
|
||||||
|
'duty_phone': config.DUTY_PHONE
|
||||||
|
}
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭数据库连接"""
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试代码
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 设置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
generator = DailyReportGenerator()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 测试获取最新日期
|
||||||
|
latest_date = generator.get_latest_date()
|
||||||
|
print(f"最新日期: {latest_date}")
|
||||||
|
|
||||||
|
# 测试生成日报
|
||||||
|
report = generator.generate_report(latest_date)
|
||||||
|
print(f"\n日报内容:\n{report}")
|
||||||
|
|
||||||
|
# 测试保存到文件
|
||||||
|
success = generator.save_report_to_file(latest_date)
|
||||||
|
print(f"\n保存到文件: {'成功' if success else '失败'}")
|
||||||
|
|
||||||
|
except ReportGeneratorError as e:
|
||||||
|
print(f"日报生成错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"未知错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
generator.close()
|
||||||
|
|||||||
356
src/retry.py
Normal file
356
src/retry.py
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
重试机制模块
|
||||||
|
提供重试装饰器和工具函数
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Optional, Type, Tuple, Any
|
||||||
|
|
||||||
|
from src.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def retry(
|
||||||
|
max_attempts: int = 3,
|
||||||
|
delay: float = 1.0,
|
||||||
|
backoff_factor: float = 2.0,
|
||||||
|
exceptions: Optional[Tuple[Type[Exception], ...]] = None,
|
||||||
|
on_retry: Optional[Callable[[int, Exception], None]] = None
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
重试装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
max_attempts: 最大重试次数
|
||||||
|
delay: 初始延迟时间(秒)
|
||||||
|
backoff_factor: 退避因子,每次重试延迟时间乘以该因子
|
||||||
|
exceptions: 要捕获的异常类型,None表示捕获所有异常
|
||||||
|
on_retry: 重试时的回调函数,参数为 (attempt, exception)
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@retry(max_attempts=3, delay=2.0, backoff_factor=2.0)
|
||||||
|
def fetch_data():
|
||||||
|
# 可能失败的代码
|
||||||
|
pass
|
||||||
|
|
||||||
|
@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
|
||||||
|
def network_request():
|
||||||
|
# 网络请求代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
# 检查是否需要捕获此异常
|
||||||
|
if exceptions and not isinstance(e, exceptions):
|
||||||
|
raise
|
||||||
|
|
||||||
|
last_exception = e
|
||||||
|
|
||||||
|
# 如果是最后一次尝试,不再重试
|
||||||
|
if attempt == max_attempts - 1:
|
||||||
|
logger.error(
|
||||||
|
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 计算延迟时间
|
||||||
|
current_delay = delay * (backoff_factor ** attempt)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||||
|
f"{current_delay:.2f}秒后重试..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用重试回调
|
||||||
|
if on_retry:
|
||||||
|
try:
|
||||||
|
on_retry(attempt + 1, e)
|
||||||
|
except Exception as callback_error:
|
||||||
|
logger.error(f"重试回调执行失败: {callback_error}")
|
||||||
|
|
||||||
|
# 等待
|
||||||
|
time.sleep(current_delay)
|
||||||
|
|
||||||
|
# 理论上不会到达这里,但为了类型检查
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def retry_with_exponential_backoff(
|
||||||
|
max_attempts: int = 3,
|
||||||
|
initial_delay: float = 1.0,
|
||||||
|
max_delay: float = 60.0
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
使用指数退避的重试装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
max_attempts: 最大重试次数
|
||||||
|
initial_delay: 初始延迟时间(秒)
|
||||||
|
max_delay: 最大延迟时间(秒)
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@retry_with_exponential_backoff(max_attempts=5, initial_delay=2.0)
|
||||||
|
def api_call():
|
||||||
|
# API调用代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
last_exception = e
|
||||||
|
|
||||||
|
if attempt == max_attempts - 1:
|
||||||
|
logger.error(
|
||||||
|
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 计算延迟时间(指数退避,但不超过最大延迟)
|
||||||
|
current_delay = min(initial_delay * (2 ** attempt), max_delay)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||||
|
f"{current_delay:.2f}秒后重试..."
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(current_delay)
|
||||||
|
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def retry_on_exception(
|
||||||
|
exception_type: Type[Exception],
|
||||||
|
max_attempts: int = 3,
|
||||||
|
delay: float = 1.0
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
只在特定异常时重试的装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
exception_type: 要捕获的异常类型
|
||||||
|
max_attempts: 最大重试次数
|
||||||
|
delay: 延迟时间(秒)
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@retry_on_exception(ConnectionError, max_attempts=5, delay=2.0)
|
||||||
|
def fetch_data():
|
||||||
|
# 可能抛出 ConnectionError 的代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except exception_type as e:
|
||||||
|
last_exception = e
|
||||||
|
|
||||||
|
if attempt == max_attempts - 1:
|
||||||
|
logger.error(
|
||||||
|
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||||
|
f"{delay:.2f}秒后重试..."
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class RetryContext:
|
||||||
|
"""重试上下文管理器"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
operation_name: str,
|
||||||
|
max_attempts: int = 3,
|
||||||
|
delay: float = 1.0,
|
||||||
|
backoff_factor: float = 2.0,
|
||||||
|
exceptions: Optional[Tuple[Type[Exception], ...]] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化重试上下文
|
||||||
|
|
||||||
|
参数:
|
||||||
|
operation_name: 操作名称
|
||||||
|
max_attempts: 最大重试次数
|
||||||
|
delay: 初始延迟时间(秒)
|
||||||
|
backoff_factor: 退避因子
|
||||||
|
exceptions: 要捕获的异常类型
|
||||||
|
"""
|
||||||
|
self.operation_name = operation_name
|
||||||
|
self.max_attempts = max_attempts
|
||||||
|
self.delay = delay
|
||||||
|
self.backoff_factor = backoff_factor
|
||||||
|
self.exceptions = exceptions
|
||||||
|
self.attempt = 0
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.attempt = 0
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if exc_type is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查是否需要捕获此异常
|
||||||
|
if self.exceptions and not isinstance(exc_val, self.exceptions):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.attempt += 1
|
||||||
|
|
||||||
|
# 如果超过最大尝试次数,不再重试
|
||||||
|
if self.attempt >= self.max_attempts:
|
||||||
|
logger.error(
|
||||||
|
f"{self.operation_name} 在 {self.max_attempts} 次尝试后仍然失败: {exc_val}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 计算延迟时间
|
||||||
|
current_delay = self.delay * (self.backoff_factor ** (self.attempt - 1))
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"{self.operation_name} 第 {self.attempt} 次尝试失败: {exc_val}, "
|
||||||
|
f"{current_delay:.2f}秒后重试..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 等待
|
||||||
|
time.sleep(current_delay)
|
||||||
|
|
||||||
|
# 抑制异常,继续重试
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def async_retry(
|
||||||
|
max_attempts: int = 3,
|
||||||
|
delay: float = 1.0,
|
||||||
|
backoff_factor: float = 2.0
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
异步重试装饰器(用于异步函数)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
max_attempts: 最大重试次数
|
||||||
|
delay: 初始延迟时间(秒)
|
||||||
|
backoff_factor: 退避因子
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@async_retry(max_attempts=3, delay=2.0)
|
||||||
|
async def async_fetch_data():
|
||||||
|
# 异步代码
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs) -> Any:
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
last_exception = e
|
||||||
|
|
||||||
|
if attempt == max_attempts - 1:
|
||||||
|
logger.error(
|
||||||
|
f"{func.__name__} 在 {max_attempts} 次尝试后仍然失败: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 计算延迟时间
|
||||||
|
current_delay = delay * (backoff_factor ** attempt)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"{func.__name__} 第 {attempt + 1} 次尝试失败: {e}, "
|
||||||
|
f"{current_delay:.2f}秒后重试..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 异步等待
|
||||||
|
await asyncio.sleep(current_delay)
|
||||||
|
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 测试代码
|
||||||
|
|
||||||
|
# 测试重试装饰器
|
||||||
|
call_count = 0
|
||||||
|
|
||||||
|
@retry(max_attempts=3, delay=0.1)
|
||||||
|
def test_retry():
|
||||||
|
global call_count
|
||||||
|
call_count += 1
|
||||||
|
print(f"调用次数: {call_count}")
|
||||||
|
if call_count < 3:
|
||||||
|
raise ValueError("测试异常")
|
||||||
|
return "成功"
|
||||||
|
|
||||||
|
result = test_retry()
|
||||||
|
print(f"测试结果: {result}")
|
||||||
|
|
||||||
|
# 测试重试上下文管理器
|
||||||
|
context_call_count = 0
|
||||||
|
|
||||||
|
def test_context_operation():
|
||||||
|
global context_call_count
|
||||||
|
context_call_count += 1
|
||||||
|
print(f"上下文调用次数: {context_call_count}")
|
||||||
|
if context_call_count < 3:
|
||||||
|
raise ValueError("测试异常")
|
||||||
|
return "成功"
|
||||||
|
|
||||||
|
with RetryContext("测试操作", max_attempts=3, delay=0.1):
|
||||||
|
result = test_context_operation()
|
||||||
|
print(f"上下文测试结果: {result}")
|
||||||
|
|
||||||
|
# 测试特定异常重试
|
||||||
|
@retry_on_exception(ValueError, max_attempts=3, delay=0.1)
|
||||||
|
def test_specific_exception():
|
||||||
|
raise ValueError("测试异常")
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_specific_exception()
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"特定异常测试: {e}")
|
||||||
Reference in New Issue
Block a user