企迹(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
+10
View File
@@ -0,0 +1,10 @@
from app.schemas.customer import (
CustomerCreate, CustomerUpdate, CustomerOut, CustomerListOut,
ContactCreate, ContactOut, AssignmentCreate, AssignmentOut, BatchAssignRequest,
)
from app.schemas.visit import VisitCreate, VisitUpdate, VisitOut, VisitListOut
from app.schemas.work_plan import WorkPlanCreate, WorkPlanUpdate, WorkPlanOut
from app.schemas.mini_business import MiniBusinessCreate, MiniBusinessUpdate, MiniBusinessOut
from app.schemas.key_visit import KeyVisitCreate, KeyVisitUpdate, KeyVisitOut
from app.schemas.user import TokenResponse, WecomLoginRequest, CasdoorLoginRequest, WecomBindRequest
from app.schemas.customer import UserCreate, UserOut
+118
View File
@@ -0,0 +1,118 @@
from pydantic import BaseModel, Field
from typing import Optional
import uuid
from datetime import date, datetime
# ── User ──
class UserCreate(BaseModel):
name: str
role: str = "manager"
department: str = ""
wecom_userid: Optional[str] = None
class UserOut(BaseModel):
id: uuid.UUID
casdoor_id: str
name: str
role: str
department: str
wecom_userid: Optional[str] = None
model_config = {"from_attributes": True}
# ── CustomerContact ──
class ContactCreate(BaseModel):
name: str
phone: str = ""
role_desc: str = ""
class ContactOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
name: str
phone: str
role_desc: str
model_config = {"from_attributes": True}
# ── Customer ──
class CustomerCreate(BaseModel):
name: str
industry: str = ""
address: str = ""
in_use_services: str = ""
monthly_fee: str = ""
remarks: str = ""
contacts: list[ContactCreate] = []
assignee_id: Optional[uuid.UUID] = None
class CustomerUpdate(BaseModel):
name: Optional[str] = None
industry: Optional[str] = None
address: Optional[str] = None
in_use_services: Optional[str] = None
monthly_fee: Optional[str] = None
remarks: Optional[str] = None
assignee_id: Optional[uuid.UUID] = None
class CustomerOut(BaseModel):
id: uuid.UUID
name: str
industry: str
address: str
in_use_services: str
monthly_fee: str
remarks: str = ""
created_by: uuid.UUID
created_at: datetime
updated_at: datetime
contacts: list[ContactOut] = []
primary_manager_name: Optional[str] = None
model_config = {"from_attributes": True}
class CustomerListOut(BaseModel):
id: uuid.UUID
name: str
industry: str
in_use_services: str
model_config = {"from_attributes": True}
class CustomerListResponse(BaseModel):
items: list[CustomerListOut]
total: int
page: int
page_size: int
# ── CustomerAssignment ──
class AssignmentCreate(BaseModel):
customer_id: uuid.UUID
manager_id: uuid.UUID
role: str = "primary"
class AssignmentOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
manager_id: uuid.UUID
role: str
assigned_at: datetime
assigned_by: uuid.UUID
model_config = {"from_attributes": True}
class BatchAssignRequest(BaseModel):
customer_ids: list[uuid.UUID]
manager_id: uuid.UUID
+32
View File
@@ -0,0 +1,32 @@
from pydantic import BaseModel
from typing import Optional
import uuid
from datetime import date, datetime
class DailyNoteCreate(BaseModel):
note_date: str # YYYY-MM-DD
category: str = "其他"
content: str = ""
time_range: str = ""
class DailyNoteUpdate(BaseModel):
note_date: Optional[str] = None
category: Optional[str] = None
content: Optional[str] = None
time_range: Optional[str] = None
class DailyNoteOut(BaseModel):
id: uuid.UUID
manager_id: uuid.UUID
note_date: date
category: str
content: str
time_range: str
created_at: datetime
updated_at: datetime
manager_name: Optional[str] = None
model_config = {"from_attributes": True}
+39
View File
@@ -0,0 +1,39 @@
from pydantic import BaseModel
from typing import Optional
import uuid
class KeyVisitCreate(BaseModel):
customer_id: uuid.UUID
urgency_level: str = "一般" # 重要/一般/紧急
description: str = ""
progress_status: str = "未开始"
planned_date: str = ""
planned_visitor: str = ""
visit_target: str = ""
class KeyVisitUpdate(BaseModel):
customer_id: Optional[uuid.UUID] = None
urgency_level: Optional[str] = None
description: Optional[str] = None
progress_status: Optional[str] = None
planned_date: Optional[str] = None
planned_visitor: Optional[str] = None
visit_target: Optional[str] = None
class KeyVisitOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
urgency_level: str
description: str
progress_status: str
planned_date: str
planned_visitor: str
visit_target: str
manager_id: uuid.UUID
customer_name: Optional[str] = None
manager_name: Optional[str] = None
model_config = {"from_attributes": True}
+36
View File
@@ -0,0 +1,36 @@
from pydantic import BaseModel
from typing import Optional
import uuid
class MiniBusinessCreate(BaseModel):
customer_id: uuid.UUID
product_type: str = ""
amount: str = ""
follow_up_detail: str = ""
status: str = "跟进中"
expected_revenue_date: str = ""
class MiniBusinessUpdate(BaseModel):
customer_id: Optional[uuid.UUID] = None
product_type: Optional[str] = None
amount: Optional[str] = None
follow_up_detail: Optional[str] = None
status: Optional[str] = None
expected_revenue_date: Optional[str] = None
class MiniBusinessOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
product_type: str
amount: str
follow_up_detail: str
status: str
manager_id: uuid.UUID
expected_revenue_date: str
customer_name: Optional[str] = None
manager_name: Optional[str] = None
model_config = {"from_attributes": True}
+25
View File
@@ -0,0 +1,25 @@
from pydantic import BaseModel
from typing import Optional
import uuid
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user_id: str
name: str
role: str
class WecomLoginRequest(BaseModel):
code: str
class CasdoorLoginRequest(BaseModel):
code: str
state: str
class WecomBindRequest(BaseModel):
casdoor_code: str
wecom_userid: Optional[str] = None
+60
View File
@@ -0,0 +1,60 @@
from pydantic import BaseModel, Field
from typing import Optional
import uuid
from datetime import date, datetime
class VisitCreate(BaseModel):
customer_id: uuid.UUID
visit_date: str # "YYYY-MM-DD"
visit_method: str = "上门"
time_range: str = ""
communication_content: str = ""
customer_demand: str = ""
companions: list[uuid.UUID] = []
photos: list[str] = []
class VisitUpdate(BaseModel):
customer_id: Optional[uuid.UUID] = None
visit_date: Optional[str] = None
visit_method: Optional[str] = None
time_range: Optional[str] = None
communication_content: Optional[str] = None
customer_demand: Optional[str] = None
companions: Optional[list[uuid.UUID]] = None
photos: Optional[list[str]] = None
class VisitOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
visit_date: date
visit_method: str
time_range: str
communication_content: str
customer_demand: str
companions: Optional[list[uuid.UUID]] = None
photos: Optional[list[str]] = None
manager_id: uuid.UUID
created_at: datetime
updated_at: datetime
# Joined fields
customer_name: Optional[str] = None
manager_name: Optional[str] = None
model_config = {"from_attributes": True}
class VisitListOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
visit_date: date
visit_method: str
time_range: str
manager_id: uuid.UUID
customer_name: Optional[str] = None
manager_name: Optional[str] = None
has_photos: bool = False
model_config = {"from_attributes": True}
+31
View File
@@ -0,0 +1,31 @@
from pydantic import BaseModel
from typing import Optional
import uuid
from datetime import date
class WorkPlanCreate(BaseModel):
customer_id: uuid.UUID
plan_content: str = ""
plan_date: str # "YYYY-MM-DD"
status: str = "计划中"
class WorkPlanUpdate(BaseModel):
customer_id: Optional[uuid.UUID] = None
plan_content: Optional[str] = None
plan_date: Optional[str] = None
status: Optional[str] = None
class WorkPlanOut(BaseModel):
id: uuid.UUID
customer_id: uuid.UUID
plan_content: str
plan_date: date
manager_id: uuid.UUID
status: str
customer_name: Optional[str] = None
manager_name: Optional[str] = None
model_config = {"from_attributes": True}