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:
@@ -0,0 +1,80 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
router = APIRouter(prefix="/audit-logs", tags=["AuditLogs"])
|
||||
|
||||
ENTITY_LABELS: dict[str, str] = {
|
||||
"customer": "客户",
|
||||
"visit": "拜访记录",
|
||||
"daily_note": "今日纪要",
|
||||
"work_plan": "工作计划",
|
||||
"mini_business": "小微商机",
|
||||
"key_visit": "要客拜访",
|
||||
"user": "用户",
|
||||
"leave": "请假",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_audit_logs(
|
||||
entity_type: Optional[str] = Query(None),
|
||||
action: Optional[str] = Query(None),
|
||||
operator_id: Optional[str] = Query(None),
|
||||
date_from: Optional[str] = Query(None),
|
||||
date_to: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=10, le=200),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List audit logs. Only director/leader can access."""
|
||||
if current_user["role"] not in ("director", "leader"):
|
||||
return {"items": [], "total": 0}
|
||||
|
||||
base = select(AuditLog)
|
||||
count_q = select(func.count(AuditLog.id))
|
||||
|
||||
if entity_type:
|
||||
base = base.where(AuditLog.entity_type == entity_type)
|
||||
count_q = count_q.where(AuditLog.entity_type == entity_type)
|
||||
if action:
|
||||
base = base.where(AuditLog.action == action)
|
||||
count_q = count_q.where(AuditLog.action == action)
|
||||
if operator_id:
|
||||
base = base.where(AuditLog.operator_id == uuid.UUID(operator_id))
|
||||
count_q = count_q.where(AuditLog.operator_id == uuid.UUID(operator_id))
|
||||
if date_from:
|
||||
base = base.where(AuditLog.created_at >= date_from)
|
||||
count_q = count_q.where(AuditLog.created_at >= date_from)
|
||||
if date_to:
|
||||
base = base.where(AuditLog.created_at < date_to + "T23:59:59")
|
||||
count_q = count_q.where(AuditLog.created_at < date_to + "T23:59:59")
|
||||
|
||||
total = (await db.execute(count_q)).scalar() or 0
|
||||
|
||||
query = base.order_by(AuditLog.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
rows = (await db.execute(query)).scalars().all()
|
||||
|
||||
items = []
|
||||
for r in rows:
|
||||
items.append({
|
||||
"id": str(r.id),
|
||||
"entity_type": r.entity_type,
|
||||
"entity_type_label": ENTITY_LABELS.get(r.entity_type, r.entity_type),
|
||||
"entity_id": str(r.entity_id),
|
||||
"entity_name": r.entity_name,
|
||||
"action": r.action,
|
||||
"operator_id": str(r.operator_id),
|
||||
"operator_name": r.operator_name,
|
||||
"summary": r.summary or "",
|
||||
"details": r.details,
|
||||
"created_at": str(r.created_at),
|
||||
})
|
||||
|
||||
return {"items": items, "total": total}
|
||||
@@ -34,6 +34,7 @@ async def casdoor_login(req: CasdoorLoginRequest, db: AsyncSession = Depends(get
|
||||
user_id=str(user.id),
|
||||
name=user.name,
|
||||
role=user.role,
|
||||
theme=user.theme or "editorial",
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +57,7 @@ async def wecom_login(req: WecomLoginRequest, db: AsyncSession = Depends(get_db)
|
||||
user_id=str(user.id),
|
||||
name=user.name,
|
||||
role=user.role,
|
||||
theme=user.theme or "editorial",
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.models.mini_business import MiniBusiness
|
||||
from app.models.key_visit import KeyVisit
|
||||
from app.models.daily_note import DailyNote
|
||||
from app.models.user import User
|
||||
from app.utils.audit import log_audit
|
||||
from app.schemas.customer import (
|
||||
CustomerCreate, CustomerUpdate, CustomerOut, CustomerListOut, CustomerListResponse,
|
||||
ContactCreate, ContactOut, AssignmentCreate, AssignmentOut, BatchAssignRequest,
|
||||
@@ -399,6 +400,10 @@ async def create_customer(
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
# Audit log
|
||||
await log_audit(db, "customer", customer.id, customer.name, "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.id == customer.id).options(selectinload(Customer.contacts))
|
||||
)
|
||||
@@ -470,6 +475,10 @@ async def update_customer(
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Audit log
|
||||
await log_audit(db, "customer", uuid_mod.UUID(customer_id), customer.name, "update", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.id == customer.id).options(selectinload(Customer.contacts))
|
||||
)
|
||||
@@ -486,8 +495,12 @@ async def delete_customer(
|
||||
customer = result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
cust_name = customer.name
|
||||
await db.delete(customer)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "customer", uuid_mod.UUID(customer_id), cust_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
|
||||
@@ -715,3 +728,33 @@ async def assign_manager(
|
||||
await db.commit()
|
||||
await db.refresh(assignment)
|
||||
return assignment
|
||||
|
||||
|
||||
@router.get("/{customer_id}/overdue-plans")
|
||||
async def get_overdue_plans(
|
||||
customer_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return the latest overdue (plan_date < today) and still '计划中' plan for a customer.
|
||||
Used by the frontend to pre-fill visit forms with pending plan content."""
|
||||
from app.utils.timezone import today_cst
|
||||
today = today_cst()
|
||||
result = await db.execute(
|
||||
select(WorkPlan).where(
|
||||
WorkPlan.customer_id == uuid_mod.UUID(customer_id),
|
||||
WorkPlan.status == "计划中",
|
||||
WorkPlan.plan_date < today,
|
||||
).order_by(WorkPlan.plan_date.desc()).limit(1)
|
||||
)
|
||||
plan = result.scalar_one_or_none()
|
||||
if not plan:
|
||||
return {"plan": None}
|
||||
return {
|
||||
"plan": {
|
||||
"id": str(plan.id),
|
||||
"plan_content": plan.plan_content,
|
||||
"plan_date": str(plan.plan_date),
|
||||
"manager_id": str(plan.manager_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.models.user import User
|
||||
from app.schemas.daily_note import DailyNoteCreate, DailyNoteUpdate, DailyNoteOut
|
||||
from app.utils.timezone import today_cst, parse_date
|
||||
from app.utils.edit_log import compute_diff, append_entry, init_entry
|
||||
from app.utils.audit import log_audit
|
||||
|
||||
router = APIRouter(prefix="/daily-notes", tags=["DailyNotes"])
|
||||
|
||||
@@ -90,6 +91,9 @@ async def create_note(
|
||||
db.add(note)
|
||||
await db.commit()
|
||||
await db.refresh(note)
|
||||
# Audit log
|
||||
await log_audit(db, "daily_note", note.id, f"{note.note_date} 纪要", "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(note, db)
|
||||
|
||||
|
||||
@@ -118,6 +122,9 @@ async def update_note(
|
||||
append_entry(note, current_user["name"], changes, getattr(data, "edit_reason", None))
|
||||
await db.commit()
|
||||
await db.refresh(note)
|
||||
# Audit log
|
||||
await log_audit(db, "daily_note", note.id, f"{note.note_date} 纪要", "update", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(note, db)
|
||||
|
||||
|
||||
@@ -133,6 +140,10 @@ async def delete_note(
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(note.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
note_date = str(note.note_date)
|
||||
await db.delete(note)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "daily_note", uuid.UUID(note_id), f"{note_date} 纪要", "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.models.customer import Customer
|
||||
from app.models.user import User
|
||||
from app.schemas.key_visit import KeyVisitCreate, KeyVisitUpdate, KeyVisitOut
|
||||
from app.utils.edit_log import compute_diff, append_entry, init_entry
|
||||
from app.utils.audit import log_audit
|
||||
|
||||
router = APIRouter(prefix="/key-visits", tags=["KeyVisits"])
|
||||
|
||||
@@ -69,6 +70,21 @@ async def create_key_visit(
|
||||
db.add(k)
|
||||
await db.commit()
|
||||
await db.refresh(k)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == k.customer_id))
|
||||
await log_audit(db, "key_visit", k.id, f"{cust.scalar_one_or_none() or ''} ({k.urgency_level})", "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(k, db)
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
async def get_key_visit(item_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)):
|
||||
result = await db.execute(select(KeyVisit).where(KeyVisit.id == item_id))
|
||||
k = result.scalar_one_or_none()
|
||||
if not k:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(k.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return await _enrich(k, db)
|
||||
|
||||
|
||||
@@ -95,6 +111,10 @@ async def update_key_visit(
|
||||
append_entry(k, current_user["name"], changes, getattr(data, "edit_reason", None))
|
||||
await db.commit()
|
||||
await db.refresh(k)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == k.customer_id))
|
||||
await log_audit(db, "key_visit", k.id, f"{cust.scalar_one_or_none() or ''} ({k.urgency_level})", "update", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(k, db)
|
||||
|
||||
|
||||
@@ -110,6 +130,11 @@ async def delete_key_visit(
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(k.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == k.customer_id))
|
||||
entity_name = f"{cust.scalar_one_or_none() or ''} ({k.urgency_level})"
|
||||
await db.delete(k)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "key_visit", uuid.UUID(item_id), entity_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
@@ -43,6 +43,7 @@ async def create_leave(
|
||||
data=data.model_dump(),
|
||||
submitted_by=UUID(current_user["user_id"]),
|
||||
role=current_user["role"],
|
||||
operator_name=current_user["name"],
|
||||
)
|
||||
return {
|
||||
"id": str(leave.id),
|
||||
@@ -74,6 +75,7 @@ async def update_leave(
|
||||
data=data.model_dump(exclude_none=True),
|
||||
user_id=UUID(current_user["user_id"]),
|
||||
role=current_user["role"],
|
||||
operator_name=current_user["name"],
|
||||
)
|
||||
return {
|
||||
"id": str(leave.id),
|
||||
@@ -102,6 +104,7 @@ async def delete_leave(
|
||||
leave_id=leave_id,
|
||||
user_id=UUID(current_user["user_id"]),
|
||||
role=current_user["role"],
|
||||
operator_name=current_user["name"],
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="请假记录不存在")
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.models.customer import Customer
|
||||
from app.models.user import User
|
||||
from app.schemas.mini_business import MiniBusinessCreate, MiniBusinessUpdate, MiniBusinessOut
|
||||
from app.utils.edit_log import compute_diff, append_entry, init_entry
|
||||
from app.utils.audit import log_audit
|
||||
|
||||
router = APIRouter(prefix="/mini-business", tags=["MiniBusiness"])
|
||||
|
||||
@@ -67,6 +68,21 @@ async def create_mini_business(
|
||||
db.add(m)
|
||||
await db.commit()
|
||||
await db.refresh(m)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == m.customer_id))
|
||||
await log_audit(db, "mini_business", m.id, f"{cust.scalar_one_or_none() or ''} ({m.product_type})", "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(m, db)
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
async def get_mini_business(item_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)):
|
||||
result = await db.execute(select(MiniBusiness).where(MiniBusiness.id == item_id))
|
||||
m = result.scalar_one_or_none()
|
||||
if not m:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(m.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return await _enrich(m, db)
|
||||
|
||||
|
||||
@@ -93,6 +109,10 @@ async def update_mini_business(
|
||||
append_entry(m, current_user["name"], changes, getattr(data, "edit_reason", None))
|
||||
await db.commit()
|
||||
await db.refresh(m)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == m.customer_id))
|
||||
await log_audit(db, "mini_business", m.id, f"{cust.scalar_one_or_none() or ''} ({m.product_type})", "update", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(m, db)
|
||||
|
||||
|
||||
@@ -108,6 +128,11 @@ async def delete_mini_business(
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(m.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == m.customer_id))
|
||||
entity_name = f"{cust.scalar_one_or_none() or ''} ({m.product_type})"
|
||||
await db.delete(m)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "mini_business", uuid.UUID(item_id), entity_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
@@ -7,6 +7,7 @@ from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_director
|
||||
from app.models.user import User
|
||||
from app.utils.audit import log_audit
|
||||
from app.schemas.customer import UserOut
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["Users"])
|
||||
@@ -78,6 +79,9 @@ async def update_user_role(
|
||||
user.color = data.color
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
# Audit log
|
||||
await log_audit(db, "user", uuid.UUID(user_id), user.name, "update", current_user["user_id"], current_user["name"], f"角色更新为 {data.role}")
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
@@ -143,6 +147,32 @@ async def delete_user(
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user_name = user.name
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
return {"detail": "deleted", "name": user.name}
|
||||
# Audit log
|
||||
await log_audit(db, "user", uuid.UUID(user_id), user_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted", "name": user_name}
|
||||
|
||||
|
||||
class UpdateThemeRequest(BaseModel):
|
||||
theme: str # editorial / light
|
||||
|
||||
|
||||
@router.put("/me/theme")
|
||||
async def update_my_theme(
|
||||
data: UpdateThemeRequest,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update the current user's theme preference."""
|
||||
if data.theme not in ("editorial", "light"):
|
||||
raise HTTPException(status_code=400, detail="Invalid theme")
|
||||
result = await db.execute(select(User).where(User.id == uuid.UUID(current_user["user_id"])))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user.theme = data.theme
|
||||
await db.commit()
|
||||
return {"theme": user.theme}
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.utils.timezone import today_cst, parse_date
|
||||
from app.services.minio_client import delete_objects
|
||||
from app.api.upload import is_owned_upload_key
|
||||
from app.utils.edit_log import compute_diff, append_entry, init_entry
|
||||
from app.utils.audit import log_audit
|
||||
|
||||
router = APIRouter(prefix="/visits", tags=["Visits"])
|
||||
|
||||
@@ -61,6 +62,7 @@ async def _enrich_visit(visit: Visit, db: AsyncSession) -> dict:
|
||||
"photos": visit.photos,
|
||||
"manager_id": str(visit.manager_id),
|
||||
"manager_name": manager_name,
|
||||
"visit_group_id": str(visit.visit_group_id) if visit.visit_group_id else None,
|
||||
"edit_log": visit.edit_log or [],
|
||||
"created_at": str(visit.created_at),
|
||||
"updated_at": str(visit.updated_at),
|
||||
@@ -146,8 +148,9 @@ async def create_visit(
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a visit record. If companions are selected, creates draft copies for them."""
|
||||
"""Create a visit record. If companions are selected, creates full copies for them."""
|
||||
_validate_photo_keys(data.photos, current_user["user_id"])
|
||||
group_id = uuid.uuid4()
|
||||
visit = Visit(
|
||||
customer_id=data.customer_id,
|
||||
visit_date=parse_date(data.visit_date),
|
||||
@@ -159,6 +162,7 @@ async def create_visit(
|
||||
companion_names=data.companion_names,
|
||||
photos=data.photos,
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
visit_group_id=group_id,
|
||||
)
|
||||
init_entry(visit, current_user["name"])
|
||||
db.add(visit)
|
||||
@@ -184,22 +188,36 @@ async def create_visit(
|
||||
plan.status = "已完成"
|
||||
append_entry(plan, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "拜访自动完成"}])
|
||||
|
||||
# Create draft copies for companions
|
||||
# Create full copies for companions (not blank drafts)
|
||||
creator_name = current_user["name"]
|
||||
for companion_id in data.companions:
|
||||
if companion_id != uuid.UUID(current_user["user_id"]):
|
||||
# Build companion's companions list: the creator + other companions excluding self
|
||||
comp_companions = [uuid.UUID(current_user["user_id"])]
|
||||
for cid in data.companions:
|
||||
if cid != companion_id and cid not in comp_companions:
|
||||
comp_companions.append(cid)
|
||||
draft = Visit(
|
||||
customer_id=data.customer_id,
|
||||
visit_date=parse_date(data.visit_date),
|
||||
visit_method=data.visit_method,
|
||||
time_range=data.time_range,
|
||||
communication_content="", # Leave blank for companion to fill
|
||||
customer_demand="",
|
||||
companions=[],
|
||||
photos=[],
|
||||
visitor_name=data.visitor_name,
|
||||
visitor_phone=data.visitor_phone,
|
||||
communication_content=(data.communication_content or "") + f"(协同{creator_name})",
|
||||
customer_demand=data.customer_demand,
|
||||
companions=comp_companions,
|
||||
companion_names=[],
|
||||
photos=data.photos,
|
||||
manager_id=companion_id,
|
||||
visit_group_id=group_id,
|
||||
)
|
||||
init_entry(draft, current_user["name"])
|
||||
db.add(draft)
|
||||
|
||||
# Audit log (before commit — part of same transaction)
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id))
|
||||
await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({data.visit_date})", "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
await db.refresh(visit)
|
||||
return await _enrich_visit(visit, db)
|
||||
@@ -249,6 +267,22 @@ async def update_visit(
|
||||
if changes:
|
||||
append_entry(visit, current_user["name"], changes, getattr(data, "edit_reason", None))
|
||||
|
||||
# Auto-complete matching work plans when visit date or customer changes
|
||||
from app.models.work_plan import WorkPlan
|
||||
plans_result = await db.execute(
|
||||
select(WorkPlan).where(
|
||||
WorkPlan.customer_id == visit.customer_id,
|
||||
WorkPlan.status == "计划中",
|
||||
WorkPlan.plan_date <= visit.visit_date,
|
||||
)
|
||||
)
|
||||
for plan in plans_result.scalars().all():
|
||||
plan.status = "已完成"
|
||||
append_entry(plan, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "拜访更新自动完成"}])
|
||||
|
||||
# Audit log (before commit — part of same transaction)
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id))
|
||||
await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({visit.visit_date})", "update", current_user["user_id"], current_user["name"], ", ".join([c["field"] for c in changes]) if changes else "")
|
||||
await db.commit()
|
||||
await db.refresh(visit)
|
||||
return await _enrich_visit(visit, db)
|
||||
@@ -275,6 +309,10 @@ async def delete_visit(
|
||||
if visit.photos:
|
||||
delete_objects(visit.photos)
|
||||
|
||||
# Snapshot entity name before deletion
|
||||
cust_name_result = await db.execute(select(Customer.name).where(Customer.id == customer_id))
|
||||
entity_name = f"{cust_name_result.scalar_one_or_none() or ''} ({visit.visit_date})"
|
||||
|
||||
await db.delete(visit)
|
||||
|
||||
# Recalculate customer's last_visit_date from remaining visits
|
||||
@@ -288,7 +326,6 @@ async def delete_visit(
|
||||
if cust:
|
||||
if new_latest:
|
||||
cust.last_visit_date = new_latest
|
||||
# Keep the existing manager if date unchanged, or find who made the latest visit
|
||||
latest_visit = await db.execute(
|
||||
select(Visit).where(
|
||||
Visit.customer_id == customer_id,
|
||||
@@ -302,5 +339,7 @@ async def delete_visit(
|
||||
cust.last_visit_date = None
|
||||
cust.last_visit_manager_id = None
|
||||
|
||||
# Audit log (before final commit — part of same transaction)
|
||||
await log_audit(db, "visit", uuid.UUID(visit_id), entity_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_any_role
|
||||
from app.utils.timezone import parse_date
|
||||
from app.utils.edit_log import compute_diff, append_entry, init_entry
|
||||
from app.utils.audit import log_audit
|
||||
from app.models.work_plan import WorkPlan
|
||||
from app.models.customer import Customer
|
||||
from app.models.user import User
|
||||
@@ -54,17 +55,48 @@ async def create_work_plan(
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
plan_date = parse_date(data.plan_date)
|
||||
|
||||
# Auto-complete if a visit already exists for this customer on/after the plan date
|
||||
from app.models.visit import Visit
|
||||
if data.status == "计划中":
|
||||
existing_visit = await db.execute(
|
||||
select(Visit.id).where(
|
||||
Visit.customer_id == data.customer_id,
|
||||
Visit.visit_date >= plan_date,
|
||||
).limit(1)
|
||||
)
|
||||
if existing_visit.scalar_one_or_none():
|
||||
data.status = "已完成"
|
||||
|
||||
wp = WorkPlan(
|
||||
customer_id=data.customer_id,
|
||||
plan_content=data.plan_content,
|
||||
plan_date=parse_date(data.plan_date),
|
||||
plan_date=plan_date,
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
status=data.status,
|
||||
)
|
||||
init_entry(wp, current_user["name"])
|
||||
if data.status == "已完成":
|
||||
append_entry(wp, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "已有拜访记录,自动完成"}])
|
||||
db.add(wp)
|
||||
await db.commit()
|
||||
await db.refresh(wp)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
|
||||
await log_audit(db, "work_plan", wp.id, f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})", "create", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(wp, db)
|
||||
|
||||
|
||||
@router.get("/{plan_id}")
|
||||
async def get_work_plan(plan_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)):
|
||||
result = await db.execute(select(WorkPlan).where(WorkPlan.id == plan_id))
|
||||
wp = result.scalar_one_or_none()
|
||||
if not wp:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(wp.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return await _enrich(wp, db)
|
||||
|
||||
|
||||
@@ -93,6 +125,10 @@ async def update_work_plan(
|
||||
append_entry(wp, current_user["name"], changes, getattr(data, "edit_reason", None))
|
||||
await db.commit()
|
||||
await db.refresh(wp)
|
||||
# Audit log
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
|
||||
await log_audit(db, "work_plan", wp.id, f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})", "update", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return await _enrich(wp, db)
|
||||
|
||||
|
||||
@@ -108,6 +144,11 @@ async def delete_work_plan(
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(wp.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
|
||||
entity_name = f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})"
|
||||
await db.delete(wp)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "work_plan", uuid.UUID(plan_id), entity_name, "delete", current_user["user_id"], current_user["name"])
|
||||
await db.commit()
|
||||
return {"detail": "deleted"}
|
||||
|
||||
Reference in New Issue
Block a user