mirror of
https://devops.liangqichi.top/qichi.liang/Orbitin.git
synced 2026-02-10 07:41:29 +08:00
- 新增 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. 日报中正确显示次日班次人员信息
323 lines
11 KiB
Python
323 lines
11 KiB
Python
#!/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() |