diff --git a/.gitignore b/.gitignore index c984f45..7b26b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ debug/ # OS .DS_Store Thumbs.db +AGENTS.md # IDE .vscode/ diff --git a/README.md b/README.md index ddfa3d5..5718744 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/config.py b/src/config.py index 413ad9f..448ca4d 100644 --- a/src/config.py +++ b/src/config.py @@ -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')) diff --git a/src/confluence/log_parser.py b/src/confluence/log_parser.py index 6fed18e..ed249c2 100644 --- a/src/confluence/log_parser.py +++ b/src/confluence/log_parser.py @@ -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)) diff --git a/src/database/daily_logs.py b/src/database/daily_logs.py index eb0fa6d..355af25 100644 --- a/src/database/daily_logs.py +++ b/src/database/daily_logs.py @@ -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("数据库表结构初始化完成") @@ -581,6 +596,124 @@ class DailyLogsDatabase(DatabaseBase): 'adjustments': [], '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__': diff --git a/src/gui.py b/src/gui.py index a244bb5..f440a2e 100644 --- a/src/gui.py +++ b/src/gui.py @@ -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)} 字符") @@ -876,6 +937,11 @@ class OrbitInGUI: self.log_message(f"未知错误: {e}", is_error=True) 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('', 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('', lambda e: self.on_ok()) + self.bind('', 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()