v2.0.0 — 日打卡彻底改为周打卡
This commit is contained in:
176
app.py
176
app.py
@@ -6,7 +6,7 @@ from datetime import timedelta
|
||||
from functools import wraps
|
||||
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from database import init_db, get_checkin, save_checkin, delete_checkin, get_all_checkins, get_wishes, save_wish, update_wish, delete_wish, reorder_wishes
|
||||
from database import init_db, get_checkin, save_checkin, delete_checkin, get_all_checkins, get_weeks_list, get_daily_backup, get_wishes, save_wish, update_wish, delete_wish, reorder_wishes
|
||||
|
||||
app = Flask(__name__)
|
||||
# 固定密钥确保 gunicorn 多 worker 下 session 可互通
|
||||
@@ -72,7 +72,7 @@ def logout():
|
||||
def compute_stats():
|
||||
"""计算统计数据,供 API 和模板共用"""
|
||||
rows = get_all_checkins()
|
||||
total_days = len(rows)
|
||||
total_weeks = len(rows)
|
||||
total_morning = 0
|
||||
total_evening = 0
|
||||
total_study = 0
|
||||
@@ -187,7 +187,7 @@ def compute_stats():
|
||||
pillar_breakdown[p]['study_items'].append(si)
|
||||
|
||||
return dict(
|
||||
total_days=total_days, total_morning=total_morning,
|
||||
total_weeks=total_weeks, total_morning=total_morning,
|
||||
total_evening=total_evening, total_study=total_study,
|
||||
calendar=calendar,
|
||||
morning_items=all_morning_items,
|
||||
@@ -214,10 +214,10 @@ def index():
|
||||
@app.route('/api/checkin', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_checkin():
|
||||
date = request.args.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400
|
||||
row = get_checkin(date)
|
||||
week = request.args.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
|
||||
row = get_checkin(week)
|
||||
return jsonify({'ok': True, 'data': row})
|
||||
|
||||
|
||||
@@ -225,21 +225,29 @@ def api_get_checkin():
|
||||
@login_required
|
||||
def api_save_checkin():
|
||||
body = request.get_json(force=True)
|
||||
date = body.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 字段'}), 400
|
||||
week = body.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 字段'}), 400
|
||||
data = body.get('data', {})
|
||||
save_checkin(date, data)
|
||||
save_checkin(week, data)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/checkin/<date>', methods=['DELETE'])
|
||||
@app.route('/api/checkin/<week>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_delete_checkin(date):
|
||||
delete_checkin(date)
|
||||
def api_delete_checkin(week):
|
||||
delete_checkin(week)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/checkin/weeks', methods=['GET'])
|
||||
@login_required
|
||||
def api_weeks():
|
||||
"""返回所有已打卡周号"""
|
||||
weeks = get_weeks_list()
|
||||
return jsonify({'ok': True, 'data': weeks})
|
||||
|
||||
|
||||
@app.route('/api/history', methods=['GET'])
|
||||
@login_required
|
||||
def api_history():
|
||||
@@ -247,6 +255,17 @@ def api_history():
|
||||
return jsonify({'ok': True, 'data': rows})
|
||||
|
||||
|
||||
@app.route('/api/history/daily', methods=['GET'])
|
||||
@login_required
|
||||
def api_history_daily():
|
||||
"""查看某周的原始日记录"""
|
||||
week = request.args.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
|
||||
rows = get_daily_backup(week)
|
||||
return jsonify({'ok': True, 'data': rows})
|
||||
|
||||
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
@login_required
|
||||
def api_stats():
|
||||
@@ -318,100 +337,85 @@ CALENDAR_CACHE = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'zi
|
||||
DING_MCP_URL = 'https://mcp-gw.dingtalk.com/server/95959163f85d8b58a167f65cd8bd3d22690b16c75ebacd0fa095016396a10e0d?key=0993e4d4e44c50ea25a0db840cc5815e'
|
||||
|
||||
|
||||
def _fetch_dingtalk_events(date_str):
|
||||
"""调用钉钉 MCP 实时查询指定日期的日程"""
|
||||
def _fetch_dingtalk_events_week(week_str):
|
||||
"""调用钉钉 MCP 查询指定周(周一~周日)的全部日程"""
|
||||
import json as _json, urllib.request as _ur
|
||||
from datetime import datetime, timezone, timedelta
|
||||
tz = timezone(timedelta(hours=8))
|
||||
try:
|
||||
d = datetime.strptime(date_str, '%Y-%m-%d').replace(tzinfo=tz)
|
||||
except ValueError:
|
||||
year_part, week_part = week_str.split('-W')
|
||||
year = int(year_part)
|
||||
week = int(week_part)
|
||||
except (ValueError, IndexError):
|
||||
return []
|
||||
start_ts = int(d.replace(hour=0, minute=0, second=0).timestamp() * 1000)
|
||||
end_ts = int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000)
|
||||
body = _json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'tools/call', 'id': 1,
|
||||
'params': {'name': 'list_calendar_events', 'arguments': {
|
||||
'calendarId': 'primary', 'startTime': start_ts, 'endTime': end_ts, 'limit': 20
|
||||
}}
|
||||
}).encode('utf-8')
|
||||
req = _ur.Request(DING_MCP_URL, data=body, headers={
|
||||
'Content-Type': 'application/json', 'Accept': 'application/json'
|
||||
})
|
||||
try:
|
||||
with _ur.urlopen(req, timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', [])
|
||||
except Exception:
|
||||
return None # 网络错误返回 None,调用方回退缓存
|
||||
jan1 = datetime(year, 1, 1, tzinfo=tz)
|
||||
monday = jan1 + timedelta(days=(week - 1) * 7 - jan1.weekday())
|
||||
results = []
|
||||
for e in events:
|
||||
summary = (e.get('summary') or '').strip()
|
||||
if not summary:
|
||||
continue
|
||||
start = e.get('start', {}).get('dateTime', '')
|
||||
end = e.get('end', {}).get('dateTime', '')
|
||||
time_str = ''
|
||||
if start and end:
|
||||
time_str = start[11:16] + '-' + end[11:16]
|
||||
results.append({
|
||||
'date': date_str, 'summary': summary,
|
||||
'time': time_str, 'location': e.get('location') or ''
|
||||
for i in range(7):
|
||||
d = monday + timedelta(days=i)
|
||||
ds = d.strftime('%Y-%m-%d')
|
||||
start_ts = int(d.replace(hour=0, minute=0, second=0).timestamp() * 1000)
|
||||
end_ts = int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000)
|
||||
body = _json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'tools/call', 'id': 1,
|
||||
'params': {'name': 'list_calendar_events', 'arguments': {
|
||||
'calendarId': 'primary', 'startTime': start_ts, 'endTime': end_ts, 'limit': 20
|
||||
}}
|
||||
}).encode('utf-8')
|
||||
req = _ur.Request(DING_MCP_URL, data=body, headers={
|
||||
'Content-Type': 'application/json', 'Accept': 'application/json'
|
||||
})
|
||||
# 写入缓存
|
||||
try:
|
||||
all_cached = []
|
||||
try:
|
||||
with open(CALENDAR_CACHE, 'r') as f:
|
||||
all_cached = _json.load(f)
|
||||
except (FileNotFoundError, _json.JSONDecodeError):
|
||||
pass
|
||||
# 替换同一天的旧缓存
|
||||
all_cached = [c for c in all_cached if c.get('date') != date_str] + results
|
||||
with open(CALENDAR_CACHE, 'w') as f:
|
||||
_json.dump(all_cached, f, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
with _ur.urlopen(req, timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', [])
|
||||
except Exception:
|
||||
continue
|
||||
for e in events:
|
||||
summary = (e.get('summary') or '').strip()
|
||||
if not summary:
|
||||
continue
|
||||
start = e.get('start', {}).get('dateTime', '')
|
||||
end = e.get('end', {}).get('dateTime', '')
|
||||
time_str = ''
|
||||
if start and end:
|
||||
time_str = start[11:16] + '-' + end[11:16]
|
||||
results.append({
|
||||
'date': ds, 'summary': summary,
|
||||
'time': time_str, 'location': e.get('location') or ''
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
@app.route('/api/calendar-sync', methods=['GET'])
|
||||
@login_required
|
||||
def api_calendar_sync():
|
||||
import json as _json
|
||||
date = request.args.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400
|
||||
# 优先实时查询钉钉 MCP
|
||||
events = _fetch_dingtalk_events(date)
|
||||
if events is None:
|
||||
# MCP 不可用,回退缓存
|
||||
try:
|
||||
with open(CALENDAR_CACHE, 'r') as f:
|
||||
all_cached = _json.load(f)
|
||||
events = [e for e in all_cached if e.get('date') == date]
|
||||
except (FileNotFoundError, _json.JSONDecodeError):
|
||||
events = []
|
||||
week = request.args.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
|
||||
events = _fetch_dingtalk_events_week(week)
|
||||
return jsonify({'ok': True, 'data': events})
|
||||
|
||||
|
||||
@app.route('/api/calendar-sync-all', methods=['GET'])
|
||||
@login_required
|
||||
def api_calendar_sync_all():
|
||||
"""批量查询过去15天~未来15天的钉钉日程,按日期分组返回,并自动填充到对应日期的 checkin"""
|
||||
"""批量查询过去4周~未来4周的钉钉日程,并自动填充到对应周的 checkin"""
|
||||
import json as _json
|
||||
from datetime import datetime, timedelta
|
||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
results = []
|
||||
saved_days = 0
|
||||
for delta in range(-15, 16):
|
||||
d = today + timedelta(days=delta)
|
||||
ds = d.strftime('%Y-%m-%d')
|
||||
events = _fetch_dingtalk_events(ds)
|
||||
saved_weeks = 0
|
||||
for delta in range(-4, 5):
|
||||
# 计算目标周
|
||||
target = today + timedelta(weeks=delta)
|
||||
iso_year, iso_week, _ = target.isocalendar()
|
||||
week_key = f"{iso_year}-W{iso_week:02d}"
|
||||
events = _fetch_dingtalk_events_week(week_key)
|
||||
if events:
|
||||
results.append({'date': ds, 'events': events})
|
||||
# 自动去重填充到对应日期的 checkin
|
||||
existing = get_checkin(ds)
|
||||
results.append({'date': week_key, 'events': events})
|
||||
# 自动去重填充
|
||||
existing = get_checkin(week_key)
|
||||
data = existing['data'] if existing else {'morning': [], 'evening': [], 'study': []}
|
||||
morning = data.get('morning', [])
|
||||
existing_texts = set()
|
||||
@@ -435,9 +439,9 @@ def api_calendar_sync_all():
|
||||
added = True
|
||||
if added:
|
||||
data['morning'] = morning
|
||||
save_checkin(ds, data)
|
||||
saved_days += 1
|
||||
return jsonify({'ok': True, 'data': results, 'saved_days': saved_days})
|
||||
save_checkin(week_key, data)
|
||||
saved_weeks += 1
|
||||
return jsonify({'ok': True, 'data': results, 'saved_days': saved_weeks})
|
||||
|
||||
|
||||
# ── 启动 ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user