feat: 新增月底/月初数据调整和Confluence月份页面映射功能
- 新增月底最后一天自动剔除12点后数据功能 - 实现月底剔除数据自动转移到次月1号 - 新增Confluence月份页面ID映射功能,解决每月页面ID变化问题 - 修复1月份页面解析问题,支持'2026.1.1'日期格式 - 优化GUI界面,增加页面ID配置管理 - 精简README文档,增加详细功能说明 - 修复月度统计计算包含调整数据的问题
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ debug/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
467
README.md
467
README.md
@@ -4,28 +4,144 @@
|
|||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 从 Confluence 获取交接班日志 HTML
|
- **数据获取**:从 Confluence API 获取交接班日志 HTML
|
||||||
- 提取保留布局的文本内容
|
- **文本提取**:提取保留布局的文本内容,支持表格格式化
|
||||||
- SQLite3 数据库存储
|
- **数据库存储**:SQLite3 数据库存储,支持二次靠泊记录合并
|
||||||
- 生成日报和月度统计
|
- **报表生成**:生成日报和月度统计,包含完成率计算
|
||||||
- 支持未统计数据手动录入
|
- **数据调整**:支持手动添加/剔除统计数据,月底数据自动转移
|
||||||
- 支持去除多余统计数据(对称功能)
|
- **智能调整**:月底最后一天自动询问剔除12点后数据,月初自动添加上月数据
|
||||||
- 支持二次靠泊记录合并
|
- **GUI界面**:tkinter 图形界面,支持一键操作
|
||||||
- GUI 图形界面(可选)
|
- **飞书集成**:自动获取排班人员信息,支持应用凭证自动刷新token
|
||||||
- 飞书排班表集成(自动获取班次人员)
|
- **月份页面映射**:支持配置各月份的Confluence页面ID,解决每月页面ID变化问题
|
||||||
|
|
||||||
## 项目结构
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install requests beautifulsoup4 python-dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制配置文件模板
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 编辑 .env 文件,填入以下配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件 (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Confluence 配置
|
||||||
|
CONFLUENCE_BASE_URL=https://confluence.westwell-lab.com/rest/api
|
||||||
|
CONFLUENCE_TOKEN=your-api-token
|
||||||
|
CONFLUENCE_CONTENT_ID=155764524
|
||||||
|
|
||||||
|
# 飞书表格配置(用于获取排班人员信息)
|
||||||
|
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
||||||
|
FEISHU_SPREADSHEET_TOKEN=EgNPssi2ghZ7BLtGiTxcIBUmnVh
|
||||||
|
|
||||||
|
# 飞书应用凭证(推荐方式,自动获取tenant_access_token)
|
||||||
|
FEISHU_APP_ID=your-feishu-app-id
|
||||||
|
FEISHU_APP_SECRET=your-feishu-app-secret
|
||||||
|
|
||||||
|
# 业务配置
|
||||||
|
DAILY_TARGET_TEU=300 # 每日目标TEU数量,用于计算完成率
|
||||||
|
DUTY_PHONE=13107662315 # 值班电话,显示在日报中
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
#### 命令行方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取并保存数据到数据库
|
||||||
|
python3 main.py fetch-save
|
||||||
|
|
||||||
|
# 仅获取HTML并提取文本(保存到debug目录)
|
||||||
|
python3 main.py fetch
|
||||||
|
|
||||||
|
# 生成日报(指定日期)
|
||||||
|
python3 main.py report 2025-12-28
|
||||||
|
|
||||||
|
# 生成今日日报
|
||||||
|
python3 main.py report-today
|
||||||
|
|
||||||
|
# 添加未统计数据
|
||||||
|
python3 main.py --unaccounted 118 --month 2025-12
|
||||||
|
|
||||||
|
# 去除未统计数据
|
||||||
|
python3 main.py --remove-unaccounted --month 2025-12
|
||||||
|
|
||||||
|
# 配置测试(验证所有连接)
|
||||||
|
python3 main.py config-test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GUI 方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## GUI功能详解
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
- **获取并处理数据**:从Confluence获取数据并保存到数据库
|
||||||
|
- **生成日报**:生成指定日期的日报,支持复制内容
|
||||||
|
- **今日日报**:自动获取前一天数据并生成日报
|
||||||
|
- **重置数据库**:清空数据库并重新获取数据
|
||||||
|
|
||||||
|
### 数据调整功能
|
||||||
|
- **添加未统计数据**:用于补全缺失的箱量
|
||||||
|
- **去除多余统计数据**:用于删除多余统计的箱量(对称功能)
|
||||||
|
- **月底智能调整**:月底最后一天自动弹出剔除对话框
|
||||||
|
- **数据自动转移**:月底剔除的数据自动转移到次月1号
|
||||||
|
|
||||||
|
### 配置管理
|
||||||
|
- **管理月份页面ID映射**:配置各月份的Confluence页面ID
|
||||||
|
- **数据库统计**:显示当月每艘船的作业量总计
|
||||||
|
- **自动刷新排班信息**:从飞书获取最新的排班人员信息
|
||||||
|
|
||||||
|
## 高级功能说明
|
||||||
|
|
||||||
|
### 月份页面ID映射
|
||||||
|
由于每月Confluence页面ID不同,系统支持配置月份到页面ID的映射:
|
||||||
|
- 在GUI中点击"管理月份页面ID映射"按钮
|
||||||
|
- 添加、编辑、删除各月份的页面ID配置
|
||||||
|
- 获取数据时自动使用当前月份对应的页面ID
|
||||||
|
|
||||||
|
### 月底/月初数据调整
|
||||||
|
系统支持智能化的月底/月初数据调整:
|
||||||
|
|
||||||
|
1. **月底最后一天**:
|
||||||
|
- 获取数据后自动询问是否需要剔除12点后的数据
|
||||||
|
- 用户可以输入需要剔除的船名、TEU以及具体尺寸(20尺/40尺)
|
||||||
|
- 剔除后的数据会自动转移到次月1号
|
||||||
|
|
||||||
|
2. **月初1号**:
|
||||||
|
- 系统自动添加上月剔除的数据,无需用户手动操作
|
||||||
|
- 确保月度数据的准确性和连续性
|
||||||
|
|
||||||
|
3. **其他日期**:
|
||||||
|
- 默认不弹出调整对话框
|
||||||
|
- 但GUI侧边栏保留了手动添加/剔除TEU的功能入口
|
||||||
|
|
||||||
|
### 二次靠泊合并
|
||||||
|
解析时会自动合并同一天的二次靠泊记录:
|
||||||
|
- 夜班 学友洋山: 273TEU
|
||||||
|
- 夜班 学友洋山(二次靠泊): 14TEU
|
||||||
|
- 合并后: 夜班 学友洋山: 287TEU
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
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 # 排班数据缓存
|
||||||
@@ -46,342 +162,43 @@ 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 # 模块导出
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install requests beautifulsoup4 python-dotenv
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置
|
|
||||||
|
|
||||||
在 `.env` 文件中配置:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
# Confluence 配置
|
|
||||||
CONFLUENCE_BASE_URL=https://your-confluence.atlassian.net/rest/api
|
|
||||||
CONFLUENCE_TOKEN=your-api-token
|
|
||||||
CONFLUENCE_CONTENT_ID=155764524
|
|
||||||
|
|
||||||
# 飞书表格配置(用于获取排班人员信息)
|
|
||||||
FEISHU_BASE_URL=https://open.feishu.cn/open-apis/sheets/v3
|
|
||||||
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
|
|
||||||
|
|
||||||
# 数据库配置
|
|
||||||
DATABASE_PATH=data/daily_logs.db
|
|
||||||
|
|
||||||
# 业务配置
|
|
||||||
DAILY_TARGET_TEU=300 # 每日目标TEU数量,用于计算完成率
|
|
||||||
DUTY_PHONE=13107662315 # 值班电话,显示在日报中
|
|
||||||
SEPARATOR_CHAR=─ # 分隔线字符,用于格式化输出
|
|
||||||
SEPARATOR_LENGTH=50 # 分隔线长度
|
|
||||||
SCHEDULE_REFRESH_DAYS=30 # 排班数据刷新间隔(天)
|
|
||||||
```
|
|
||||||
|
|
||||||
参考 `.env.example` 文件创建 `.env` 文件。
|
|
||||||
|
|
||||||
### 使用方法
|
|
||||||
|
|
||||||
#### 命令行方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 默认:获取、提取、解析并保存到数据库
|
|
||||||
python3 main.py fetch-save
|
|
||||||
|
|
||||||
# 仅获取HTML并提取文本(保存到debug目录)
|
|
||||||
python3 main.py fetch
|
|
||||||
|
|
||||||
# 获取并保存带时间戳的debug文件
|
|
||||||
python3 main.py fetch-debug
|
|
||||||
|
|
||||||
# 生成日报(指定日期)
|
|
||||||
python3 main.py report 2025-12-28
|
|
||||||
|
|
||||||
# 生成今日日报
|
|
||||||
python3 main.py report-today
|
|
||||||
|
|
||||||
# 配置测试(验证所有连接)
|
|
||||||
python3 main.py config-test
|
|
||||||
|
|
||||||
# 添加未统计数据
|
|
||||||
python3 main.py --unaccounted 118 --month 2025-12
|
|
||||||
|
|
||||||
# 去除未统计数据
|
|
||||||
python3 main.py --remove-unaccounted --month 2025-12
|
|
||||||
|
|
||||||
# 显示帮助
|
|
||||||
python3 main.py --help
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GUI 方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 src/gui.py
|
|
||||||
```
|
|
||||||
|
|
||||||
GUI 功能:
|
|
||||||
- 获取并处理数据
|
|
||||||
- 重置数据库(删除并重新获取)
|
|
||||||
- 生成日报
|
|
||||||
- 今日日报(自动获取前一天数据)
|
|
||||||
- 添加未统计数据
|
|
||||||
- 去除多余统计数据(对称功能)
|
|
||||||
- 月底/月初智能调整(自动弹出对话框)
|
|
||||||
- 数据库统计(显示当月每艘船的作业量)
|
|
||||||
- 日报内容可复制
|
|
||||||
- 自动刷新排班信息
|
|
||||||
|
|
||||||
#### 智能调整功能
|
|
||||||
- **月初1号**:自动询问是否添加上月数据
|
|
||||||
- **月底最后一天**:自动询问是否剔除12点后数据
|
|
||||||
- **其他日期**:保留手动调整入口
|
|
||||||
- **调整数据**:在日报中清晰显示,确保数据准确性
|
|
||||||
|
|
||||||
## 数据格式
|
|
||||||
|
|
||||||
### 日报表 (daily_handover_logs)
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | INTEGER | 主键 |
|
|
||||||
| date | TEXT | 日期 YYYY-MM-DD |
|
|
||||||
| shift | TEXT | 班次 (白班/夜班) |
|
|
||||||
| 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 | 创建时间 |
|
|
||||||
|
|
||||||
### 手动调整表 (manual_adjustments)
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | INTEGER | 主键 |
|
|
||||||
| date | TEXT | 调整适用的日期 YYYY-MM-DD |
|
|
||||||
| ship_name | TEXT | 船名 |
|
|
||||||
| teu | INTEGER | TEU数量 |
|
|
||||||
| twenty_feet | INTEGER | 20尺箱量 |
|
|
||||||
| forty_feet | INTEGER | 40尺箱量 |
|
|
||||||
| adjustment_type | TEXT | 调整类型 'add' 或 'exclude' |
|
|
||||||
| note | TEXT | 备注 |
|
|
||||||
| created_at | TEXT | 创建时间 |
|
|
||||||
|
|
||||||
## 特性说明
|
|
||||||
|
|
||||||
### 二次靠泊合并
|
|
||||||
|
|
||||||
解析时会自动合并同一天的二次靠泊记录:
|
|
||||||
|
|
||||||
- 夜班 学友洋山: 273TEU
|
|
||||||
- 夜班 学友洋山(二次靠泊): 14TEU
|
|
||||||
- 合并后: 夜班 学友洋山: 287TEU
|
|
||||||
|
|
||||||
### 未统计数据
|
|
||||||
|
|
||||||
可以在数据库统计中查看当月每艘船的作业量总计,便于跟踪船舶运营情况。
|
|
||||||
|
|
||||||
### 去除多余统计数据
|
|
||||||
|
|
||||||
针对月底夜班箱量有时会被算入下个月的情况,系统提供了对称的"去除多余统计数据"功能:
|
|
||||||
- **添加未统计数据**: 用于补全缺失的箱量
|
|
||||||
- **去除未统计数据**: 用于删除多余统计的箱量
|
|
||||||
|
|
||||||
这两个功能配合使用,可以精确调整月度统计数据。
|
|
||||||
|
|
||||||
### 月底/月初数据调整功能
|
|
||||||
|
|
||||||
系统支持智能化的月底/月初数据调整:
|
|
||||||
|
|
||||||
#### 1. 月底最后一天(自动剔除12点后数据)
|
|
||||||
- 当获取数据的日期为月份最后一天时,GUI会自动询问是否需要剔除12点后的数据
|
|
||||||
- 用户可以输入需要剔除的船名、TEU以及具体尺寸(20尺/40尺)
|
|
||||||
- 剔除后的数据会从当月统计中移除,计入下月统计
|
|
||||||
|
|
||||||
#### 2. 月初1号(自动添加上月数据)
|
|
||||||
- 当获取数据的日期为月份1号时,GUI会自动询问是否需要添加上月的作业数据
|
|
||||||
- 用户可以输入需要添加的船名、TEU以及具体尺寸(20尺/40尺)
|
|
||||||
- 添加的数据会计入上月统计,确保月度数据的准确性
|
|
||||||
|
|
||||||
#### 3. 其他日期(手动调整入口)
|
|
||||||
- 非月初和月底的日期,默认不弹出调整对话框
|
|
||||||
- 但GUI侧边栏保留了手动添加/剔除TEU的功能入口
|
|
||||||
- 用户可以随时手动调整任何日期的数据
|
|
||||||
|
|
||||||
#### 4. 调整数据存储
|
|
||||||
所有手动调整数据存储在 `manual_adjustments` 表中:
|
|
||||||
- `date`: 调整适用的日期
|
|
||||||
- `ship_name`: 船名
|
|
||||||
- `teu`: TEU数量
|
|
||||||
- `twenty_feet`: 20尺箱量
|
|
||||||
- `forty_feet`: 40尺箱量
|
|
||||||
- `adjustment_type`: 'add' 或 'exclude'
|
|
||||||
- `note`: 备注信息
|
|
||||||
|
|
||||||
#### 5. 日报显示
|
|
||||||
调整数据会在日报中清晰显示:
|
|
||||||
- 每艘船下方显示具体的添加/剔除记录
|
|
||||||
- 日报末尾显示调整汇总信息
|
|
||||||
- 净调整量计算,确保数据准确性
|
|
||||||
|
|
||||||
## 示例输出
|
|
||||||
|
|
||||||
```
|
|
||||||
日期: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 客户端,支持自动获取和刷新tenant_access_token
|
|
||||||
- **`parser.py`** - 排班数据解析器
|
|
||||||
- **`manager.py`** - 飞书排班管理器,缓存和刷新排班信息
|
|
||||||
|
|
||||||
#### Token 自动获取机制
|
|
||||||
飞书模块现在支持两种认证方式:
|
|
||||||
1. **推荐方式**:使用应用凭证(FEISHU_APP_ID + FEISHU_APP_SECRET)
|
|
||||||
- 系统会自动调用飞书API获取tenant_access_token
|
|
||||||
- token有效期2小时,系统会在过期前30分钟自动刷新
|
|
||||||
- 无需手动管理token过期问题
|
|
||||||
|
|
||||||
2. **备选方式**:使用手动配置的FEISHU_TOKEN
|
|
||||||
- 兼容旧配置方式
|
|
||||||
- token过期后需要手动更新
|
|
||||||
- 不推荐长期使用
|
|
||||||
|
|
||||||
#### 如何获取应用凭证
|
|
||||||
1. 登录飞书开放平台:https://open.feishu.cn/
|
|
||||||
2. 创建自建应用
|
|
||||||
3. 在"凭证与基础信息"中获取App ID和App Secret
|
|
||||||
4. 为应用添加"获取tenant_access_token"权限
|
|
||||||
5. 将应用发布到企业(仅自建应用需要)
|
|
||||||
|
|
||||||
### 数据库模块 (`src/database/`)
|
|
||||||
- **`base.py`** - 数据库基类,提供统一的连接管理
|
|
||||||
- **`daily_logs.py`** - 每日交接班日志数据库
|
|
||||||
- **`schedules.py`** - 排班数据库
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Python 3.7+
|
- Python 3.7+
|
||||||
- SQLite3
|
- SQLite3
|
||||||
- Requests (HTTP 客户端)
|
- Requests (HTTP 客户端)
|
||||||
- BeautifulSoup4 (HTML 解析)
|
- BeautifulSoup4 (HTML 解析)
|
||||||
- tkinter (GUI,可选)
|
- tkinter (GUI)
|
||||||
- 类型提示 (Python 3.5+)
|
- 类型提示 (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
|
1. **连接失败**:检查 `.env` 文件中的 API 令牌和 URL
|
||||||
2. **数据库错误**: 确保 `data/` 目录存在且有写入权限
|
2. **数据库错误**:确保 `data/` 目录存在且有写入权限
|
||||||
3. **解析错误**: 检查 Confluence 页面结构是否发生变化
|
3. **解析错误**:检查 Confluence 页面结构是否发生变化
|
||||||
4. **飞书数据获取失败**:
|
4. **飞书数据获取失败**:
|
||||||
- 验证飞书表格权限
|
- 验证飞书表格权限
|
||||||
- 检查应用凭证是否正确(FEISHU_APP_ID + FEISHU_APP_SECRET)
|
- 检查应用凭证是否正确(FEISHU_APP_ID + FEISHU_APP_SECRET)
|
||||||
- 确认应用已发布到企业(自建应用需要)
|
- 确认应用已发布到企业(自建应用需要)
|
||||||
- 检查网络连接是否能访问飞书API
|
5. **月份页面ID问题**:
|
||||||
5. **飞书token获取失败**:
|
- 在GUI中配置正确的月份页面ID映射
|
||||||
- 确认应用有"获取tenant_access_token"权限
|
- 确保当前月份的页面ID已正确配置
|
||||||
- 检查app_id和app_secret是否正确
|
|
||||||
- 查看日志文件获取详细错误信息
|
|
||||||
|
|
||||||
### 日志级别
|
### 日志查看
|
||||||
|
|
||||||
- 默认日志级别: INFO
|
- 默认日志级别: INFO
|
||||||
- 调试日志级别: DEBUG (设置环境变量 `LOG_LEVEL=DEBUG`)
|
- 调试日志级别: DEBUG (设置环境变量 `LOG_LEVEL=DEBUG`)
|
||||||
- 日志文件: `logs/app.log`,自动轮转
|
- 日志文件: `logs/app.log`,自动轮转
|
||||||
|
|
||||||
## License
|
## 许可证
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Config:
|
|||||||
# GUI 配置
|
# GUI 配置
|
||||||
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
GUI_FONT_FAMILY = os.getenv('GUI_FONT_FAMILY', 'SimHei')
|
||||||
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
GUI_FONT_SIZE = int(os.getenv('GUI_FONT_SIZE', '10'))
|
||||||
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '900x700')
|
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'))
|
||||||
|
|||||||
@@ -110,7 +110,26 @@ class HandoverLogParser:
|
|||||||
try:
|
try:
|
||||||
logs: List[ShipLog] = []
|
logs: List[ShipLog] = []
|
||||||
|
|
||||||
# 预处理:移除单行分隔符(前后都是空行的分隔符)
|
# 预处理:修复日期格式和特殊字符
|
||||||
|
# 1. 修复日期格式:将 "2026.1.1" 转换为 "2026.01.01"
|
||||||
|
def fix_date_format(match):
|
||||||
|
date_str = match.group(1)
|
||||||
|
parts = date_str.split('.')
|
||||||
|
if len(parts) == 3:
|
||||||
|
year, month, day = parts
|
||||||
|
# 补零
|
||||||
|
month = month.zfill(2)
|
||||||
|
day = day.zfill(2)
|
||||||
|
return f"日期:{year}.{month}.{day}"
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# 修复日期格式
|
||||||
|
text = re.sub(r'日期:(\d{4}\.\d{1,2}\.\d{1,2})', fix_date_format, text)
|
||||||
|
|
||||||
|
# 2. 修复特殊空格字符(\xa0 转换为普通空格)
|
||||||
|
text = text.replace('\xa0', ' ')
|
||||||
|
|
||||||
|
# 3. 移除单行分隔符(前后都是空行的分隔符)
|
||||||
# 保留真正的内容分隔符(前后有内容的)
|
# 保留真正的内容分隔符(前后有内容的)
|
||||||
lines = text.split('\n')
|
lines = text.split('\n')
|
||||||
processed_lines: List[str] = []
|
processed_lines: List[str] = []
|
||||||
@@ -136,7 +155,7 @@ class HandoverLogParser:
|
|||||||
if not block.strip():
|
if not block.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查块中是否包含日期
|
# 检查块中是否包含日期(使用改进后的正则表达式)
|
||||||
date_match = re.search(r'日期:(\d{4}\.\d{2}\.\d{2})', block)
|
date_match = re.search(r'日期:(\d{4}\.\d{2}\.\d{2})', block)
|
||||||
if date_match:
|
if date_match:
|
||||||
current_date = self.parse_date(date_match.group(1))
|
current_date = self.parse_date(date_match.group(1))
|
||||||
|
|||||||
@@ -119,6 +119,21 @@ class DailyLogsDatabase(DatabaseBase):
|
|||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_date ON manual_adjustments(date)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_date ON manual_adjustments(date)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_type ON manual_adjustments(adjustment_type)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_type ON manual_adjustments(adjustment_type)')
|
||||||
|
|
||||||
|
# 创建Confluence页面ID映射表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS confluence_pages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
month_key TEXT NOT NULL UNIQUE, -- 月份键,格式:'2025-12', '2026-01'
|
||||||
|
page_id TEXT NOT NULL, -- Confluence页面ID
|
||||||
|
page_title TEXT, -- 页面标题(可选)
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 创建索引
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_confluence_month ON confluence_pages(month_key)')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.debug("数据库表结构初始化完成")
|
logger.debug("数据库表结构初始化完成")
|
||||||
|
|
||||||
@@ -582,6 +597,124 @@ class DailyLogsDatabase(DatabaseBase):
|
|||||||
'total_adjustment_teu': 0
|
'total_adjustment_teu': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def insert_confluence_page(self, month_key: str, page_id: str, page_title: str = '') -> bool:
|
||||||
|
"""
|
||||||
|
插入或更新Confluence页面ID映射
|
||||||
|
|
||||||
|
参数:
|
||||||
|
month_key: 月份键,格式:'2025-12', '2026-01'
|
||||||
|
page_id: Confluence页面ID
|
||||||
|
page_title: 页面标题(可选)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = '''
|
||||||
|
INSERT OR REPLACE INTO confluence_pages
|
||||||
|
(month_key, page_id, page_title, updated_at)
|
||||||
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
'''
|
||||||
|
params = (month_key, page_id, page_title)
|
||||||
|
self.execute_update(query, params)
|
||||||
|
logger.info(f"插入Confluence页面映射: {month_key} -> {page_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"插入Confluence页面映射失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_confluence_page(self, month_key: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取指定月份的Confluence页面ID映射
|
||||||
|
|
||||||
|
参数:
|
||||||
|
month_key: 月份键,格式:'2025-12', '2026-01'
|
||||||
|
|
||||||
|
返回:
|
||||||
|
页面映射字典,如果不存在则返回None
|
||||||
|
"""
|
||||||
|
query = 'SELECT * FROM confluence_pages WHERE month_key = ?'
|
||||||
|
result = self.execute_query(query, (month_key,))
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
def get_all_confluence_pages(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取所有Confluence页面ID映射
|
||||||
|
|
||||||
|
返回:
|
||||||
|
页面映射列表
|
||||||
|
"""
|
||||||
|
query = 'SELECT * FROM confluence_pages ORDER BY month_key DESC'
|
||||||
|
return self.execute_query(query)
|
||||||
|
|
||||||
|
def delete_confluence_page(self, month_key: str) -> bool:
|
||||||
|
"""
|
||||||
|
删除指定月份的Confluence页面ID映射
|
||||||
|
|
||||||
|
参数:
|
||||||
|
month_key: 月份键,格式:'2025-12', '2026-01'
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否成功删除
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = 'DELETE FROM confluence_pages WHERE month_key = ?'
|
||||||
|
result = self.execute_update(query, (month_key,))
|
||||||
|
if result > 0:
|
||||||
|
logger.info(f"删除Confluence页面映射: {month_key}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到Confluence页面映射: {month_key}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除Confluence页面映射失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_confluence_page_for_date(self, date: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
根据日期获取对应的Confluence页面ID
|
||||||
|
|
||||||
|
参数:
|
||||||
|
date: 日期字符串,格式:'2025-12-31'
|
||||||
|
|
||||||
|
返回:
|
||||||
|
Confluence页面ID,如果不存在则返回None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从日期中提取年月
|
||||||
|
year_month = date[:7] # '2025-12-31' -> '2025-12'
|
||||||
|
|
||||||
|
# 查询数据库
|
||||||
|
page_info = self.get_confluence_page(year_month)
|
||||||
|
if page_info:
|
||||||
|
return page_info['page_id']
|
||||||
|
|
||||||
|
# 如果没有找到,尝试从环境变量中获取
|
||||||
|
import os
|
||||||
|
from src.config import config
|
||||||
|
|
||||||
|
# 检查环境变量中的配置
|
||||||
|
env_key = f"CONFLUENCE_PAGE_{year_month.replace('-', '_')}"
|
||||||
|
page_id = os.getenv(env_key)
|
||||||
|
if page_id:
|
||||||
|
# 保存到数据库以便下次使用
|
||||||
|
self.insert_confluence_page(year_month, page_id, f"从环境变量获取: {env_key}")
|
||||||
|
return page_id
|
||||||
|
|
||||||
|
# 使用默认配置
|
||||||
|
default_page_id = config.CONFLUENCE_CONTENT_ID
|
||||||
|
if default_page_id:
|
||||||
|
logger.warning(f"未找到 {year_month} 的Confluence页面映射,使用默认页面ID: {default_page_id}")
|
||||||
|
return default_page_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取Confluence页面ID失败: {date}, 错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 测试代码
|
# 测试代码
|
||||||
|
|||||||
340
src/gui.py
340
src/gui.py
@@ -174,6 +174,20 @@ class OrbitInGUI:
|
|||||||
)
|
)
|
||||||
btn_clear.pack(pady=5)
|
btn_clear.pack(pady=5)
|
||||||
|
|
||||||
|
# 分隔线
|
||||||
|
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# Confluence页面ID管理
|
||||||
|
ttk.Label(left_frame, text="Confluence页面ID:").pack(anchor=tk.W, pady=(10, 5))
|
||||||
|
|
||||||
|
btn_confluence_pages = ttk.Button(
|
||||||
|
left_frame,
|
||||||
|
text="管理页面ID映射",
|
||||||
|
command=self.manage_confluence_pages,
|
||||||
|
width=20
|
||||||
|
)
|
||||||
|
btn_confluence_pages.pack(pady=5)
|
||||||
|
|
||||||
# === 右侧主区域 ===
|
# === 右侧主区域 ===
|
||||||
|
|
||||||
# 状态标签
|
# 状态标签
|
||||||
@@ -181,6 +195,11 @@ class OrbitInGUI:
|
|||||||
status_label = ttk.Label(right_frame, textvariable=self.status_var)
|
status_label = ttk.Label(right_frame, textvariable=self.status_var)
|
||||||
status_label.pack(anchor=tk.W)
|
status_label.pack(anchor=tk.W)
|
||||||
|
|
||||||
|
# Confluence页面ID显示
|
||||||
|
self.confluence_id_var = tk.StringVar(value="Confluence页面ID: 未获取")
|
||||||
|
confluence_id_label = ttk.Label(right_frame, textvariable=self.confluence_id_var, font=('', 9))
|
||||||
|
confluence_id_label.pack(anchor=tk.W, pady=(5, 0))
|
||||||
|
|
||||||
# 日报完整内容(可复制)
|
# 日报完整内容(可复制)
|
||||||
ttk.Label(right_frame, text="日报内容 (可复制):").pack(anchor=tk.W, pady=(5, 0))
|
ttk.Label(right_frame, text="日报内容 (可复制):").pack(anchor=tk.W, pady=(5, 0))
|
||||||
|
|
||||||
@@ -274,16 +293,39 @@ class OrbitInGUI:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查配置
|
# 检查配置
|
||||||
if not config.CONFLUENCE_BASE_URL or not config.CONFLUENCE_TOKEN or not config.CONFLUENCE_CONTENT_ID:
|
if not config.CONFLUENCE_BASE_URL or not config.CONFLUENCE_TOKEN:
|
||||||
self.log_message("错误: 未配置 Confluence 信息,请检查 .env 文件", is_error=True)
|
self.log_message("错误: 未配置 Confluence 信息,请检查 .env 文件", is_error=True)
|
||||||
self.logger.error("Confluence 配置不完整")
|
self.logger.error("Confluence 配置不完整")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 获取当前月份对应的页面ID
|
||||||
|
# 程序是在第二天打开获取昨天的数据,所以使用昨天的日期
|
||||||
|
yesterday = datetime.now() - timedelta(days=1)
|
||||||
|
yesterday_str = yesterday.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
page_id = db.get_confluence_page_for_date(yesterday_str)
|
||||||
|
|
||||||
|
if not page_id:
|
||||||
|
# 如果没有找到映射,使用默认配置
|
||||||
|
if not config.CONFLUENCE_CONTENT_ID:
|
||||||
|
self.log_message("错误: 未配置 Confluence 页面ID,请检查 .env 文件或配置页面ID映射", is_error=True)
|
||||||
|
self.logger.error("Confluence 页面ID未配置")
|
||||||
|
return
|
||||||
|
page_id = config.CONFLUENCE_CONTENT_ID
|
||||||
|
self.log_message(f"警告: 未找到 {yesterday_str} 的页面ID映射,使用默认页面ID: {page_id}")
|
||||||
|
self.logger.warning(f"未找到 {yesterday_str} 的页面ID映射,使用默认页面ID: {page_id}")
|
||||||
|
self.confluence_id_var.set(f"Confluence页面ID: {page_id} (默认)")
|
||||||
|
else:
|
||||||
|
self.log_message(f"使用页面ID映射: {yesterday_str} -> {page_id}")
|
||||||
|
self.logger.info(f"使用页面ID映射: {yesterday_str} -> {page_id}")
|
||||||
|
self.confluence_id_var.set(f"Confluence页面ID: {page_id} ({yesterday_str})")
|
||||||
|
|
||||||
# 获取 HTML
|
# 获取 HTML
|
||||||
self.log_message("正在从 Confluence 获取 HTML...")
|
self.log_message("正在从 Confluence 获取 HTML...")
|
||||||
self.logger.info("正在从 Confluence 获取 HTML...")
|
self.logger.info("正在从 Confluence 获取 HTML...")
|
||||||
client = ConfluenceClient(config.CONFLUENCE_BASE_URL, config.CONFLUENCE_TOKEN)
|
client = ConfluenceClient(config.CONFLUENCE_BASE_URL, config.CONFLUENCE_TOKEN)
|
||||||
html = client.get_html(config.CONFLUENCE_CONTENT_ID)
|
html = client.get_html(page_id)
|
||||||
|
|
||||||
if not html:
|
if not html:
|
||||||
self.log_message("错误: 未获取到 HTML 内容", is_error=True)
|
self.log_message("错误: 未获取到 HTML 内容", is_error=True)
|
||||||
@@ -761,13 +803,32 @@ class OrbitInGUI:
|
|||||||
self.log_message("正在尝试获取最新作业数据...")
|
self.log_message("正在尝试获取最新作业数据...")
|
||||||
self.logger.info("正在尝试获取最新作业数据...")
|
self.logger.info("正在尝试获取最新作业数据...")
|
||||||
|
|
||||||
if config.CONFLUENCE_BASE_URL and config.CONFLUENCE_TOKEN and config.CONFLUENCE_CONTENT_ID:
|
if config.CONFLUENCE_BASE_URL and config.CONFLUENCE_TOKEN:
|
||||||
try:
|
try:
|
||||||
|
# 获取当前月份对应的页面ID
|
||||||
|
# 程序是在第二天打开获取昨天的数据,所以使用昨天的日期
|
||||||
|
yesterday = datetime.now() - timedelta(days=1)
|
||||||
|
yesterday_str = yesterday.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
page_id = db.get_confluence_page_for_date(yesterday_str)
|
||||||
|
|
||||||
|
if not page_id:
|
||||||
|
# 如果没有找到映射,使用默认配置
|
||||||
|
if not config.CONFLUENCE_CONTENT_ID:
|
||||||
|
self.log_message("Confluence 页面ID未配置,跳过数据获取")
|
||||||
|
self.logger.warning("Confluence 页面ID未配置,跳过数据获取")
|
||||||
|
return
|
||||||
|
page_id = config.CONFLUENCE_CONTENT_ID
|
||||||
|
self.log_message(f"警告: 未找到 {yesterday_str} 的页面ID映射,使用默认页面ID: {page_id}")
|
||||||
|
self.logger.warning(f"未找到 {yesterday_str} 的页面ID映射,使用默认页面ID: {page_id}")
|
||||||
|
self.confluence_id_var.set(f"Confluence页面ID: {page_id} (默认)")
|
||||||
|
|
||||||
# 获取 HTML
|
# 获取 HTML
|
||||||
self.log_message("正在从 Confluence 获取 HTML...")
|
self.log_message("正在从 Confluence 获取 HTML...")
|
||||||
self.logger.info("正在从 Confluence 获取 HTML...")
|
self.logger.info("正在从 Confluence 获取 HTML...")
|
||||||
client = ConfluenceClient(config.CONFLUENCE_BASE_URL, config.CONFLUENCE_TOKEN)
|
client = ConfluenceClient(config.CONFLUENCE_BASE_URL, config.CONFLUENCE_TOKEN)
|
||||||
html = client.get_html(config.CONFLUENCE_CONTENT_ID)
|
html = client.get_html(page_id)
|
||||||
|
|
||||||
if html:
|
if html:
|
||||||
self.log_message(f"获取成功,共 {len(html)} 字符")
|
self.log_message(f"获取成功,共 {len(html)} 字符")
|
||||||
@@ -877,6 +938,11 @@ class OrbitInGUI:
|
|||||||
self.logger.error(f"未知错误: {e}", exc_info=True)
|
self.logger.error(f"未知错误: {e}", exc_info=True)
|
||||||
self.set_status("错误")
|
self.set_status("错误")
|
||||||
|
|
||||||
|
def manage_confluence_pages(self):
|
||||||
|
"""管理Confluence页面ID映射"""
|
||||||
|
dialog = ConfluencePagesDialog(self.root, self)
|
||||||
|
self.root.wait_window(dialog)
|
||||||
|
|
||||||
|
|
||||||
class AddDataDialog(tk.Toplevel):
|
class AddDataDialog(tk.Toplevel):
|
||||||
"""添加数据对话框"""
|
"""添加数据对话框"""
|
||||||
@@ -1137,6 +1203,272 @@ class ExcludeDataDialog(tk.Toplevel):
|
|||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfluencePagesDialog(tk.Toplevel):
|
||||||
|
"""Confluence页面ID映射管理对话框"""
|
||||||
|
|
||||||
|
def __init__(self, parent, gui):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Confluence页面ID映射管理")
|
||||||
|
self.gui = gui
|
||||||
|
|
||||||
|
# 设置对话框大小和位置
|
||||||
|
self.geometry("600x500")
|
||||||
|
self.resizable(True, True)
|
||||||
|
|
||||||
|
# 使对话框模态
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
|
||||||
|
# 创建主框架
|
||||||
|
main_frame = ttk.Frame(self, padding="10")
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
ttk.Label(main_frame, text="Confluence页面ID映射管理", font=('', 12, 'bold')).pack(anchor=tk.W, pady=(0, 10))
|
||||||
|
|
||||||
|
# 说明文本
|
||||||
|
ttk.Label(main_frame, text="每月Confluence页面ID不同,请在此配置各月份的页面ID映射。",
|
||||||
|
wraplength=550).pack(anchor=tk.W, pady=(0, 10))
|
||||||
|
|
||||||
|
# 列表框架
|
||||||
|
list_frame = ttk.LabelFrame(main_frame, text="现有页面ID映射", padding="10")
|
||||||
|
list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
|
# 创建Treeview
|
||||||
|
columns = ('month_key', 'page_id', 'page_title', 'updated_at')
|
||||||
|
self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=8)
|
||||||
|
|
||||||
|
# 设置列标题
|
||||||
|
self.tree.heading('month_key', text='月份')
|
||||||
|
self.tree.heading('page_id', text='页面ID')
|
||||||
|
self.tree.heading('page_title', text='页面标题')
|
||||||
|
self.tree.heading('updated_at', text='更新时间')
|
||||||
|
|
||||||
|
# 设置列宽度
|
||||||
|
self.tree.column('month_key', width=80)
|
||||||
|
self.tree.column('page_id', width=120)
|
||||||
|
self.tree.column('page_title', width=200)
|
||||||
|
self.tree.column('updated_at', width=120)
|
||||||
|
|
||||||
|
# 添加滚动条
|
||||||
|
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
|
self.tree.configure(yscroll=scrollbar.set)
|
||||||
|
|
||||||
|
# 布局
|
||||||
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
# 按钮框架
|
||||||
|
button_frame = ttk.Frame(main_frame)
|
||||||
|
button_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Button(button_frame, text="添加新映射", command=self.add_mapping).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="编辑选中", command=self.edit_mapping).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="删除选中", command=self.delete_mapping).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="刷新列表", command=self.refresh_list).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 关闭按钮
|
||||||
|
ttk.Button(main_frame, text="关闭", command=self.destroy).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
|
# 绑定双击事件
|
||||||
|
self.tree.bind('<Double-Button-1>', lambda e: self.edit_mapping())
|
||||||
|
|
||||||
|
# 加载数据
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def refresh_list(self):
|
||||||
|
"""刷新列表"""
|
||||||
|
# 清空现有数据
|
||||||
|
for item in self.tree.get_children():
|
||||||
|
self.tree.delete(item)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
pages = db.get_all_confluence_pages()
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
self.tree.insert('', tk.END, values=(
|
||||||
|
page['month_key'],
|
||||||
|
page['page_id'],
|
||||||
|
page['page_title'] or '',
|
||||||
|
page['updated_at']
|
||||||
|
))
|
||||||
|
|
||||||
|
self.gui.log_message(f"加载了 {len(pages)} 个页面ID映射")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.gui.log_message(f"加载页面ID映射失败: {e}", is_error=True)
|
||||||
|
|
||||||
|
def add_mapping(self):
|
||||||
|
"""添加新映射"""
|
||||||
|
dialog = ConfluencePageEditDialog(self, self.gui, None)
|
||||||
|
self.wait_window(dialog)
|
||||||
|
if dialog.result:
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def edit_mapping(self):
|
||||||
|
"""编辑选中映射"""
|
||||||
|
selection = self.tree.selection()
|
||||||
|
if not selection:
|
||||||
|
messagebox.showwarning("警告", "请先选择一个映射")
|
||||||
|
return
|
||||||
|
|
||||||
|
item = self.tree.item(selection[0])
|
||||||
|
month_key = item['values'][0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
page_info = db.get_confluence_page(month_key)
|
||||||
|
if page_info:
|
||||||
|
dialog = ConfluencePageEditDialog(self, self.gui, page_info)
|
||||||
|
self.wait_window(dialog)
|
||||||
|
if dialog.result:
|
||||||
|
self.refresh_list()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("错误", f"未找到月份 {month_key} 的映射")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("错误", f"获取映射信息失败: {e}")
|
||||||
|
|
||||||
|
def delete_mapping(self):
|
||||||
|
"""删除选中映射"""
|
||||||
|
selection = self.tree.selection()
|
||||||
|
if not selection:
|
||||||
|
messagebox.showwarning("警告", "请先选择一个映射")
|
||||||
|
return
|
||||||
|
|
||||||
|
item = self.tree.item(selection[0])
|
||||||
|
month_key = item['values'][0]
|
||||||
|
page_id = item['values'][1]
|
||||||
|
|
||||||
|
if not messagebox.askyesno("确认删除", f"确定要删除月份 {month_key} 的页面ID映射吗?\n页面ID: {page_id}"):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
success = db.delete_confluence_page(month_key)
|
||||||
|
if success:
|
||||||
|
self.gui.log_message(f"已删除页面ID映射: {month_key} -> {page_id}")
|
||||||
|
self.refresh_list()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("错误", "删除失败")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("错误", f"删除失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfluencePageEditDialog(tk.Toplevel):
|
||||||
|
"""Confluence页面ID映射编辑对话框"""
|
||||||
|
|
||||||
|
def __init__(self, parent, gui, page_info):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("编辑Confluence页面ID映射" if page_info else "添加Confluence页面ID映射")
|
||||||
|
self.gui = gui
|
||||||
|
self.page_info = page_info
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
# 设置对话框大小和位置
|
||||||
|
self.geometry("400x250")
|
||||||
|
self.resizable(False, False)
|
||||||
|
|
||||||
|
# 使对话框模态
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
|
||||||
|
# 创建输入字段
|
||||||
|
frame = ttk.Frame(self, padding="20")
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 月份键
|
||||||
|
ttk.Label(frame, text="月份键 (YYYY-MM):").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||||
|
self.month_key_var = tk.StringVar(value=page_info['month_key'] if page_info else '')
|
||||||
|
month_key_entry = ttk.Entry(frame, textvariable=self.month_key_var, width=15)
|
||||||
|
month_key_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||||
|
ttk.Label(frame, text="例如: 2025-12, 2026-01").grid(row=0, column=2, sticky=tk.W, pady=5)
|
||||||
|
|
||||||
|
# 页面ID
|
||||||
|
ttk.Label(frame, text="页面ID:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||||
|
self.page_id_var = tk.StringVar(value=page_info['page_id'] if page_info else '')
|
||||||
|
page_id_entry = ttk.Entry(frame, textvariable=self.page_id_var, width=20)
|
||||||
|
page_id_entry.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||||
|
|
||||||
|
# 页面标题
|
||||||
|
ttk.Label(frame, text="页面标题 (可选):").grid(row=2, column=0, sticky=tk.W, pady=5)
|
||||||
|
self.page_title_var = tk.StringVar(value=page_info['page_title'] if page_info else '')
|
||||||
|
page_title_entry = ttk.Entry(frame, textvariable=self.page_title_var, width=30)
|
||||||
|
page_title_entry.grid(row=2, column=1, sticky=tk.W, pady=5)
|
||||||
|
|
||||||
|
# 按钮
|
||||||
|
button_frame = ttk.Frame(frame)
|
||||||
|
button_frame.grid(row=3, column=0, columnspan=2, pady=20)
|
||||||
|
|
||||||
|
ttk.Button(button_frame, text="确定", command=self.on_ok).pack(side=tk.LEFT, padx=10)
|
||||||
|
ttk.Button(button_frame, text="取消", command=self.on_cancel).pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
# 绑定回车键
|
||||||
|
self.bind('<Return>', lambda e: self.on_ok())
|
||||||
|
self.bind('<Escape>', lambda e: self.on_cancel())
|
||||||
|
|
||||||
|
# 焦点设置
|
||||||
|
if page_info:
|
||||||
|
page_id_entry.focus_set()
|
||||||
|
else:
|
||||||
|
month_key_entry.focus_set()
|
||||||
|
|
||||||
|
def on_ok(self):
|
||||||
|
"""确定按钮处理"""
|
||||||
|
try:
|
||||||
|
# 验证输入
|
||||||
|
month_key = self.month_key_var.get().strip()
|
||||||
|
page_id = self.page_id_var.get().strip()
|
||||||
|
page_title = self.page_title_var.get().strip()
|
||||||
|
|
||||||
|
if not month_key:
|
||||||
|
messagebox.showerror("错误", "请输入月份键")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 验证月份键格式
|
||||||
|
try:
|
||||||
|
year, month = month_key.split('-')
|
||||||
|
if len(year) != 4 or len(month) != 2:
|
||||||
|
raise ValueError
|
||||||
|
int(year)
|
||||||
|
int(month)
|
||||||
|
if int(month) < 1 or int(month) > 12:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
messagebox.showerror("错误", "月份键格式无效,请使用 YYYY-MM 格式")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not page_id:
|
||||||
|
messagebox.showerror("错误", "请输入页面ID")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 验证页面ID是否为数字
|
||||||
|
try:
|
||||||
|
int(page_id)
|
||||||
|
except ValueError:
|
||||||
|
if not messagebox.askyesno("确认", f"页面ID '{page_id}' 不是纯数字,确定要继续吗?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
db = DailyLogsDatabase()
|
||||||
|
success = db.insert_confluence_page(month_key, page_id, page_title)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.gui.log_message(f"保存页面ID映射: {month_key} -> {page_id}")
|
||||||
|
self.result = True
|
||||||
|
self.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("错误", "保存失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("错误", f"保存失败: {e}")
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
"""取消按钮处理"""
|
||||||
|
self.result = None
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
Reference in New Issue
Block a user