13 Commits
gui ... n1

Author SHA1 Message Date
fuzhou
0a576b04cf n1 2026-02-01 20:56:37 +08:00
486313044c fix: 修复排班表读取时缓存优先的问题
- 移除了缓存优先逻辑,改为每次从飞书获取最新数据
- 每次都覆盖保存到数据库,确保人员变动能及时更新
- 移除了 check_sheet_update 检查(因为飞书表格即使人员变动,版本号也可能不变)
2026-01-07 20:06:56 +08:00
84e907b93e feat: 日志日期分片和尺寸解析增强
- 日志文件按日期分片存储 (logs/YYYY-MM/YYYY-MM-DD.log)
- 增强尺寸箱量解析支持多种格式 (*和×分隔符)
- 支持带括号和无括号两种尺寸格式
2026-01-04 01:19:07 +08:00
5fd05fcd3c feat: 添加转堆作业解析功能
- 新增 _parse_relocation() 方法解析转堆作业
- 新增 _parse_log_entry() 方法复用解析逻辑
- 转堆作业使用 '转堆作业' 作为船名标识
2026-01-03 22:37:30 +08:00
f04478fd8f docs: 重写README为项目说明文档,包含功能特性、项目结构、核心模块说明 2026-01-02 03:00:28 +08:00
f5ba5624aa docs: 修正月底数据调整的描述,适用于每个月最后一天 2026-01-02 02:54:18 +08:00
071a3f05f3 docs: 重写使用手册,更清晰的操作指南
- 新增目录结构,便于快速导航
- 添加GUI界面布局说明(ASCII图示)
- 详细说明各功能的使用场景和操作步骤
- 新增常见场景章节(日常使用、月底处理、月初补调、跨日船)
- 完善故障排除章节
- 添加附录(文件结构、数据库表结构、版本历史)
2026-01-02 02:48:15 +08:00
bb3f25a643 fix: 修复月份选择器问题,确保12月正确显示
- 修复跨年月份计算逻辑(1月时正确计算为去年12月)
- 改进_get_month_list()方法,生成正确的近12个月列表
- 增加Combobox宽度以完整显示月份值如'2025-12'
- 优化手动剔除次月多统计的船对话框
2026-01-02 02:46:56 +08:00
53eef800b4 feat: 新增月底/月初数据调整和Confluence月份页面映射功能
- 新增月底最后一天自动剔除12点后数据功能
- 实现月底剔除数据自动转移到次月1号
- 新增Confluence月份页面ID映射功能,解决每月页面ID变化问题
- 修复1月份页面解析问题,支持'2026.1.1'日期格式
- 优化GUI界面,增加页面ID配置管理
- 精简README文档,增加详细功能说明
- 修复月度统计计算包含调整数据的问题
2026-01-02 01:29:03 +08:00
1b688c1603 refactor: 移除月初1号的添加数据对话框
优化逻辑:既然月底剔除的数据已经自动添加到次月1号,月初1号就不需要再弹窗让用户手动填写了。

修改内容:
1. 修改_handle_post_fetch_adjustment()方法
   - 只保留月底最后一天的弹窗逻辑
   - 移除月初1号的弹窗逻辑
   - 简化用户操作流程

2. 业务逻辑优化
   - 月底剔除数据 → 自动添加到次月1号
   - 月初1号不再需要用户手动填写
   - 减少了不必要的弹窗干扰

测试通过:月初1号不再弹出添加数据对话框,月底逻辑正常工作。
2026-01-02 00:19:47 +08:00
0cbc587bf3 feat: 实现月底/月初数据调整功能
1. 新增月底/月初智能数据调整功能
   - 月底最后一天自动弹出剔除数据对话框
   - 月初1号自动弹出添加数据对话框
   - 普通日期不弹出对话框

2. 实现月底剔除数据自动转移到次月1号
   - 月底剔除的数据自动添加到次月1号统计
   - 支持跨月、跨年数据转移
   - 数据备注自动记录转移信息

3. 修复自动获取数据后不弹出调整对话框的问题
   - 修改auto_fetch_data()方法,成功获取数据后调用调整处理
   - 确保第一次打开GUI也能弹出相应对话框

4. 修复月度统计不包含调整数据的问题
   - 修改get_monthly_stats()方法包含手动调整数据
   - 确保调整数据正确影响月度统计

5. 恢复日报原始模板格式
   - 移除调整数据的详细说明
   - 保持原始日报模板,只显示最终结果

6. 数据库增强
   - 新增manual_adjustments表存储手动调整数据
   - 实现调整数据的增删改查方法
   - 实现包含调整数据的每日数据获取方法

测试通过:所有功能正常工作,数据计算准确。
2026-01-02 00:08:57 +08:00
9b19015156 feat: 添加飞书tenant_access_token自动获取功能
- 在FeishuSheetsClient中添加_get_tenant_access_token()方法
- 实现token自动缓存和刷新机制(提前30分钟刷新)
- 更新配置类支持FEISHU_APP_ID和FEISHU_APP_SECRET
- 从.env中移除FEISHU_TOKEN,完全使用应用凭证
- 更新report.py和gui.py支持新的配置检查逻辑
- 更新FeishuScheduleManager配置检查逻辑
- 更新文档和示例文件说明新的配置方式

系统现在支持两种认证方式:
1. 推荐:使用应用凭证(FEISHU_APP_ID + FEISHU_APP_SECRET)
2. 备选:使用手动token(FEISHU_TOKEN)

所有功能测试通过,系统能自动获取、缓存和刷新token。
2025-12-31 06:03:51 +08:00
929c4b836f feat: 添加尺寸箱量解析和显示功能
- 更新ShipLog数据类以支持20尺和40尺箱量字段
- 修改日志解析器提取尺寸箱量数据(支持格式如'95TEU(20尺*95)'和'90TEU(20尺*52 40尺*19)')
- 更新数据库表结构存储尺寸箱量
- 修改报告生成器在日报中显示尺寸箱量信息
- 修复解析器分隔符处理逻辑
- 确保二次靠泊记录尺寸箱量正确合并
2025-12-31 05:21:16 +08:00
34 changed files with 13392 additions and 4765 deletions

View File

@@ -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
View File

@@ -23,6 +23,8 @@ debug/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
AGENTS.md
# IDE # IDE
.vscode/ .vscode/
plans/

View File

@@ -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
View File

@@ -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
View 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
View File

@@ -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:

View File

@@ -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']

View File

@@ -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("配置验证失败")

View File

@@ -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'
] ]

View File

@@ -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)

View File

@@ -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: TEU20尺*240尺*3 或 TEU20尺*240尺*3
size_pattern = re.search(r'TEU[,\s]*([^]+)', cleaned)
# 测试合并功能 if not size_pattern:
duplicate_text = """ # 格式2: TEU20尺*240尺*3- 无空格版本
日期2025.12.30 size_pattern = re.search(r'TEU[(]([^)]+)[)]', cleaned)
——————————————————————————————————————————————— if not size_pattern:
白班: # 格式3: 作业量/效率100TEU20尺*240尺*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: TEU20尺*240尺*3 或 TEU20尺*240尺*3
size_pattern = re.search(r'TEU[,\s]*([^]+)', cleaned)
if not size_pattern:
# 格式2: TEU20尺*240尺*3- 无空格版本
size_pattern = re.search(r'TEU[(]([^)]+)[)]', cleaned)
if not size_pattern:
# 格式3: 作业量/效率100TEU20尺*240尺*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)

View File

@@ -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)

View File

@@ -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('&nbsp;', ' ').replace('&', '&').replace('<', '<').replace('>', '>') text = text.replace('&nbsp;', ' ').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)

View File

@@ -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

Binary file not shown.

View File

@@ -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'
] ]

View File

@@ -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

View File

@@ -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
View 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}")

View File

@@ -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'
] ]

View File

@@ -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)

View File

@@ -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)}")

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View 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 - 数据获取完成

View 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已就绪

View 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已就绪

View 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已就绪

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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}")