Files
Orbitin/src/schedule_database.py
qichi.liang dc2a55bbf4 feat: 添加飞书表格模块支持排班人员信息获取
- 新增 src/feishu_v2.py: 飞书表格API客户端,支持数据库存储和2026年全年排班表
- 新增 src/schedule_database.py: 排班信息数据库模块,用于缓存排班数据
- 新增 docs/feishu_data_flow.md: 飞书数据流文档
- 新增 plans/feishu_scheduling_plan.md: 飞书排班表模块设计文档
- 更新 src/report.py: 使用新的飞书模块获取排班人员信息
- 更新 src/gui.py: 启动时自动获取新数据,添加auto_fetch_data方法
- 更新 .env.example: 添加飞书配置示例
- 更新 AGENTS.md: 更新项目文档
- 更新 main.py: 集成飞书模块

功能特性:
1. 支持从飞书表格获取排班人员信息
2. 支持2025年月度表格和2026年全年排班表
3. 使用SQLite数据库缓存,减少API调用
4. 自动检测表格更新
5. GUI启动时自动获取最新数据
6. 日报中正确显示次日班次人员信息
2025-12-31 00:03:34 +08:00

323 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
排班人员数据库模块
"""
import sqlite3
import os
import json
from datetime import datetime
from typing import List, Dict, Optional, Tuple
import hashlib
class ScheduleDatabase:
"""排班人员数据库"""
def __init__(self, db_path: str = 'data/daily_logs.db'):
"""
初始化数据库
参数:
db_path: 数据库文件路径
"""
self.db_path = db_path
self._ensure_directory()
self.conn = self._connect()
self._init_schema()
def _ensure_directory(self):
"""确保数据目录存在"""
data_dir = os.path.dirname(self.db_path)
if data_dir and not os.path.exists(data_dir):
os.makedirs(data_dir)
def _connect(self) -> sqlite3.Connection:
"""连接数据库"""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init_schema(self):
"""初始化表结构"""
cursor = self.conn.cursor()
# 创建排班人员表
cursor.execute('''
CREATE TABLE IF NOT EXISTS schedule_personnel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
day_shift TEXT,
night_shift TEXT,
day_shift_list TEXT, -- JSON数组
night_shift_list TEXT, -- JSON数组
sheet_id TEXT,
sheet_title TEXT,
data_hash TEXT, -- 数据哈希,用于检测更新
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date)
)
''')
# 创建表格版本表(用于检测表格是否有更新)
cursor.execute('''
CREATE TABLE IF NOT EXISTS sheet_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sheet_id TEXT NOT NULL,
sheet_title TEXT NOT NULL,
revision INTEGER NOT NULL,
data_hash TEXT,
last_checked_at TEXT DEFAULT CURRENT_TIMESTAMP,
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_sheet ON schedule_personnel(sheet_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sheet_versions ON sheet_versions(sheet_id)')
self.conn.commit()
def _calculate_hash(self, data: Dict) -> str:
"""计算数据哈希值"""
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
return hashlib.md5(data_str.encode('utf-8')).hexdigest()
def check_sheet_update(self, sheet_id: str, sheet_title: str, revision: int, data: Dict) -> bool:
"""
检查表格是否有更新
参数:
sheet_id: 表格ID
sheet_title: 表格标题
revision: 表格版本号
data: 表格数据
返回:
True: 有更新,需要重新获取
False: 无更新,可以使用缓存
"""
cursor = self.conn.cursor()
# 查询当前版本
cursor.execute(
'SELECT revision, data_hash FROM sheet_versions WHERE sheet_id = ?',
(sheet_id,)
)
result = cursor.fetchone()
if not result:
# 第一次获取,记录版本
data_hash = self._calculate_hash(data)
cursor.execute('''
INSERT INTO sheet_versions (sheet_id, sheet_title, revision, data_hash, last_checked_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (sheet_id, sheet_title, revision, data_hash))
self.conn.commit()
return True
# 检查版本号或数据是否有变化
old_revision = result['revision']
old_hash = result['data_hash']
new_hash = self._calculate_hash(data)
if old_revision != revision or old_hash != new_hash:
# 有更新,更新版本信息
cursor.execute('''
UPDATE sheet_versions
SET revision = ?, data_hash = ?, last_checked_at = CURRENT_TIMESTAMP
WHERE sheet_id = ?
''', (revision, new_hash, sheet_id))
self.conn.commit()
return True
# 无更新,更新检查时间
cursor.execute('''
UPDATE sheet_versions
SET last_checked_at = CURRENT_TIMESTAMP
WHERE sheet_id = ?
''', (sheet_id,))
self.conn.commit()
return False
def save_schedule(self, date: str, schedule_data: Dict, sheet_id: str = None, sheet_title: str = None) -> bool:
"""
保存排班信息到数据库
参数:
date: 日期 (YYYY-MM-DD)
schedule_data: 排班数据
sheet_id: 表格ID
sheet_title: 表格标题
返回:
是否成功
"""
try:
cursor = self.conn.cursor()
# 准备数据
day_shift = schedule_data.get('day_shift', '')
night_shift = schedule_data.get('night_shift', '')
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)
data_hash = self._calculate_hash(schedule_data)
# 使用 INSERT OR REPLACE 来更新已存在的记录
cursor.execute('''
INSERT OR REPLACE INTO schedule_personnel
(date, day_shift, night_shift, day_shift_list, night_shift_list,
sheet_id, sheet_title, data_hash, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
date, day_shift, night_shift, day_shift_list, night_shift_list,
sheet_id, sheet_title, data_hash
))
self.conn.commit()
return True
except sqlite3.Error as e:
print(f"数据库错误: {e}")
return False
def get_schedule(self, date: str) -> Optional[Dict]:
"""
获取指定日期的排班信息
参数:
date: 日期 (YYYY-MM-DD)
返回:
排班信息字典未找到返回None
"""
cursor = self.conn.cursor()
cursor.execute(
'SELECT * FROM schedule_personnel WHERE date = ?',
(date,)
)
result = cursor.fetchone()
if not result:
return None
# 解析JSON数组
day_shift_list = json.loads(result['day_shift_list']) if result['day_shift_list'] else []
night_shift_list = json.loads(result['night_shift_list']) if result['night_shift_list'] else []
return {
'date': result['date'],
'day_shift': result['day_shift'],
'night_shift': result['night_shift'],
'day_shift_list': day_shift_list,
'night_shift_list': night_shift_list,
'sheet_id': result['sheet_id'],
'sheet_title': result['sheet_title'],
'updated_at': result['updated_at']
}
def get_schedule_by_range(self, start_date: str, end_date: str) -> List[Dict]:
"""
获取日期范围内的排班信息
参数:
start_date: 开始日期 (YYYY-MM-DD)
end_date: 结束日期 (YYYY-MM-DD)
返回:
排班信息列表
"""
cursor = self.conn.cursor()
cursor.execute('''
SELECT * FROM schedule_personnel
WHERE date >= ? AND date <= ?
ORDER BY date
''', (start_date, end_date))
results = []
for row in cursor.fetchall():
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 []
results.append({
'date': row['date'],
'day_shift': row['day_shift'],
'night_shift': row['night_shift'],
'day_shift_list': day_shift_list,
'night_shift_list': night_shift_list,
'sheet_id': row['sheet_id'],
'sheet_title': row['sheet_title'],
'updated_at': row['updated_at']
})
return results
def delete_old_schedules(self, before_date: str) -> int:
"""
删除指定日期之前的排班记录
参数:
before_date: 日期 (YYYY-MM-DD)
返回:
删除的记录数
"""
cursor = self.conn.cursor()
cursor.execute(
'DELETE FROM schedule_personnel WHERE date < ?',
(before_date,)
)
deleted_count = cursor.rowcount
self.conn.commit()
return deleted_count
def get_stats(self) -> Dict:
"""获取统计信息"""
cursor = self.conn.cursor()
cursor.execute('SELECT COUNT(*) FROM schedule_personnel')
total = cursor.fetchone()[0]
cursor.execute('SELECT MIN(date), MAX(date) FROM schedule_personnel')
date_range = cursor.fetchone()
cursor.execute('SELECT COUNT(DISTINCT sheet_id) FROM schedule_personnel')
sheet_count = cursor.fetchone()[0]
return {
'total': total,
'date_range': {'start': date_range[0], 'end': date_range[1]},
'sheet_count': sheet_count
}
def close(self):
"""关闭连接"""
if self.conn:
self.conn.close()
if __name__ == '__main__':
# 测试代码
db = ScheduleDatabase()
# 测试保存
test_schedule = {
'day_shift': '张勤、杨俊豪',
'night_shift': '刘炜彬、梁启迟',
'day_shift_list': ['张勤', '杨俊豪'],
'night_shift_list': ['刘炜彬', '梁启迟']
}
success = db.save_schedule('2025-12-31', test_schedule, 'zcYLIk', '12月')
print(f"保存成功: {success}")
# 测试获取
schedule = db.get_schedule('2025-12-31')
print(f"获取结果: {schedule}")
# 测试统计
stats = db.get_stats()
print(f"统计: {stats}")
db.close()