Files
qiji/backend/app/services/scheduler.py
T

173 lines
6.3 KiB
Python

from datetime import date, datetime
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.visit import Visit
from app.models.daily_note import DailyNote
from app.models.user import User
from app.models.leave import Leave
from app.models.work_plan import WorkPlan
from app.models.customer import Customer
from app.services.wecom import wecom_client
from app.utils.timezone import today_cst
async def check_daily_reporting(db: AsyncSession) -> dict:
"""Check today's reporting progress and send wecom reminders to managers who haven't reported."""
today = today_cst()
weekday = today.weekday()
if weekday >= 5: # Skip weekends
return {"status": "weekend", "date": str(today)}
# Get all users who need to report
result = await db.execute(
select(User).where(User.require_report == True)
)
managers = result.scalars().all()
# ── Exclude managers on leave today ──
leaves_today = await db.execute(
select(Leave.manager_id).where(and_(
Leave.start_date <= today,
Leave.end_date >= today,
))
)
on_leave_ids = {str(uid) for uid, in leaves_today.all()}
# Get managers who have reported today
reported_visits = await db.execute(
select(Visit.manager_id).where(Visit.visit_date == today)
)
reported_notes = await db.execute(
select(DailyNote.manager_id).where(DailyNote.note_date == today)
)
reported_map = {str(uid): True for uid, in reported_visits.all()}
for uid, in reported_notes.all():
reported_map[str(uid)] = True
not_reported = []
reported_names = []
on_leave_names = []
for m in managers:
if str(m.id) in on_leave_ids:
on_leave_names.append(m.name)
continue # skip — exempt from reporting
if str(m.id) in reported_map:
reported_names.append(m.name)
else:
not_reported.append(m)
# 1. Send template card to not-reported managers (tap to open app)
if not_reported:
user_ids = [m.wecom_userid for m in not_reported if m.wecom_userid]
if user_ids:
desc = f"{today} | 已填报 {len(reported_names)}/{len(managers)} 人"
success = await wecom_client.send_template_card(
user_ids=user_ids,
title="📋 今日填报提醒",
desc=desc,
url="https://qj.dhdx.fun/m",
btn_text="去填报",
)
# Fallback to text
if not success:
names_text = "、".join(m.name for m in not_reported)
await wecom_client.send_text_message(
user_ids,
f"📋 今日填报提醒\n\n{desc}\n未填报:{names_text}\n\n请尽快完成填报 🙏\nhttps://qj.dhdx.fun/m",
)
# 2. Send summary to director
directors = await db.execute(
select(User).where(User.role.in_(["director", "leader"]), User.wecom_userid.isnot(None))
)
for d in directors.scalars().all():
if managers:
effective_total = len(managers) - len(on_leave_names)
if effective_total > 0:
pct = len(reported_map) / effective_total * 100
else:
pct = 100.0
leave_note = ""
if on_leave_names:
leave_note = f"\n> 请假中(已豁免):{len(on_leave_names)}\n"
leave_note += "".join(f"- {n} (请假)\n" for n in on_leave_names)
summary = (
f"## 📊 今日填报汇总\n\n"
f"> 日期:{today}\n"
f"> 填报率:**{pct:.0f}%** ({len(reported_map)}/{effective_total})\n"
)
if leave_note:
summary += leave_note + "\n"
if not_reported:
summary += "**未填报:**\n" + "".join(f"- {m.name}\n" for m in not_reported)
else:
summary += "✅ 全体已完成今日填报"
await wecom_client.send_markdown_message(summary) # broadcast for director visibility
return {
"status": "ok",
"date": str(today),
"total_managers": len(managers),
"reported": len(reported_map),
"not_reported": len(not_reported),
}
async def check_overdue_plans(db: AsyncSession) -> dict:
"""Check for overdue work plans and remind managers (runs at 9:00 AM)."""
today = today_cst()
if today.weekday() >= 5:
return {"status": "weekend", "date": str(today)}
result = await db.execute(
select(WorkPlan).where(
WorkPlan.status == "计划中",
WorkPlan.plan_date < today,
).order_by(WorkPlan.manager_id, WorkPlan.plan_date)
)
overdue = result.scalars().all()
if not overdue:
return {"status": "ok", "date": str(today), "overdue": 0}
# Group by manager
by_manager: dict[str, list] = {}
for p in overdue:
mid = str(p.manager_id)
by_manager.setdefault(mid, []).append(p)
users_result = await db.execute(
select(User).where(User.id.in_([uid for uid in by_manager.keys()]))
)
user_map = {str(u.id): u for u in users_result.scalars().all()}
for mid, plans in by_manager.items():
user = user_map.get(mid)
if not user or not user.wecom_userid:
continue
names = "、".join(f"{p.customer_id}" for p in plans[:5])
# Get customer names
cust_result = await db.execute(
select(Customer.name).where(Customer.id.in_([p.customer_id for p in plans[:5]]))
)
cust_names = [r[0] for r in cust_result.all()]
plan_lines = "".join(f"- {n} (计划 {p.plan_date})\n" for p, n in zip(plans[:5], cust_names))
content = (
f"📅 拜访计划过期提醒\n\n"
f"以下 {len(plans)} 个拜访计划已过期,请尽快安排拜访:\n"
f"{plan_lines}"
)
if len(plans) > 5:
content += f"... 还有 {len(plans) - 5} 个过期计划\n"
content += f"\n👉 查看详情:https://qj.dhdx.fun/light-board"
await wecom_client.send_text_message([user.wecom_userid], content)
return {
"status": "ok",
"date": str(today),
"overdue": len(overdue),
"managers_affected": len(by_manager),
}