feat: 操作日志系统 + 日期修复 + 协同去重 + 主题持久化 + 计划自动完成

本次累积提交包含以下功能:

1. 修复:el-date-picker 缺失 value-format 导致日期保存偏移(8处)
2. 新增:协同拜访 visit_group_id 去重机制(完整副本 + 周报合并 + 亮灯去重)
3. 新增:计划自动完成(创建计划检测已有拜访 / 编辑拜访触发)
4. 新增:拜访表单选择客户后自动预填过期计划内容
5. 新增:移动端主题切换按钮 + 后端持久化
6. 新增:操作日志系统 (audit_logs),全覆盖 8 个模块 CRUD
7. 新增:PC端「我的数据」支局长/领导视角客户经理列

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-30 12:01:14 +08:00
parent 9b5805447e
commit 2187be7aeb
35 changed files with 839 additions and 80 deletions
+1
View File
@@ -103,4 +103,5 @@ def build_token_for_user(user: User) -> str:
"name": user.name,
"role": user.role,
"department": user.department,
"theme": user.theme or "editorial",
})
+46 -24
View File
@@ -28,7 +28,8 @@ async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = No
today = date.today()
visits_count = (await db.execute(
select(func.count(Visit.id)).where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
select(func.count(func.distinct(func.coalesce(Visit.visit_group_id, Visit.id))))
.where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
)).scalar() or 0
plans_count = (await db.execute(
@@ -171,31 +172,52 @@ async def get_weekly_report(
visits_result = await db.execute(visit_query)
visits = visits_result.scalars().all()
visits_data = []
# Group visits by visit_group_id (or id for solo visits)
groups: dict[str, list] = {}
for v in visits:
# Resolve companion names: system users → names, external → direct
companion_names_resolved = [user_map.get(c, str(c)) for c in (v.companions or [])]
companion_names_resolved.extend(v.companion_names or [])
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,
"visitor_name": v.visitor_name or "",
"visitor_phone": v.visitor_phone or "",
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"companions": [str(c) for c in (v.companions or [])],
"companion_names": v.companion_names or [],
gid = str(v.visit_group_id) if v.visit_group_id else str(v.id)
groups.setdefault(gid, []).append(v)
visits_data = []
for gid, gvisits in groups.items():
# Determine the "primary" — first record in group (the creator's)
primary = gvisits[0]
companion_names_resolved = [user_map.get(c, str(c)) for c in (primary.companions or [])]
companion_names_resolved.extend(primary.companion_names or [])
merged = {
"id": str(primary.id),
"customer_id": str(primary.customer_id),
"customer_name": customer_map.get(primary.customer_id, ""),
"visit_date": str(primary.visit_date),
"visit_method": primary.visit_method,
"time_range": primary.time_range,
"visitor_name": primary.visitor_name or "",
"visitor_phone": primary.visitor_phone or "",
"visit_group_id": gid if len(gvisits) > 1 else None,
"companions": [str(c) for c in (primary.companions or [])],
"companion_names": primary.companion_names or [],
"companion_names_resolved": companion_names_resolved,
"photos": v.photos or [],
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"edit_log": v.edit_log or [],
"created_at": str(v.created_at),
})
"communication_content": primary.communication_content,
"customer_demand": primary.customer_demand,
"photos": primary.photos or [],
"manager_id": str(primary.manager_id),
"manager_name": user_map.get(primary.manager_id, ""),
"edit_log": primary.edit_log or [],
"created_at": str(primary.created_at),
"participants": [
{
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"role": "primary" if v is primary else "companion",
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"photos": v.photos or [],
}
for v in gvisits
],
}
visits_data.append(merged)
# ── Work Plans ──
wp_query = select(WorkPlan)
+14
View File
@@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.leave import Leave
from app.models.user import User
from app.utils.timezone import today_cst
from app.utils.audit import log_audit
from app.services.dashboard import get_week_range
@@ -88,6 +89,7 @@ async def create_leave(
data: dict,
submitted_by: UUID,
role: str,
operator_name: str = "",
) -> Leave:
"""Create leave. Manager can only create for self; director can create for anyone."""
if role == "manager":
@@ -108,6 +110,9 @@ async def create_leave(
db.add(leave)
await db.commit()
await db.refresh(leave)
# Audit log
await log_audit(db, "leave", leave.id, f"{leave.leave_type} ({leave.start_date}~{leave.end_date})", "create", str(submitted_by), operator_name)
await db.commit()
return leave
@@ -117,6 +122,7 @@ async def update_leave(
data: dict,
user_id: UUID,
role: str,
operator_name: str = "",
) -> Leave:
"""Update leave. Director can edit any; manager can edit own submitted leaves."""
leave = await get_leave_by_id(db, leave_id)
@@ -136,6 +142,9 @@ async def update_leave(
await db.commit()
await db.refresh(leave)
# Audit log
await log_audit(db, "leave", leave.id, f"{leave.leave_type} ({leave.start_date}~{leave.end_date})", "update", str(user_id), operator_name)
await db.commit()
return leave
@@ -144,6 +153,7 @@ async def delete_leave(
leave_id: UUID,
user_id: UUID,
role: str,
operator_name: str = "",
) -> bool:
"""Delete leave. Director can delete any; manager can delete own submitted leaves."""
leave = await get_leave_by_id(db, leave_id)
@@ -154,8 +164,12 @@ async def delete_leave(
if str(leave.submitted_by) != str(user_id):
raise PermissionError("客户经理只能删除自己提交的请假")
entity_name = f"{leave.leave_type} ({leave.start_date}~{leave.end_date})"
await db.delete(leave)
await db.commit()
# Audit log
await log_audit(db, "leave", leave_id, entity_name, "delete", str(user_id), operator_name)
await db.commit()
return True