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:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View 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

View 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

View 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 # 上游系统店铺 IDBIGINT
site_name: str # 店铺名称
site_code: str | None = None # 可选简写ID6 位 3+3 格式)

View 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

View 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="新密码")

View 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

View File

@@ -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):

View File

@@ -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

View 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="是否隐藏")

View 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="确认时间")

View 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 causeUserEditRequest 需要同时携带角色+绑定字段 | SummaryUserEditRequest 扩展 assistant_id/staff_id 字段,支持角色+绑定合并提交 | VerifyPATCH /users/{id} 接受 assistantId/staffId 参数
- 2026-03-24 | Prompt: 审核弹窗头像昵称+排版优化 | Direct causeApplicationListItem 缺少 avatar_url | Summary新增 avatar_url 字段 | VerifyGET /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="新角色 codecoach/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")

View 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

View File

@@ -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="目标角色 codecoach/staff/site_admin/tenant_admin")
role_code: str = Field(..., description="目标角色 codecoach/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="球房ID6位字母/数字)")
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
# ── 店铺 ──────────────────────────────────────────────────

View File

@@ -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 SchemaBOARD-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

View File

@@ -104,3 +104,5 @@ class ChatStreamRequest(CamelModel):
chat_id: int
content: str
source_page: str | None = None
page_context: dict | None = None

View File

@@ -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

View File

@@ -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] = []

View File

@@ -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

View File

@@ -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):

View File

@@ -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]