企迹(qiji) 政企周报管理系统 — v0.1

后端: FastAPI + SQLAlchemy 2.0 (async) + Alembic + MinIO + Casdoor + 企微
前端: Vue 3 + Vite + TypeScript + Element Plus + Pinia

功能清单:
- 8 张数据表自动建表 / Casdoor OIDC 登录 / 企微静默登录
- 双布局: 移动端(填报) + PC端(汇总管理)
- 拜访记录 CRUD + MinIO 照片直传 + 缩略图预览 + 同访人草稿
- 今日纪要 (6 分类) / 工作计划 / 小微商机 / 要客拜访 CRUD
- 客户档案: 备注/收支费用/联系人/归属分配/批量转移
- 客户导入导出 + 模板下载 + 搜索/分页/筛选
- 仪表盘: 四卡统计 + 填报进度 (拜访+纪要双维度)
- 周报详情: 五 Tab + 按人/客户筛选 + 时间轴
- 用户管理 / 客户经理 PC 端工作台
- 企微: 催办/公告/定时提醒 / 时区修正
- Docker 部署配置

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:35:46 +08:00
parent 2a8225c31f
commit a1886074dd
97 changed files with 8830 additions and 0 deletions
+253
View File
@@ -0,0 +1,253 @@
from datetime import date, timedelta
from uuid import UUID
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased
from app.models.customer import Customer
from app.models.user import User
from app.models.visit import Visit
from app.models.work_plan import WorkPlan
from app.models.mini_business import MiniBusiness
from app.models.key_visit import KeyVisit
from app.models.daily_note import DailyNote
from app.utils.timezone import today_cst
def get_week_range(reference_date: date | None = None):
today = reference_date or date.today()
monday = today - timedelta(days=today.weekday())
sunday = monday + timedelta(days=6)
return monday, sunday
async def get_dashboard_stats(db: AsyncSession) -> dict:
"""Get dashboard statistics for the current week."""
monday, sunday = get_week_range()
today = date.today()
visits_count = (await db.execute(
select(func.count(Visit.id)).where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
)).scalar() or 0
plans_count = (await db.execute(
select(func.count(WorkPlan.id))
)).scalar() or 0
mini_biz_count = (await db.execute(
select(func.count(MiniBusiness.id))
)).scalar() or 0
key_visit_count = (await db.execute(
select(func.count(KeyVisit.id))
)).scalar() or 0
return {
"week_visits": visits_count,
"work_plans": plans_count,
"mini_business": mini_biz_count,
"key_visits": key_visit_count,
"week_start": str(monday),
"week_end": str(sunday),
}
async def get_reporting_progress(db: AsyncSession) -> list[dict]:
"""Get per-manager reporting progress for the current week."""
monday, sunday = get_week_range()
# Get all managers
managers_result = await db.execute(select(User).where(User.role == "manager"))
managers = managers_result.scalars().all()
# Get visit counts per manager this week
visits_result = await db.execute(
select(Visit.manager_id, func.count(Visit.id))
.where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
.group_by(Visit.manager_id)
)
visit_map = {str(uid): cnt for uid, cnt in visits_result.all()}
progress = []
for m in managers:
count = visit_map.get(str(m.id), 0)
# Calculate expected working days (Mon-Fri)
days_passed = min((date.today() - monday).days + 1, 5)
expected = days_passed # At least 1 visit per working day
progress.append({
"manager_id": str(m.id),
"manager_name": m.name,
"department": m.department,
"visit_count": count,
"expected": expected,
"completed": count >= expected,
"has_reported_today": False, # Will be set below
})
# Check today's reporting — visits OR daily notes
today = today_cst()
today_visits = await db.execute(
select(Visit.manager_id).where(Visit.visit_date == today)
)
today_notes = await db.execute(
select(DailyNote.manager_id).where(DailyNote.note_date == today)
)
reported_today = {str(uid) for uid, in today_visits.all()} | {str(uid) for uid, in today_notes.all()}
for p in progress:
p["has_reported_today"] = p["manager_id"] in reported_today
return progress
async def get_weekly_report(
db: AsyncSession, user_id: UUID, role: str,
filter_manager_id: UUID | None = None,
filter_customer_id: UUID | None = None,
) -> dict:
"""Get full weekly report data organized by module."""
monday, sunday = get_week_range()
# Base filters respecting role visibility
customer_map = {}
user_map = {}
customers_result = await db.execute(select(Customer.id, Customer.name))
customer_map = {c.id: c.name for c in customers_result.all()}
users_result = await db.execute(select(User.id, User.name))
user_map = {u.id: u.name for u in users_result.all()}
def build_manager_filter(existing_filter=None):
"""If role is manager, only see own data. Otherwise optionally filter by manager_id."""
if role == "manager":
return str(user_id)
return str(filter_manager_id) if filter_manager_id else None
# ── Visits ──
visit_query = select(Visit).where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
if role == "manager":
visit_query = visit_query.where(Visit.manager_id == user_id)
elif filter_manager_id:
visit_query = visit_query.where(Visit.manager_id == filter_manager_id)
if filter_customer_id:
visit_query = visit_query.where(Visit.customer_id == filter_customer_id)
visit_query = visit_query.order_by(Visit.visit_date.desc())
visits_result = await db.execute(visit_query)
visits = visits_result.scalars().all()
visits_data = []
for v in visits:
visits_data.append({
"id": str(v.id),
"customer_id": str(v.customer_id),
"customer_name": customer_map.get(v.customer_id, ""),
"visit_date": str(v.visit_date),
"visit_method": v.visit_method,
"time_range": v.time_range,
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"companions": [str(c) for c in (v.companions or [])],
"photos": v.photos or [],
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"created_at": str(v.created_at),
})
# ── Work Plans ──
wp_query = select(WorkPlan)
if role == "manager":
wp_query = wp_query.where(WorkPlan.manager_id == user_id)
elif filter_manager_id:
wp_query = wp_query.where(WorkPlan.manager_id == filter_manager_id)
if filter_customer_id:
wp_query = wp_query.where(WorkPlan.customer_id == filter_customer_id)
wp_result = await db.execute(wp_query)
work_plans_data = []
for w in wp_result.scalars():
work_plans_data.append({
"id": str(w.id),
"customer_id": str(w.customer_id),
"customer_name": customer_map.get(w.customer_id, ""),
"plan_content": w.plan_content,
"plan_date": str(w.plan_date),
"manager_id": str(w.manager_id),
"manager_name": user_map.get(w.manager_id, ""),
"status": w.status,
})
# ── Mini Business ──
mb_query = select(MiniBusiness)
if role == "manager":
mb_query = mb_query.where(MiniBusiness.manager_id == user_id)
elif filter_manager_id:
mb_query = mb_query.where(MiniBusiness.manager_id == filter_manager_id)
if filter_customer_id:
mb_query = mb_query.where(MiniBusiness.customer_id == filter_customer_id)
mb_result = await db.execute(mb_query)
mini_biz_data = []
for m in mb_result.scalars():
mini_biz_data.append({
"id": str(m.id),
"customer_id": str(m.customer_id),
"customer_name": customer_map.get(m.customer_id, ""),
"product_type": m.product_type,
"amount": m.amount,
"follow_up_detail": m.follow_up_detail,
"status": m.status,
"manager_id": str(m.manager_id),
"manager_name": user_map.get(m.manager_id, ""),
"expected_revenue_date": m.expected_revenue_date,
})
# ── Key Visits ──
kv_query = select(KeyVisit)
if role == "manager":
kv_query = kv_query.where(KeyVisit.manager_id == user_id)
elif filter_manager_id:
kv_query = kv_query.where(KeyVisit.manager_id == filter_manager_id)
if filter_customer_id:
kv_query = kv_query.where(KeyVisit.customer_id == filter_customer_id)
kv_result = await db.execute(kv_query)
key_visits_data = []
for k in kv_result.scalars():
key_visits_data.append({
"id": str(k.id),
"customer_id": str(k.customer_id),
"customer_name": customer_map.get(k.customer_id, ""),
"urgency_level": k.urgency_level,
"description": k.description,
"progress_status": k.progress_status,
"planned_date": k.planned_date,
"planned_visitor": k.planned_visitor,
"visit_target": k.visit_target,
"manager_id": str(k.manager_id),
"manager_name": user_map.get(k.manager_id, ""),
})
# ── Daily Notes ──
dn_query = select(DailyNote).where(DailyNote.note_date >= monday, DailyNote.note_date <= sunday)
if role == "manager":
dn_query = dn_query.where(DailyNote.manager_id == user_id)
elif filter_manager_id:
dn_query = dn_query.where(DailyNote.manager_id == filter_manager_id)
dn_query = dn_query.order_by(DailyNote.note_date.desc())
dn_result = await db.execute(dn_query)
daily_notes_data = []
for d in dn_result.scalars():
daily_notes_data.append({
"id": str(d.id),
"note_date": str(d.note_date),
"category": d.category,
"content": d.content,
"time_range": d.time_range,
"manager_id": str(d.manager_id),
"manager_name": user_map.get(d.manager_id, ""),
})
return {
"week_start": str(monday),
"week_end": str(sunday),
"visits": visits_data,
"daily_notes": daily_notes_data,
"work_plans": work_plans_data,
"mini_business": mini_biz_data,
"key_visits": key_visits_data,
}