feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
213
apps/backend/app/schemas/admin_ai.py
Normal file
213
apps/backend/app/schemas/admin_ai.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
管理端 — AI 监控后台 Pydantic Schema。
|
||||
|
||||
覆盖:Dashboard 总览、调度任务、调用记录、缓存失效、Token 预算、批量执行、告警管理。
|
||||
|
||||
需求: A1.1, A2.1, A4.1, A5.1, A6.1, A7.1, A8.1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# ── Dashboard ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class DailyTrend(BaseModel):
|
||||
"""近 7 天按日聚合趋势项。"""
|
||||
date: str # YYYY-MM-DD
|
||||
calls: int
|
||||
success_rate: float
|
||||
|
||||
|
||||
class AppDistItem(BaseModel):
|
||||
"""各 App 调用占比分布项。"""
|
||||
app_type: str
|
||||
count: int
|
||||
percentage: float
|
||||
|
||||
|
||||
class BudgetInfo(BaseModel):
|
||||
"""日/月 Token 预算进度。"""
|
||||
daily_used: int
|
||||
daily_limit: int
|
||||
daily_pct: float
|
||||
monthly_used: int
|
||||
monthly_limit: int
|
||||
monthly_pct: float
|
||||
|
||||
|
||||
class AlertItem(BaseModel):
|
||||
"""告警事件项(失败/超时/熔断)。"""
|
||||
id: int
|
||||
app_type: str
|
||||
status: str # failed / timeout / circuit_open
|
||||
alert_status: str | None # pending / acknowledged / ignored
|
||||
error_message: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class AppHealthItem(BaseModel):
|
||||
"""各 App 最近一次调用状态。"""
|
||||
app_type: str
|
||||
last_status: str | None
|
||||
last_call_at: str | None
|
||||
|
||||
|
||||
class DashboardResponse(BaseModel):
|
||||
"""Dashboard 总览统计响应。"""
|
||||
today_calls: int
|
||||
today_success_rate: float # 0.0 ~ 1.0
|
||||
today_tokens: int
|
||||
today_avg_latency_ms: float
|
||||
trend_7d: list[DailyTrend]
|
||||
app_distribution: list[AppDistItem]
|
||||
budget: BudgetInfo
|
||||
recent_alerts: list[AlertItem]
|
||||
app_health: list[AppHealthItem]
|
||||
|
||||
|
||||
# ── 调度任务 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class TriggerJobItem(BaseModel):
|
||||
"""调度任务列表项。"""
|
||||
id: int
|
||||
event_type: str
|
||||
member_id: int | None
|
||||
status: str
|
||||
app_chain: str | None
|
||||
is_forced: bool
|
||||
site_id: int
|
||||
started_at: str | None
|
||||
finished_at: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class TriggerJobListResponse(BaseModel):
|
||||
"""调度任务分页列表响应。"""
|
||||
items: list[TriggerJobItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
today_skipped_duplicates: int # 今日去重跳过数
|
||||
|
||||
|
||||
class TriggerJobDetailResponse(TriggerJobItem):
|
||||
"""调度任务详情响应(含 payload、error_message)。"""
|
||||
payload: dict | None
|
||||
error_message: str | None
|
||||
connector_type: str
|
||||
|
||||
|
||||
class RetryResponse(BaseModel):
|
||||
"""手动重跑响应。"""
|
||||
trigger_job_id: int
|
||||
status: str # "pending"
|
||||
|
||||
|
||||
# ── 调用记录 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class RunLogItem(BaseModel):
|
||||
"""调用记录列表项。"""
|
||||
id: int
|
||||
app_type: str
|
||||
trigger_type: str
|
||||
member_id: int | None
|
||||
tokens_used: int
|
||||
latency_ms: int | None
|
||||
status: str
|
||||
site_id: int
|
||||
created_at: str
|
||||
|
||||
|
||||
class RunLogListResponse(BaseModel):
|
||||
"""调用记录分页列表响应。"""
|
||||
items: list[RunLogItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class RunLogDetailResponse(RunLogItem):
|
||||
"""调用记录详情响应(含完整 prompt/response,不脱敏)。"""
|
||||
request_prompt: str | None
|
||||
response_text: str | None
|
||||
error_message: str | None
|
||||
session_id: str | None
|
||||
finished_at: str | None
|
||||
|
||||
|
||||
# ── 缓存失效 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class CacheInvalidateRequest(BaseModel):
|
||||
"""缓存失效请求(site_id 必填)。"""
|
||||
site_id: int
|
||||
app_type: str | None = None
|
||||
member_id: int | None = None
|
||||
|
||||
|
||||
class CacheInvalidateResponse(BaseModel):
|
||||
"""缓存失效响应。"""
|
||||
affected_count: int
|
||||
|
||||
|
||||
# ── Token 预算 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class BudgetResponse(BaseModel):
|
||||
"""Token 预算使用情况响应。"""
|
||||
daily_used: int
|
||||
daily_limit: int
|
||||
daily_pct: float
|
||||
monthly_used: int
|
||||
monthly_limit: int
|
||||
monthly_pct: float
|
||||
|
||||
|
||||
# ── 批量执行 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class BatchRunRequest(BaseModel):
|
||||
"""批量执行请求。"""
|
||||
app_types: list[str]
|
||||
member_ids: list[int]
|
||||
site_id: int
|
||||
|
||||
|
||||
class BatchRunEstimate(BaseModel):
|
||||
"""批量执行预估响应(不立即执行)。"""
|
||||
batch_id: str
|
||||
estimated_calls: int
|
||||
estimated_tokens: int
|
||||
|
||||
|
||||
class BatchRunConfirm(BaseModel):
|
||||
"""批量执行确认请求。"""
|
||||
batch_id: str
|
||||
|
||||
|
||||
class BatchRunConfirmResponse(BaseModel):
|
||||
"""批量执行确认响应。"""
|
||||
status: str # "started"
|
||||
|
||||
|
||||
# ── 告警 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class AlertListResponse(BaseModel):
|
||||
"""告警分页列表响应。"""
|
||||
items: list[AlertItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class AlertActionResponse(BaseModel):
|
||||
"""告警操作(确认/忽略)响应。"""
|
||||
id: int
|
||||
alert_status: str
|
||||
21
apps/backend/app/schemas/admin_db_health.py
Normal file
21
apps/backend/app/schemas/admin_db_health.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""管理端 — 数据库健康监控 Pydantic Schema。
|
||||
|
||||
需求: 6.1, 6.2
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DbHealthItem(BaseModel):
|
||||
"""单个数据库健康状态"""
|
||||
db_name: str
|
||||
status: Literal['connected', 'disconnected']
|
||||
active_connections: int | None = None
|
||||
idle_connections: int | None = None
|
||||
db_size_mb: float | None = None
|
||||
slow_query_count: int | None = None
|
||||
84
apps/backend/app/schemas/admin_registry.py
Normal file
84
apps/backend/app/schemas/admin_registry.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
管理端 — 注册体系 Pydantic Schema。
|
||||
|
||||
覆盖:租户列表、店铺列表、简写ID 管理、店铺同步。
|
||||
|
||||
需求: A2.1, A2.2, A2.4, A2.5
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
# ── 租户 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TenantItem(CamelModel):
|
||||
"""租户列表项(含连接器名称)。"""
|
||||
id: int
|
||||
tenant_id: int
|
||||
tenant_name: str | None = None
|
||||
connector_name: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
# ── 店铺 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class SiteItem(CamelModel):
|
||||
"""店铺列表项。"""
|
||||
id: int
|
||||
site_id: int
|
||||
site_name: str | None = None
|
||||
site_code: str | None = None
|
||||
site_label: str | None = None
|
||||
is_active: bool
|
||||
|
||||
|
||||
# ── 简写ID 管理 ──────────────────────────────────────────
|
||||
|
||||
|
||||
class UpdateSiteCodeRequest(CamelModel):
|
||||
"""设置/修改店铺简写ID 请求。"""
|
||||
new_code: str # 6 位,3+3 格式,统一大写
|
||||
|
||||
|
||||
class SiteCodeResult(CamelModel):
|
||||
"""简写ID 修改结果。"""
|
||||
site_id: int
|
||||
old_code: str | None = None
|
||||
new_code: str
|
||||
history_cleaned: bool # 旧 code 是否被清理
|
||||
|
||||
|
||||
class SiteCodeHistoryItem(CamelModel):
|
||||
"""简写ID 变更历史条目。"""
|
||||
id: int
|
||||
site_code: str
|
||||
is_current: bool
|
||||
created_at: datetime
|
||||
retired_at: datetime | None = None
|
||||
|
||||
|
||||
# ── 店铺同步 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class SiteSyncResult(CamelModel):
|
||||
"""店铺同步结果。"""
|
||||
inserted: int # 新增店铺数
|
||||
updated: int # 更新店铺数
|
||||
|
||||
|
||||
# ── 测试用:手动创建/删除店铺 ─────────────────────────────
|
||||
|
||||
|
||||
class CreateSiteRequest(CamelModel):
|
||||
"""手动创建店铺请求(测试功能)。"""
|
||||
tenant_id: int # 所属租户(biz.tenants.tenant_id,上游 BIGINT)
|
||||
site_id: int # 上游系统店铺 ID(BIGINT)
|
||||
site_name: str # 店铺名称
|
||||
site_code: str | None = None # 可选简写ID(6 位 3+3 格式)
|
||||
118
apps/backend/app/schemas/admin_task_engine.py
Normal file
118
apps/backend/app/schemas/admin_task_engine.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P18 任务引擎运营看板 — Pydantic v2 Schema
|
||||
|
||||
包含:转移日志、待审核任务、候选助教、参数管理等数据模型。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ---- 转移日志 ----
|
||||
|
||||
class TransferLogItem(BaseModel):
|
||||
id: int
|
||||
site_id: int
|
||||
site_name: str = ""
|
||||
member_id: int
|
||||
member_name: str = ""
|
||||
from_assistant_id: int
|
||||
from_assistant_name: str = ""
|
||||
to_assistant_id: int
|
||||
to_assistant_name: str = ""
|
||||
transfer_reason: str | None = None
|
||||
transfer_score: float | None = None
|
||||
guard_checks: dict | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class TransferLogPage(BaseModel):
|
||||
items: list[TransferLogItem]
|
||||
total: int
|
||||
|
||||
|
||||
# ---- 待审核任务 ----
|
||||
|
||||
class PendingReviewItem(BaseModel):
|
||||
id: int
|
||||
site_id: int
|
||||
site_name: str = ""
|
||||
member_id: int
|
||||
member_name: str = ""
|
||||
assistant_id: int
|
||||
assistant_name: str = ""
|
||||
task_type: str
|
||||
task_type_label: str = ""
|
||||
transfer_count: int = 0
|
||||
priority_score: float | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class PendingReviewPage(BaseModel):
|
||||
items: list[PendingReviewItem]
|
||||
total: int
|
||||
|
||||
|
||||
class CandidateAssistant(BaseModel):
|
||||
assistant_id: int
|
||||
assistant_name: str = ""
|
||||
rs_display: float = 0
|
||||
ms_display: float = 0
|
||||
ml_display: float = 0
|
||||
transfer_score: float = 0
|
||||
source: str = "pool"
|
||||
|
||||
|
||||
class CandidateListResponse(BaseModel):
|
||||
candidates: list[CandidateAssistant]
|
||||
|
||||
|
||||
class ReassignRequest(BaseModel):
|
||||
to_assistant_id: int
|
||||
|
||||
|
||||
class ReassignResponse(BaseModel):
|
||||
success: bool
|
||||
new_task_id: int | None = None
|
||||
|
||||
|
||||
class CloseRequest(BaseModel):
|
||||
reason: str = Field(..., min_length=1, max_length=500)
|
||||
|
||||
|
||||
class CloseResponse(BaseModel):
|
||||
success: bool
|
||||
|
||||
|
||||
# ---- 参数管理 ----
|
||||
|
||||
class ConfigParam(BaseModel):
|
||||
id: int
|
||||
site_id: int | None = None
|
||||
site_name: str | None = None
|
||||
param_key: str
|
||||
param_value: float
|
||||
description: str | None = None
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ConfigParamList(BaseModel):
|
||||
params: list[ConfigParam]
|
||||
|
||||
|
||||
class ConfigParamUpdate(BaseModel):
|
||||
param_value: float
|
||||
|
||||
|
||||
class ConfigParamCreate(BaseModel):
|
||||
site_id: int
|
||||
param_key: str = Field(..., max_length=64)
|
||||
param_value: float
|
||||
|
||||
|
||||
class ConfigParamResponse(BaseModel):
|
||||
success: bool
|
||||
id: int | None = None
|
||||
65
apps/backend/app/schemas/admin_tenant_admins.py
Normal file
65
apps/backend/app/schemas/admin_tenant_admins.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
管理端 — 租户管理员 CRUD Pydantic Schema。
|
||||
|
||||
覆盖:管理员列表、创建、编辑、重置密码。
|
||||
|
||||
需求: 14.1, 14.2, 14.4, 14.5
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
# ── 管理员列表 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TenantAdminListItem(CamelModel):
|
||||
"""租户管理员列表项。"""
|
||||
id: int
|
||||
username: str
|
||||
display_name: str | None = None
|
||||
tenant_id: int # 所属租户 ID(上游 BIGINT)
|
||||
tenant_name: str | None = None # 所属租户名称(JOIN biz.tenants)
|
||||
admin_type: str = "tenant_admin" # tenant_admin / site_admin
|
||||
managed_site_ids: list[int] = Field(default_factory=list)
|
||||
is_active: bool = True
|
||||
created_at: str | None = None
|
||||
last_login_at: str | None = None
|
||||
|
||||
|
||||
# ── 创建管理员 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TenantAdminCreateRequest(CamelModel):
|
||||
"""创建租户管理员请求。"""
|
||||
username: str = Field(..., min_length=1, max_length=100, description="用户名")
|
||||
password: str = Field(..., min_length=1, description="初始密码")
|
||||
display_name: str | None = Field(None, max_length=100, description="显示名称")
|
||||
# tenant_id 从 biz.tenants 选择(GET /api/admin/tenants 获取可选列表)
|
||||
tenant_id: int = Field(..., description="所属租户 ID(来源: biz.tenants)")
|
||||
managed_site_ids: list[int] = Field(..., min_length=1, description="管辖门店 ID 列表")
|
||||
|
||||
|
||||
# ── 编辑管理员 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TenantAdminEditRequest(CamelModel):
|
||||
"""编辑租户管理员请求(所有字段可选)。"""
|
||||
username: str | None = Field(None, min_length=1, max_length=100, description="用户名(需全局唯一)")
|
||||
display_name: str | None = Field(None, max_length=100, description="显示名称")
|
||||
managed_site_ids: list[int] | None = Field(None, description="管辖门店 ID 列表")
|
||||
is_active: bool | None = Field(None, description="账号状态")
|
||||
|
||||
|
||||
# ── 重置密码 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class ResetPasswordRequest(CamelModel):
|
||||
"""重置密码请求。"""
|
||||
new_password: str = Field(..., min_length=1, description="新密码")
|
||||
23
apps/backend/app/schemas/admin_triggers.py
Normal file
23
apps/backend/app/schemas/admin_triggers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""管理端 — 触发器统一视图 Pydantic Schema。
|
||||
|
||||
需求: 4.1, 4.2
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UnifiedTriggerItem(BaseModel):
|
||||
"""统一触发器视图项"""
|
||||
id: int
|
||||
name: str
|
||||
source: Literal['biz', 'ai', 'etl']
|
||||
trigger_condition: str
|
||||
status: str
|
||||
last_run_at: str | None = None
|
||||
next_run_at: str | None = None
|
||||
last_error: str | None = None
|
||||
@@ -12,8 +12,8 @@ from pydantic import BaseModel
|
||||
class CursorInfo(BaseModel):
|
||||
"""ETL 游标信息(单条任务的最后抓取状态)。"""
|
||||
task_code: str
|
||||
last_fetch_time: str | None = None
|
||||
record_count: int | None = None
|
||||
last_start: str | None = None
|
||||
last_end: str | None = None
|
||||
|
||||
|
||||
class RecentRun(BaseModel):
|
||||
|
||||
@@ -25,6 +25,13 @@ class ScheduleConfigSchema(BaseModel):
|
||||
end_date: str | None = None
|
||||
|
||||
|
||||
class MinRunIntervalItem(BaseModel):
|
||||
"""单个任务的最小执行间隔"""
|
||||
|
||||
value: int = 0
|
||||
unit: Literal["minutes", "hours", "days"] = "minutes"
|
||||
|
||||
|
||||
class CreateScheduleRequest(BaseModel):
|
||||
"""创建调度任务请求"""
|
||||
|
||||
@@ -33,6 +40,9 @@ class CreateScheduleRequest(BaseModel):
|
||||
task_config: dict[str, Any]
|
||||
schedule_config: ScheduleConfigSchema
|
||||
run_immediately: bool = False
|
||||
min_run_interval_value: int = 0 # 0 表示无限制(schedule 级别默认)
|
||||
min_run_interval_unit: Literal["minutes", "hours", "days"] = "minutes"
|
||||
min_run_intervals: dict[str, MinRunIntervalItem] = {} # per-task-code 间隔
|
||||
|
||||
|
||||
class UpdateScheduleRequest(BaseModel):
|
||||
@@ -42,6 +52,9 @@ class UpdateScheduleRequest(BaseModel):
|
||||
task_codes: list[str] | None = None
|
||||
task_config: dict[str, Any] | None = None
|
||||
schedule_config: ScheduleConfigSchema | None = None
|
||||
min_run_interval_value: int | None = None
|
||||
min_run_interval_unit: Literal["minutes", "hours", "days"] | None = None
|
||||
min_run_intervals: dict[str, MinRunIntervalItem] | None = None
|
||||
|
||||
|
||||
class ScheduleResponse(BaseModel):
|
||||
@@ -58,5 +71,9 @@ class ScheduleResponse(BaseModel):
|
||||
next_run_at: datetime | None = None
|
||||
run_count: int
|
||||
last_status: str | None = None
|
||||
min_run_interval_value: int = 0
|
||||
min_run_interval_unit: str = "minutes"
|
||||
last_success_at: datetime | None = None
|
||||
min_run_intervals: dict[str, Any] = {}
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
60
apps/backend/app/schemas/tenant_clues.py
Normal file
60
apps/backend/app/schemas/tenant_clues.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
租户管理后台 — 维客线索管理 Pydantic Schema。
|
||||
|
||||
覆盖:客户搜索结果、线索列表、线索编辑、线索隐藏/显示。
|
||||
|
||||
需求: 9.1, 10.1, 11.1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
class ClueCategory(str, Enum):
|
||||
"""线索大类枚举(6 值)。"""
|
||||
CUSTOMER_BASIC = "客户基础"
|
||||
CONSUMPTION_HABIT = "消费习惯"
|
||||
PLAY_PREFERENCE = "玩法偏好"
|
||||
PROMO_PREFERENCE = "促销偏好"
|
||||
SOCIAL_RELATION = "社交关系"
|
||||
IMPORTANT_FEEDBACK = "重要反馈"
|
||||
|
||||
|
||||
class CustomerSearchItem(CamelModel):
|
||||
"""客户搜索结果项。"""
|
||||
member_id: int
|
||||
nickname: str | None = None
|
||||
mobile_masked: str | None = None
|
||||
site_name: str | None = None
|
||||
site_id: int | None = None
|
||||
|
||||
|
||||
class ClueListItem(CamelModel):
|
||||
"""线索列表项。"""
|
||||
id: int
|
||||
category: str | None = None
|
||||
summary: str | None = None
|
||||
detail: str | None = None
|
||||
recorded_by_name: str | None = None
|
||||
source: str | None = None
|
||||
recorded_at: str | None = None
|
||||
is_hidden: bool = False
|
||||
|
||||
|
||||
class ClueEditRequest(CamelModel):
|
||||
"""线索编辑请求。"""
|
||||
category: ClueCategory = Field(..., description="线索大类(6 值枚举)")
|
||||
summary: str = Field(..., min_length=1, max_length=200, description="摘要(非空,≤200 字符)")
|
||||
detail: Optional[str] = Field(None, description="详情(可选)")
|
||||
|
||||
|
||||
class ClueVisibilityRequest(CamelModel):
|
||||
"""线索隐藏/显示请求。"""
|
||||
is_hidden: bool = Field(..., description="是否隐藏")
|
||||
134
apps/backend/app/schemas/tenant_excel.py
Normal file
134
apps/backend/app/schemas/tenant_excel.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
租户管理后台 — Excel 上传 Pydantic Schema。
|
||||
|
||||
覆盖:4 种模板行数据模型、校验结果、冲突 diff、确认请求、上传记录。
|
||||
|
||||
需求: 5.2, 7.2, 8.4
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
# ── 4 种模板行数据模型 ────────────────────────────────────
|
||||
|
||||
|
||||
class ExpenseRow(CamelModel):
|
||||
"""财务支出行数据。"""
|
||||
row_index: int = Field(..., description="行号(从 1 开始)")
|
||||
expense_month: str = Field(..., description="月份 YYYY-MM")
|
||||
category: str = Field(..., description="支出类别(8 值枚举)")
|
||||
amount: float = Field(..., description="金额(> 0,精度 2 位小数)")
|
||||
remark: str | None = Field(None, description="备注(可选,最长 500 字符)")
|
||||
|
||||
|
||||
class PlatformIncomeRow(CamelModel):
|
||||
"""团购收入行数据。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
income_month: str = Field(..., description="月份 YYYY-MM")
|
||||
platform_name: str = Field(..., description="平台名称")
|
||||
amount: float = Field(..., description="收入金额(> 0)")
|
||||
remark: str | None = Field(None, description="备注(可选,最长 500 字符)")
|
||||
|
||||
|
||||
class SalaryAdjRow(CamelModel):
|
||||
"""助教奖罚行数据。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
salary_month: str = Field(..., description="月份 YYYY-MM")
|
||||
assistant_name: str = Field(..., description="助教姓名")
|
||||
assistant_number: str = Field(..., description="助教编号")
|
||||
adjustment_type: str = Field(..., description="类型(扣款/奖金)")
|
||||
amount: float = Field(..., description="金额(> 0)")
|
||||
reason: str = Field(..., description="原因(非空,最长 200 字符)")
|
||||
assistant_id: int | None = Field(None, description="匹配到的助教 ID")
|
||||
|
||||
|
||||
class RechargeCommissionRow(CamelModel):
|
||||
"""充值业绩归属行数据。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
recharge_date: str = Field(..., description="充值日期 YYYY-MM-DD")
|
||||
member_name: str = Field(..., description="会员名称")
|
||||
recharge_amount: float = Field(..., description="充值金额(> 0)")
|
||||
assigned_assistant: str = Field(..., description="归属助教")
|
||||
reward_amount: float = Field(..., description="奖励金额(≥ 0)")
|
||||
assistant_id: int | None = Field(None, description="匹配到的助教 ID")
|
||||
|
||||
|
||||
# ── 校验错误/警告 ─────────────────────────────────────────
|
||||
|
||||
|
||||
class ValidationError(CamelModel):
|
||||
"""单行校验错误。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
column: str = Field(..., description="列名")
|
||||
message: str = Field(..., description="错误描述")
|
||||
|
||||
|
||||
class ValidationWarning(CamelModel):
|
||||
"""单行校验警告(如人员匹配失败)。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
column: str = Field(..., description="列名")
|
||||
message: str = Field(..., description="警告描述")
|
||||
|
||||
|
||||
class ValidationResult(CamelModel):
|
||||
"""校验结果。"""
|
||||
errors: list[ValidationError] = Field(default_factory=list, description="错误列表")
|
||||
warnings: list[ValidationWarning] = Field(default_factory=list, description="警告列表")
|
||||
passed_rows: list[dict] = Field(default_factory=list, description="通过校验的行数据")
|
||||
upload_id: int | None = Field(None, description="上传批次 ID(校验全部通过时创建)")
|
||||
|
||||
|
||||
# ── 冲突 diff ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class FieldDiff(CamelModel):
|
||||
"""单字段差异。"""
|
||||
field: str = Field(..., description="字段名")
|
||||
old_value: str | None = Field(None, description="旧值")
|
||||
new_value: str | None = Field(None, description="新值")
|
||||
|
||||
|
||||
class ConflictDiff(CamelModel):
|
||||
"""冲突行 diff。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
field_diffs: list[FieldDiff] = Field(default_factory=list, description="逐字段差异")
|
||||
|
||||
|
||||
# ── 确认请求 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class Resolution(CamelModel):
|
||||
"""单行冲突解决方案。"""
|
||||
row_index: int = Field(..., description="行号")
|
||||
action: Literal["replace", "keep"] = Field(..., description="操作:replace=替换/keep=保留")
|
||||
|
||||
|
||||
class ConfirmRequest(CamelModel):
|
||||
"""确认写入请求。"""
|
||||
upload_id: int = Field(..., description="上传批次 ID")
|
||||
resolutions: list[Resolution] = Field(default_factory=list, description="冲突解决方案列表")
|
||||
|
||||
|
||||
# ── 上传记录 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class UploadLogItem(CamelModel):
|
||||
"""上传记录列表项。"""
|
||||
id: int
|
||||
site_id: int
|
||||
upload_type: str = Field(..., description="模板类型")
|
||||
file_name: str = Field(..., description="原始文件名")
|
||||
uploaded_by: int = Field(..., description="上传人 ID")
|
||||
row_count: int = Field(0, description="数据行数")
|
||||
conflict_count: int = Field(0, description="冲突行数")
|
||||
resolved_count: int = Field(0, description="已解决冲突数")
|
||||
status: str = Field(..., description="状态:pending/confirmed/failed")
|
||||
created_at: str | None = Field(None, description="上传时间")
|
||||
confirmed_at: str | None = Field(None, description="确认时间")
|
||||
117
apps/backend/app/schemas/tenant_users.py
Normal file
117
apps/backend/app/schemas/tenant_users.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
租户管理后台 — 用户审核 + 用户管理 Pydantic Schema。
|
||||
|
||||
覆盖:申请列表、关联建议、审核通过/拒绝、用户列表、编辑、绑定、角色列表、人员候选。
|
||||
|
||||
需求: 3.2, 4.1
|
||||
|
||||
AI_CHANGELOG
|
||||
- 2026-03-23 17:00:00 | Prompt: P20260323-164500(审核弹窗改造)| Direct cause:新增 roles + site-staff 端点需要响应 Schema | Summary:新增 RoleItem(角色列表项)+ StaffCandidate(人员候选项,source 区分 assistant/staff)| Verify:后端 /roles 和 /site-staff 返回正确 JSON 结构
|
||||
- 2026-03-24 | Prompt: 用户管理绑定功能改造 | Direct cause:UserEditRequest 需要同时携带角色+绑定字段 | Summary:UserEditRequest 扩展 assistant_id/staff_id 字段,支持角色+绑定合并提交 | Verify:PATCH /users/{id} 接受 assistantId/staffId 参数
|
||||
- 2026-03-24 | Prompt: 审核弹窗头像昵称+排版优化 | Direct cause:ApplicationListItem 缺少 avatar_url | Summary:新增 avatar_url 字段 | Verify:GET /applications 返回 avatarUrl
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
# ── 用户申请审核 ──────────────────────────────────────────
|
||||
|
||||
|
||||
class ApplicationListItem(CamelModel):
|
||||
"""申请列表项。"""
|
||||
id: int
|
||||
user_id: int
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
phone: str | None = None
|
||||
site_code: str | None = None
|
||||
applied_role_text: str | None = None
|
||||
employee_number: str | None = None
|
||||
created_at: str | None = None
|
||||
status: str # pending / approved / rejected
|
||||
|
||||
|
||||
class MatchSuggestion(CamelModel):
|
||||
"""关联匹配建议。"""
|
||||
assistant_id: int | None = None
|
||||
staff_id: int | None = None
|
||||
name: str
|
||||
number: str | None = None
|
||||
source_table: str # v_dim_assistant / v_dim_staff
|
||||
|
||||
|
||||
class ApproveRequest(CamelModel):
|
||||
"""审核通过请求。"""
|
||||
role: str = Field(..., min_length=1, description="分配角色(coach/staff/head_coach/manager)")
|
||||
assistant_id: int | None = Field(None, description="关联助教 ID")
|
||||
staff_id: int | None = Field(None, description="关联员工 ID")
|
||||
|
||||
|
||||
class RejectRequest(CamelModel):
|
||||
"""审核拒绝请求。"""
|
||||
reason: str = Field(..., min_length=1, description="拒绝原因")
|
||||
|
||||
|
||||
# ── 用户管理 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class UserListItem(CamelModel):
|
||||
"""用户列表项。"""
|
||||
id: int
|
||||
nickname: str | None = None
|
||||
role: str | None = None # 角色中文名(显示用)
|
||||
role_code: str | None = None # 角色 code(提交用)
|
||||
assistant_id: int | None = None # 当前绑定的助教 ID
|
||||
staff_id: int | None = None # 当前绑定的员工 ID
|
||||
assistant_name: str | None = None
|
||||
site_name: str | None = None
|
||||
site_id: int | None = None
|
||||
status: str # approved / disabled
|
||||
|
||||
|
||||
class UserEditRequest(CamelModel):
|
||||
"""用户编辑请求(合并角色+绑定)。
|
||||
|
||||
角色与绑定互斥:coach 只能绑 assistant_id,其他角色只能绑 staff_id。
|
||||
换角色时后端自动清除旧绑定。staffBinding="none" 表示解绑。
|
||||
"""
|
||||
role: str | None = Field(None, description="新角色 code(coach/staff/head_coach/manager)")
|
||||
site_id: int | None = Field(None, description="新门店 ID")
|
||||
assistant_id: int | None = Field(None, description="关联助教 ID(仅 coach 角色)")
|
||||
staff_id: int | None = Field(None, description="关联员工 ID(仅非 coach 角色)")
|
||||
# CHANGE 2026-03-23 | 移除 status 字段:租户不能禁用用户,只能移除店铺关系
|
||||
# CHANGE 2026-03-24 | 合并绑定字段:角色+绑定同一请求提交,换角色自动清除旧绑定
|
||||
|
||||
|
||||
class UserBindingRequest(CamelModel):
|
||||
"""用户绑定修改请求。"""
|
||||
assistant_id: int | None = Field(None, description="关联助教 ID")
|
||||
staff_id: int | None = Field(None, description="关联员工 ID")
|
||||
|
||||
|
||||
# ── 角色 + 人员候选 ──────────────────────────────────────
|
||||
# [CHANGE P20260323-164500] intent: 审核弹窗角色动态化 + 人员联动所需的响应 Schema
|
||||
# assumptions: StaffCandidate.source 区分 assistant/staff,前端据此构造 staffBinding 值
|
||||
|
||||
|
||||
class RoleItem(CamelModel):
|
||||
"""角色列表项(从 auth.roles 动态读取)。"""
|
||||
id: int
|
||||
code: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class StaffCandidate(CamelModel):
|
||||
"""人员候选项(审核弹窗关联下拉用)。"""
|
||||
id: int = Field(..., description="assistant_id 或 staff_id")
|
||||
identity_label: str | None = Field(None, description="身份角色(level / staff_identity 的原始值)")
|
||||
name: str = Field(..., description="姓名")
|
||||
mobile: str | None = Field(None, description="手机号")
|
||||
entry_time: str | None = Field(None, description="入职时间")
|
||||
source: str = Field(..., description="assistant / staff")
|
||||
41
apps/backend/app/schemas/trigger_jobs.py
Normal file
41
apps/backend/app/schemas/trigger_jobs.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""定时任务管理 — Pydantic 响应模型"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Self
|
||||
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
|
||||
class TriggerJobItem(BaseModel):
|
||||
"""单个定时任务信息"""
|
||||
id: int
|
||||
job_type: str
|
||||
job_name: str
|
||||
trigger_condition: str
|
||||
trigger_config: dict | None = None
|
||||
last_run_at: str | None = None
|
||||
next_run_at: str | None = None
|
||||
status: str
|
||||
description: str | None = None
|
||||
last_error: str | None = None
|
||||
created_at: str | None = None
|
||||
|
||||
|
||||
class RunJobResult(BaseModel):
|
||||
"""手动执行结果"""
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class UpdateTriggerConfigRequest(BaseModel):
|
||||
"""触发器配置编辑请求(部分更新)"""
|
||||
cron_expression: str | None = None # 5 字段 cron 表达式
|
||||
interval_seconds: int | None = None # 间隔秒数,>= 1
|
||||
|
||||
@model_validator(mode='after')
|
||||
def at_least_one_field(self) -> Self:
|
||||
if self.cron_expression is None and self.interval_seconds is None:
|
||||
raise ValueError('至少提供 cron_expression 或 interval_seconds 之一')
|
||||
return self
|
||||
@@ -1,3 +1,8 @@
|
||||
# AI_CHANGELOG
|
||||
# | 日期 | Prompt | 变更 |
|
||||
# |------|--------|------|
|
||||
# | 2026-03-23 | 角色路由+页面权限守卫 | WxLoginResponse 和 UserStatusResponse 增加 role 字段 |
|
||||
|
||||
"""
|
||||
小程序认证相关 Pydantic 模型。
|
||||
|
||||
@@ -25,6 +30,8 @@ class WxLoginResponse(CamelModel):
|
||||
token_type: str = "bearer"
|
||||
user_status: str # pending / approved / rejected / disabled
|
||||
user_id: int
|
||||
# CHANGE 2026-03-23 | 角色路由:登录时返回角色 code
|
||||
role: str | None = None
|
||||
|
||||
|
||||
class DevLoginRequest(CamelModel):
|
||||
@@ -37,7 +44,7 @@ class DevLoginRequest(CamelModel):
|
||||
|
||||
class DevSwitchRoleRequest(CamelModel):
|
||||
"""切换角色请求。替换当前用户在当前门店下的所有角色为指定角色。"""
|
||||
role_code: str = Field(..., description="目标角色 code(coach/staff/site_admin/tenant_admin)")
|
||||
role_code: str = Field(..., description="目标角色 code(coach/staff/head_coach/manager)")
|
||||
|
||||
|
||||
class DevSwitchStatusRequest(CamelModel):
|
||||
@@ -71,7 +78,8 @@ class DevLoginRequest(CamelModel):
|
||||
|
||||
class ApplicationRequest(CamelModel):
|
||||
"""用户申请提交请求。"""
|
||||
site_code: str = Field(..., pattern=r"^[A-Za-z]{2}\d{3}$", description="球房ID")
|
||||
# CHANGE 2026-03-23 | 球房ID 改为 6 位字母/数字,大小写不敏感
|
||||
site_code: str = Field(..., pattern=r"^[A-Za-z0-9]{6}$", description="球房ID(6位字母/数字)")
|
||||
applied_role_text: str = Field(..., min_length=1, max_length=100, description="申请身份")
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
|
||||
employee_number: str | None = Field(None, max_length=50, description="员工编号")
|
||||
@@ -89,6 +97,30 @@ class ApplicationResponse(CamelModel):
|
||||
reviewed_at: str | None = None
|
||||
|
||||
|
||||
class LatestApplicationDetail(CamelModel):
|
||||
"""最新申请详情(含 phone/employee_number,用于前端展示和预填)。"""
|
||||
id: int
|
||||
site_code: str
|
||||
applied_role_text: str
|
||||
phone: str
|
||||
employee_number: str | None = None
|
||||
status: str
|
||||
review_note: str | None = None
|
||||
created_at: str
|
||||
reviewed_at: str | None = None
|
||||
|
||||
|
||||
class CancelApplicationResponse(CamelModel):
|
||||
"""取消申请响应(返回被取消申请的信息,用于预填重新申请表单)。"""
|
||||
id: int
|
||||
site_code: str
|
||||
applied_role_text: str
|
||||
phone: str
|
||||
employee_number: str | None = None
|
||||
status: str
|
||||
created_at: str
|
||||
|
||||
|
||||
# ── 用户状态 ──────────────────────────────────────────────
|
||||
|
||||
class UserStatusResponse(CamelModel):
|
||||
@@ -96,7 +128,18 @@ class UserStatusResponse(CamelModel):
|
||||
user_id: int
|
||||
status: str
|
||||
nickname: str | None = None
|
||||
# CHANGE 2026-03-24 | 头像:从 auth.users.avatar_url 读取
|
||||
avatar_url: str | None = None
|
||||
# CHANGE 2026-03-23 | 角色路由:返回用户在当前门店下的角色 code
|
||||
role: str | None = None
|
||||
# CHANGE 2026-03-27 | 权限改造 W2:返回权限码列表,前端据此动态控制页面/tab 可见性
|
||||
permissions: list[str] = []
|
||||
# CHANGE 2026-03-23 | banner 数据修复:补充门店名和助教等级
|
||||
store_name: str | None = None
|
||||
coach_level: str | None = None
|
||||
applications: list[ApplicationResponse] = []
|
||||
# CHANGE 2026-03-23 | 审核流程增强:最新申请详情(含 phone/employee_number)
|
||||
latest_application: LatestApplicationDetail | None = None
|
||||
|
||||
|
||||
# ── 店铺 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# - 2026-03-20 | Prompt: R3 项目类型筛选接口重建 | SkillFilterEnum 和 ProjectFilterEnum
|
||||
# 枚举值从 all/chinese/snooker/mahjong/karaoke 改为 ALL/BILLIARD/SNOOKER/MAHJONG/KTV,
|
||||
# 与 dws.cfg_area_category.category_code 一致,消除前后端映射层。
|
||||
# - 2026-03-27 | Prompt: board-finance-integration T2.4 | AreaFilterEnum 从 7 项重建为 9 项
|
||||
# (新增 vip/snooker/ktv,移除 teamBuilding),与区域筛选对照表一致。
|
||||
|
||||
"""三看板接口 Pydantic Schema(BOARD-1/2/3 请求参数枚举 + 响应模型)。"""
|
||||
|
||||
@@ -84,13 +86,17 @@ class FinanceTimeEnum(str, Enum):
|
||||
|
||||
class AreaFilterEnum(str, Enum):
|
||||
"""BOARD-3 区域筛选。"""
|
||||
# CHANGE 2026-03-27 | board-finance-integration T2.4 | 枚举从 7 项重建为 9 项,
|
||||
# 与区域筛选对照表一致(all/hall/hallA-C/vip/snooker/mahjong/ktv)
|
||||
all = "all"
|
||||
hall = "hall"
|
||||
hallA = "hallA"
|
||||
hallB = "hallB"
|
||||
hallC = "hallC"
|
||||
vip = "vip"
|
||||
snooker = "snooker"
|
||||
mahjong = "mahjong"
|
||||
teamBuilding = "teamBuilding"
|
||||
ktv = "ktv"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -137,6 +143,9 @@ class CoachBoardItem(CamelModel):
|
||||
|
||||
class CoachBoardResponse(CamelModel):
|
||||
items: list[CoachBoardItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
dim_type: str # perf/salary/sv/task
|
||||
|
||||
|
||||
@@ -261,10 +270,10 @@ class OverviewPanel(CamelModel):
|
||||
discount: float # 负值
|
||||
discount_rate: float
|
||||
confirmed_revenue: float
|
||||
cash_in: float
|
||||
cash_out: float
|
||||
cash_balance: float
|
||||
balance_rate: float
|
||||
cash_in: float | None = None
|
||||
cash_out: float | None = None
|
||||
cash_balance: float | None = None
|
||||
balance_rate: float | None = None
|
||||
# occurrence 环比
|
||||
occurrence_compare: str | None = None
|
||||
occurrence_down: bool | None = None
|
||||
@@ -340,6 +349,10 @@ class RechargePanel(CamelModel):
|
||||
card_balance_compare: str | None = None
|
||||
card_balance_down: bool | None = None
|
||||
card_balance_flat: bool | None = None
|
||||
# 全类别会员卡余额合计环比
|
||||
all_card_balance_compare: str | None = None
|
||||
all_card_balance_down: bool | None = None
|
||||
all_card_balance_flat: bool | None = None
|
||||
|
||||
|
||||
class RevenueStructureRow(CamelModel):
|
||||
@@ -355,32 +368,53 @@ class RevenueStructureRow(CamelModel):
|
||||
|
||||
class RevenueItem(CamelModel):
|
||||
label: str
|
||||
desc: str | None = None
|
||||
amount: float
|
||||
compare: str | None = None
|
||||
|
||||
|
||||
class ChannelItem(CamelModel):
|
||||
label: str
|
||||
desc: str | None = None
|
||||
amount: float
|
||||
compare: str | None = None
|
||||
|
||||
|
||||
class RevenuePanel(CamelModel):
|
||||
structure_rows: list[RevenueStructureRow]
|
||||
price_items: list[RevenueItem] # 4 项
|
||||
price_items: list[RevenueItem]
|
||||
total_occurrence: float
|
||||
discount_items: list[RevenueItem] # 4 项
|
||||
total_occurrence_compare: str | None = None
|
||||
total_occurrence_down: bool | None = None
|
||||
total_occurrence_flat: bool | None = None
|
||||
discount_items: list[RevenueItem]
|
||||
# CHANGE 2026-03-28 | board-finance-phase2 bugfix | 优惠总计供前端展示
|
||||
discount_total: float = 0.0
|
||||
discount_total_compare: str | None = None
|
||||
discount_total_down: bool | None = None
|
||||
discount_total_flat: bool | None = None
|
||||
confirmed_total: float
|
||||
channel_items: list[ChannelItem] # 3 项
|
||||
confirmed_total_compare: str | None = None
|
||||
confirmed_total_down: bool | None = None
|
||||
confirmed_total_flat: bool | None = None
|
||||
channel_items: list[ChannelItem]
|
||||
|
||||
|
||||
class CashflowItem(CamelModel):
|
||||
label: str
|
||||
desc: str | None = None
|
||||
amount: float
|
||||
compare: str | None = None
|
||||
down: bool | None = None
|
||||
|
||||
|
||||
class CashflowPanel(CamelModel):
|
||||
consume_items: list[CashflowItem] # 3 项
|
||||
recharge_items: list[CashflowItem] # 1 项
|
||||
total: float
|
||||
total_compare: str | None = None
|
||||
total_down: bool | None = None
|
||||
total_flat: bool | None = None
|
||||
|
||||
|
||||
class ExpenseItem(CamelModel):
|
||||
@@ -437,6 +471,6 @@ class FinanceBoardResponse(CamelModel):
|
||||
overview: OverviewPanel
|
||||
recharge: RechargePanel | None # area≠all 时为 null
|
||||
revenue: RevenuePanel
|
||||
cashflow: CashflowPanel
|
||||
expense: ExpensePanel
|
||||
cashflow: CashflowPanel | None # area≠all 时为 null
|
||||
expense: ExpensePanel | None # area≠all 时为 null
|
||||
coach_analysis: CoachAnalysisPanel
|
||||
|
||||
@@ -104,3 +104,5 @@ class ChatStreamRequest(CamelModel):
|
||||
|
||||
chat_id: int
|
||||
content: str
|
||||
source_page: str | None = None
|
||||
page_context: dict | None = None
|
||||
|
||||
@@ -53,8 +53,9 @@ class TopCustomer(CamelModel):
|
||||
score: str
|
||||
score_color: str
|
||||
service_count: int
|
||||
balance: str
|
||||
consume: str
|
||||
# CHANGE 2026-03-29 | str → float:后端返回原始数字,前端 WXS 格式化(避免 NaN)
|
||||
balance: float
|
||||
consume: float
|
||||
|
||||
|
||||
class CoachServiceRecord(CamelModel):
|
||||
@@ -66,7 +67,8 @@ class CoachServiceRecord(CamelModel):
|
||||
type_class: str
|
||||
table: str | None = None
|
||||
duration: str
|
||||
income: str
|
||||
# CHANGE 2026-03-29 | str → float:后端返回原始数字,前端 WXS 格式化(避免 NaN)
|
||||
income: float
|
||||
date: str
|
||||
perf_hours: str | None = None
|
||||
|
||||
@@ -74,9 +76,10 @@ class CoachServiceRecord(CamelModel):
|
||||
class HistoryMonth(CamelModel):
|
||||
month: str
|
||||
estimated: bool
|
||||
customers: str
|
||||
hours: str
|
||||
salary: str
|
||||
# CHANGE 2026-03-29 | str → int/float:后端返回原始数字,前端 WXS 格式化(避免 NaN)
|
||||
customers: int
|
||||
hours: float
|
||||
salary: float
|
||||
callback_done: int
|
||||
recall_done: int
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class CoachTask(CamelModel):
|
||||
name: str
|
||||
level: str # star / senior / middle / junior
|
||||
level_color: str
|
||||
heart_score: float = 0.0 # CHANGE 2026-03-29 | RSI 关系指数,用于爱心标识
|
||||
task_type: str
|
||||
task_color: str
|
||||
bg_class: str
|
||||
@@ -32,10 +33,10 @@ class CoachTask(CamelModel):
|
||||
metrics: list[MetricItem] = []
|
||||
|
||||
class FavoriteCoach(CamelModel):
|
||||
# CHANGE 2026-03-20 | M4 修复: emoji 注释与 P6 权威定义对齐(4 级映射)
|
||||
# intent: 注释应反映 compute_heart_icon() 的实际 4 级映射(💖🧡💛💙)
|
||||
emoji: str # 💖 / 🧡 / 💛 / 💙
|
||||
emoji: str
|
||||
name: str
|
||||
heart_score: float = 0.0
|
||||
level: str = ""
|
||||
relation_index: str
|
||||
index_color: str
|
||||
bg_class: str
|
||||
@@ -46,7 +47,7 @@ class CoachServiceItem(CamelModel):
|
||||
level: str
|
||||
level_color: str
|
||||
course_type: str # "基础课" / "激励课"
|
||||
hours: float
|
||||
hours: str # "2.5h" 格式
|
||||
perf_hours: float | None = None
|
||||
fee: float
|
||||
|
||||
@@ -57,15 +58,15 @@ class ConsumptionRecord(CamelModel):
|
||||
table_name: str | None = None
|
||||
start_time: str | None = None
|
||||
end_time: str | None = None
|
||||
duration: int | None = None
|
||||
duration: str | None = None
|
||||
table_fee: float | None = None
|
||||
table_orig_price: float | None = None
|
||||
coaches: list[CoachServiceItem] = []
|
||||
food_amount: float | None = None
|
||||
food_orig_price: float | None = None
|
||||
total_amount: float
|
||||
total_orig_price: float
|
||||
pay_method: str
|
||||
total_orig_price: float | None = None
|
||||
pay_method: str | None = None
|
||||
recharge_amount: float | None = None
|
||||
|
||||
class RetentionClue(CamelModel):
|
||||
@@ -126,5 +127,25 @@ class CustomerRecordsResponse(CamelModel):
|
||||
total_service_count: int
|
||||
month_count: int
|
||||
month_hours: float
|
||||
month_income: float = 0.0
|
||||
records: list[ServiceRecordItem] = []
|
||||
has_more: bool = False
|
||||
|
||||
|
||||
class CustomerConsumptionRecordsResponse(CamelModel):
|
||||
"""CUST-3 响应:客户消费记录(按月)。"""
|
||||
# Banner
|
||||
id: int
|
||||
name: str
|
||||
phone: str
|
||||
phone_full: str
|
||||
balance: float | None = None
|
||||
consumption_60d: float | None = None
|
||||
ideal_interval: int | None = None
|
||||
days_since_visit: int | None = None
|
||||
# 月度汇总
|
||||
visit_count: int = 0
|
||||
consume_total: float = 0.0
|
||||
recharge_total: float = 0.0
|
||||
# 消费记录
|
||||
records: list[ConsumptionRecord] = []
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.schemas.base import CamelModel
|
||||
|
||||
|
||||
class NoteCreateRequest(CamelModel):
|
||||
"""创建备注请求(含手动评分:再次服务意愿 + 再来店可能性,各 1-5)。"""
|
||||
"""创建备注请求(含手动评分:再次服务意愿 + 再来店可能性,各 1-5;备注星星评分 1-5)。"""
|
||||
|
||||
target_type: str = Field(default="member")
|
||||
target_id: int
|
||||
@@ -20,6 +20,7 @@ class NoteCreateRequest(CamelModel):
|
||||
task_id: int | None = None
|
||||
rating_service_willingness: int | None = Field(None, ge=1, le=5, description="再次服务意愿(1-5)")
|
||||
rating_revisit_likelihood: int | None = Field(None, ge=1, le=5, description="再来店可能性(1-5)")
|
||||
score: int | None = Field(None, ge=1, le=5, description="备注星星评分(1-5)")
|
||||
|
||||
|
||||
class NoteOut(CamelModel):
|
||||
@@ -30,6 +31,7 @@ class NoteOut(CamelModel):
|
||||
content: str
|
||||
rating_service_willingness: int | None
|
||||
rating_revisit_likelihood: int | None
|
||||
score: int | None
|
||||
ai_score: int | None
|
||||
ai_analysis: str | None
|
||||
task_id: int | None
|
||||
|
||||
@@ -18,12 +18,12 @@ class DateGroupRecord(CamelModel):
|
||||
"""按日期分组的单条服务记录。"""
|
||||
|
||||
customer_name: str
|
||||
member_id: int | None = None # 前端用于计算头像颜色
|
||||
avatar_char: str | None = None # PERF-1 返回,PERF-2 不返回
|
||||
avatar_color: str | None = None # PERF-1 返回,PERF-2 不返回
|
||||
heart_score: float | None = None # RS 分数,前端用于 heart-icon 组件
|
||||
time_range: str
|
||||
hours: str
|
||||
course_type: str
|
||||
course_type_class: str # 'basic' | 'vip' | 'tip'
|
||||
location: str
|
||||
income: str
|
||||
|
||||
@@ -62,8 +62,9 @@ class CustomerSummary(CamelModel):
|
||||
"""客户摘要(新客/常客基类)。"""
|
||||
|
||||
name: str
|
||||
member_id: int | None = None # 前端用于计算头像颜色
|
||||
avatar_char: str
|
||||
avatar_color: str
|
||||
heart_score: float | None = None # RS 分数
|
||||
|
||||
|
||||
class NewCustomer(CustomerSummary):
|
||||
|
||||
@@ -80,6 +80,11 @@ class TaskItem(CamelModel):
|
||||
last_visit_days: int | None = None
|
||||
balance: float | None = None
|
||||
ai_suggestion: str | None = None
|
||||
expected_days: int | None = None
|
||||
ideal_interval_days: int | None = None
|
||||
# CHANGE 2026-03-27 | 近60天服务汇总(口径同 task-detail serviceSummary)
|
||||
recent60d_hours: float = 0.0
|
||||
recent60d_income: float = 0.0
|
||||
|
||||
|
||||
class TaskListResponse(CamelModel):
|
||||
@@ -150,6 +155,7 @@ class TaskDetailResponse(CamelModel):
|
||||
# 基础信息
|
||||
id: int
|
||||
customer_name: str
|
||||
customer_phone: str | None = None
|
||||
customer_avatar: str
|
||||
task_type: str
|
||||
task_type_label: str
|
||||
@@ -160,6 +166,7 @@ class TaskDetailResponse(CamelModel):
|
||||
has_note: bool
|
||||
status: str
|
||||
customer_id: int
|
||||
balance: float | None = None
|
||||
# 扩展模块
|
||||
retention_clues: list[RetentionClue]
|
||||
talking_points: list[str]
|
||||
|
||||
Reference in New Issue
Block a user