Files
qiji/backend/app/services/scheduler.py
T
v6ole ab20bc5a1f Merge branch 'main' into develop
# Conflicts:
#	backend/app/api/key_visits.py
#	backend/app/api/mini_business.py
#	backend/app/api/visits.py
#	backend/app/api/work_plans.py
#	backend/app/main.py
#	backend/app/models/__init__.py
#	backend/app/schemas/key_visit.py
#	backend/app/schemas/mini_business.py
#	backend/app/schemas/work_plan.py
#	backend/app/services/light_board.py
#	frontend/src/components/DesktopLayout.vue
#	frontend/src/stores/theme.ts
#	frontend/src/views/desktop/ManagerWorkspace.vue
#	frontend/src/views/desktop/WorkPlans.vue
#	frontend/src/views/mobile/KeyVisitForm.vue
#	frontend/src/views/mobile/LeaveForm.vue
#	frontend/src/views/mobile/PlansList.vue
#	frontend/src/views/mobile/VisitForm.vue
#	frontend/src/views/mobile/WorkPlanForm.vue
2026-08-17 09:31:12 +08:00

201 lines
7.2 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.holidays import load_holiday_sets
from app.services.wecom import wecom_client
from app.utils.timezone import today_cst, is_working_day
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()
# Load holiday data (auto-refreshes from API if not cached)
holidays, workdays = await load_holiday_sets(db)
if not is_working_day(today, holidays, workdays):
return {"status": "rest_day", "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 directors/leaders only (not broadcast to @all)
directors = await db.execute(
select(User).where(User.role.in_(["director", "leader"]), User.wecom_userid.isnot(None))
)
director_ids = [d.wecom_userid for d in directors.scalars().all()]
if managers and director_ids:
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, user_ids=director_ids)
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)
# ── Auto-cancel overdue plans that have no matching visit ──
from app.utils.edit_log import append_entry as append_edit_log
auto_cancelled = 0
for plan in overdue:
has_visit = await db.execute(
select(Visit).where(
Visit.customer_id == plan.customer_id,
Visit.visit_date >= plan.plan_date,
)
)
if not has_visit.scalar():
plan.status = "已取消"
append_edit_log(plan, "系统", [{
"field": "status",
"from": "计划中",
"to": "已取消",
"reason": "逾期自动取消",
}])
auto_cancelled += 1
if auto_cancelled:
await db.commit()
return {
"status": "ok",
"date": str(today),
"overdue": len(overdue),
"auto_cancelled": auto_cancelled,
"managers_affected": len(by_manager),
}