feat: 新增月底/月初数据调整和Confluence月份页面映射功能

- 新增月底最后一天自动剔除12点后数据功能
- 实现月底剔除数据自动转移到次月1号
- 新增Confluence月份页面ID映射功能,解决每月页面ID变化问题
- 修复1月份页面解析问题,支持'2026.1.1'日期格式
- 优化GUI界面,增加页面ID配置管理
- 精简README文档,增加详细功能说明
- 修复月度统计计算包含调整数据的问题
This commit is contained in:
2026-01-02 01:29:03 +08:00
parent 1b688c1603
commit 53eef800b4
6 changed files with 634 additions and 332 deletions

1
.gitignore vendored
View File

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

467
README.md
View File

@@ -4,28 +4,144 @@
## 功能特性
- 从 Confluence 获取交接班日志 HTML
- 提取保留布局的文本内容
- SQLite3 数据库存储
- 生成日报和月度统计
- 支持未统计数据手动录入
- 支持去除多余统计数据(对称功能)
- 支持二次靠泊记录合并
- GUI 图形界面(可选)
- 飞书排班表集成(自动获取班次人员)
- **数据获取**从 Confluence API 获取交接班日志 HTML
- **文本提取**提取保留布局的文本内容,支持表格格式化
- **数据库存储**SQLite3 数据库存储,支持二次靠泊记录合并
- **报表生成**生成日报和月度统计,包含完成率计算
- **数据调整**:支持手动添加/剔除统计数据,月底数据自动转移
- **智能调整**月底最后一天自动询问剔除12点后数据月初自动添加上月数据
- **GUI界面**tkinter 图形界面,支持一键操作
- **飞书集成**自动获取排班人员信息支持应用凭证自动刷新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/
├── main.py # CLI 入口
├── README.md # 项目说明
├── AGENTS.md # AI助手开发文档
├── .env # 环境配置(敏感信息)
├── .env.example # 环境配置示例
├── layout_output.txt # 缓存的布局文本
├── debug/ # 调试输出目录
│ └── layout_output_*.txt # 带时间戳的调试文件
├── data/ # 数据目录
│ ├── daily_logs.db # SQLite3 数据库
│ └── schedule_cache.json # 排班数据缓存
@@ -46,342 +162,43 @@ OrbitIn/
│ ├── parser.py # HTML 内容解析器
│ ├── text.py # HTML 文本提取器
│ ├── log_parser.py # 日志解析器
── manager.py # 内容管理器
│ └── __init__.py # 模块导出
── manager.py # 内容管理器
└── feishu/ # 飞书 API 模块
├── client.py # 飞书 API 客户端
├── parser.py # 排班数据解析器
── manager.py # 飞书排班管理器
└── __init__.py # 模块导出
── manager.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+
- SQLite3
- Requests (HTTP 客户端)
- 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. **飞书数据获取失败**:
1. **连接失败**检查 `.env` 文件中的 API 令牌和 URL
2. **数据库错误**确保 `data/` 目录存在且有写入权限
3. **解析错误**检查 Confluence 页面结构是否发生变化
4. **飞书数据获取失败**
- 验证飞书表格权限
- 检查应用凭证是否正确FEISHU_APP_ID + FEISHU_APP_SECRET
- 确认应用已发布到企业(自建应用需要)
- 检查网络连接是否能访问飞书API
5. **飞书token获取失败**:
-认应用有"获取tenant_access_token"权限
- 检查app_id和app_secret是否正确
- 查看日志文件获取详细错误信息
5. **月份页面ID问题**
- 在GUI中配置正确的月份页面ID映射
-保当前月份的页面ID已正确配置
### 日志级别
### 日志查看
- 默认日志级别: INFO
- 调试日志级别: DEBUG (设置环境变量 `LOG_LEVEL=DEBUG`)
- 日志文件: `logs/app.log`,自动轮转
## License
## 许可证
MIT

View File

@@ -48,7 +48,7 @@ class Config:
# GUI 配置
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', '900x700')
GUI_WINDOW_SIZE = os.getenv('GUI_WINDOW_SIZE', '1600x900')
# 排班刷新配置
SCHEDULE_REFRESH_DAYS = int(os.getenv('SCHEDULE_REFRESH_DAYS', '30'))

View File

@@ -110,7 +110,26 @@ class HandoverLogParser:
try:
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')
processed_lines: List[str] = []
@@ -136,7 +155,7 @@ class HandoverLogParser:
if not block.strip():
continue
# 检查块中是否包含日期
# 检查块中是否包含日期(使用改进后的正则表达式)
date_match = re.search(r'日期:(\d{4}\.\d{2}\.\d{2})', block)
if date_match:
current_date = self.parse_date(date_match.group(1))

View File

@@ -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_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()
logger.debug("数据库表结构初始化完成")
@@ -582,6 +597,124 @@ class DailyLogsDatabase(DatabaseBase):
'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__':
# 测试代码

View File

@@ -174,6 +174,20 @@ class OrbitInGUI:
)
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.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))
@@ -274,16 +293,39 @@ class OrbitInGUI:
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.logger.error("Confluence 配置不完整")
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
self.log_message("正在从 Confluence 获取 HTML...")
self.logger.info("正在从 Confluence 获取 HTML...")
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:
self.log_message("错误: 未获取到 HTML 内容", is_error=True)
@@ -761,13 +803,32 @@ class OrbitInGUI:
self.log_message("正在尝试获取最新作业数据...")
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:
# 获取当前月份对应的页面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
self.log_message("正在从 Confluence 获取 HTML...")
self.logger.info("正在从 Confluence 获取 HTML...")
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:
self.log_message(f"获取成功,共 {len(html)} 字符")
@@ -877,6 +938,11 @@ class OrbitInGUI:
self.logger.error(f"未知错误: {e}", exc_info=True)
self.set_status("错误")
def manage_confluence_pages(self):
"""管理Confluence页面ID映射"""
dialog = ConfluencePagesDialog(self.root, self)
self.root.wait_window(dialog)
class AddDataDialog(tk.Toplevel):
"""添加数据对话框"""
@@ -1137,6 +1203,272 @@ class ExcludeDataDialog(tk.Toplevel):
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():
"""主函数"""
root = tk.Tk()