190 lines
56 KiB
JSON
190 lines
56 KiB
JSON
{
|
||
"built_at": "2026-03-19T21:59:42.597337+08:00",
|
||
"prompt_id": "P20260319-215752",
|
||
"prompt_at": "2026-03-19T21:57:52.794902+08:00",
|
||
"audit_required": true,
|
||
"db_docs_required": true,
|
||
"reasons": [
|
||
"root-file",
|
||
"dir:admin-web",
|
||
"dir:backend",
|
||
"dir:miniprogram",
|
||
"dir:db",
|
||
"db-schema-change"
|
||
],
|
||
"changed_files": [
|
||
"AI_CHANGELOG.md",
|
||
"apps/DEMO-miniprogram/",
|
||
"apps/DEMO-miniprogram/miniprogram/app.json",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.json",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.ts",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.wxml",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.wxss",
|
||
"apps/XCX-TEST/",
|
||
"apps/admin-web/src/api/client.ts",
|
||
"apps/backend/README.md",
|
||
"apps/backend/app/main.py",
|
||
"apps/backend/app/middleware/response_wrapper.py",
|
||
"apps/backend/app/routers/xcx_board.py",
|
||
"apps/backend/app/routers/xcx_coaches.py",
|
||
"apps/backend/app/routers/xcx_config.py",
|
||
"apps/backend/app/routers/xcx_customers.py",
|
||
"apps/backend/app/routers/xcx_performance.py",
|
||
"apps/backend/app/routers/xcx_tasks.py",
|
||
"apps/backend/app/schemas/base.py",
|
||
"apps/backend/app/schemas/xcx_auth.py",
|
||
"apps/backend/app/schemas/xcx_board.py",
|
||
"apps/backend/app/schemas/xcx_coaches.py",
|
||
"apps/backend/app/schemas/xcx_config.py",
|
||
"apps/backend/app/schemas/xcx_customers.py",
|
||
"apps/backend/app/schemas/xcx_notes.py",
|
||
"apps/backend/app/schemas/xcx_performance.py",
|
||
"apps/backend/app/schemas/xcx_tasks.py",
|
||
"apps/backend/app/services/board_service.py",
|
||
"apps/backend/app/services/coach_service.py",
|
||
"apps/backend/app/services/customer_service.py",
|
||
"apps/backend/app/services/fdw_queries.py",
|
||
"apps/backend/app/services/performance_service.py",
|
||
"apps/backend/app/services/task_manager.py",
|
||
"apps/backend/docs/API-REFERENCE.md",
|
||
"apps/etl/connectors/feiqiu/.env",
|
||
"apps/etl/connectors/feiqiu/docs/api-reference/endpoints/member_balance_changes.md",
|
||
"apps/etl/connectors/feiqiu/docs/api-reference/endpoints/member_stored_value_cards.md",
|
||
"apps/etl/connectors/feiqiu/docs/api-reference/summary/member_balance_changes.md",
|
||
"apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_recharge_summary.md",
|
||
"apps/etl/connectors/feiqiu/docs/etl_tasks/dws_tasks.md",
|
||
"apps/miniprogram/doc/useless/",
|
||
"apps/miniprogram/miniprogram/components/heart-icon/heart-icon.ts",
|
||
"apps/miniprogram/miniprogram/components/note-modal/note-modal.ts",
|
||
"apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts",
|
||
"apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts",
|
||
"apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts",
|
||
"apps/miniprogram/miniprogram/pages/chat/chat.ts",
|
||
"apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts",
|
||
"apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml",
|
||
"apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts",
|
||
"apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts",
|
||
"apps/miniprogram/miniprogram/pages/notes/notes.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance/performance.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance/performance.wxml"
|
||
],
|
||
"high_risk_files": [
|
||
"apps/admin-web/src/api/client.ts",
|
||
"apps/backend/app/main.py",
|
||
"apps/backend/app/middleware/response_wrapper.py",
|
||
"apps/backend/app/routers/xcx_board.py",
|
||
"apps/backend/app/routers/xcx_coaches.py",
|
||
"apps/backend/app/routers/xcx_config.py",
|
||
"apps/backend/app/routers/xcx_customers.py",
|
||
"apps/backend/app/routers/xcx_performance.py",
|
||
"apps/backend/app/routers/xcx_tasks.py",
|
||
"apps/backend/app/schemas/base.py",
|
||
"apps/backend/app/schemas/xcx_auth.py",
|
||
"apps/backend/app/schemas/xcx_board.py",
|
||
"apps/backend/app/schemas/xcx_coaches.py",
|
||
"apps/backend/app/schemas/xcx_config.py",
|
||
"apps/backend/app/schemas/xcx_customers.py",
|
||
"apps/backend/app/schemas/xcx_notes.py",
|
||
"apps/backend/app/schemas/xcx_performance.py",
|
||
"apps/backend/app/schemas/xcx_tasks.py",
|
||
"apps/backend/app/services/board_service.py",
|
||
"apps/backend/app/services/coach_service.py",
|
||
"apps/backend/app/services/customer_service.py",
|
||
"apps/backend/app/services/fdw_queries.py",
|
||
"apps/backend/app/services/performance_service.py",
|
||
"apps/backend/app/services/task_manager.py",
|
||
"apps/miniprogram/doc/useless/",
|
||
"apps/miniprogram/miniprogram/components/heart-icon/heart-icon.ts",
|
||
"apps/miniprogram/miniprogram/components/note-modal/note-modal.ts",
|
||
"apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts",
|
||
"apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts",
|
||
"apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts",
|
||
"apps/miniprogram/miniprogram/pages/chat/chat.ts",
|
||
"apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts",
|
||
"apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml",
|
||
"apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts",
|
||
"apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts",
|
||
"apps/miniprogram/miniprogram/pages/notes/notes.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance/performance.ts",
|
||
"apps/miniprogram/miniprogram/pages/performance/performance.wxml"
|
||
],
|
||
"session_diff": {
|
||
"added": [
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.json",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.ts",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.wxml",
|
||
"apps/DEMO-miniprogram/miniprogram/pages/emoji-test/emoji-test.wxss",
|
||
"docs/audit/prompt_logs/prompt_log_20260319_215752.md",
|
||
"docs/audit/session_logs/2026-03/19/56_29ac958f_213923/main_01_a86c2213.md",
|
||
"docs/audit/session_logs/2026-03/19/56_29ac958f_213923/sub_01_a86c2213.md",
|
||
"docs/audit/session_logs/2026-03/19/56_29ac958f_213923/sub_02_a86c2213.md"
|
||
],
|
||
"modified": [
|
||
"apps/DEMO-miniprogram/miniprogram/app.json",
|
||
"docs/audit/session_logs/2026-02/11/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/11/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/12/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/12/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/13/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/13/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/14/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/14/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/15/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/15/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/16/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/16/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/17/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/17/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/18/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/18/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/19/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/19/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/20/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/20/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/21/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/21/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/22/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/22/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/23/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/23/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/24/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/24/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/25/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/25/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/26/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/26/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/27/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/27/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-02/28/_day_index.json",
|
||
"docs/audit/session_logs/2026-02/28/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/01/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/01/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/02/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/02/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/03/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/03/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/04/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/04/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/05/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/05/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/06/_day_index.json",
|
||
"docs/audit/session_logs/2026-03/06/_day_index_full.json",
|
||
"docs/audit/session_logs/2026-03/07/_day_index.json"
|
||
],
|
||
"deleted": []
|
||
},
|
||
"compliance": {
|
||
"code_without_docs": [],
|
||
"new_migration_sql": [],
|
||
"has_bd_manual": false,
|
||
"has_audit_record": false,
|
||
"has_ddl_baseline": false,
|
||
"api_changed": false,
|
||
"openapi_spec_stale": false
|
||
},
|
||
"diff_stat": ".kiro/hooks/cwd-guard-shell.kiro.hook | 8 +-\n .kiro/state/.audit_context.json | 200 +--\n .kiro/state/.audit_state.json | 98 +-\n .kiro/state/.compliance_state.json | 2 +-\n .kiro/state/.file_baseline.json | 2 +-\n .kiro/state/.last_prompt_id.json | 4 +-\n .kiro/steering/deprecated-objects.md | 25 -\n .kiro/steering/dwd-doc-authority.md | 25 +-\n .kiro/steering/export-paths.md | 50 +-\n .kiro/steering/project-overview.md | 13 +-\n apps/admin-web/src/api/client.ts | 10 +-\n apps/backend/README.md | 17 +-\n apps/backend/app/main.py | 25 +-\n apps/backend/app/routers/xcx_tasks.py | 47 +-\n apps/backend/app/schemas/xcx_auth.py | 53 +-\n apps/backend/app/schemas/xcx_notes.py | 14 +-\n apps/backend/app/schemas/xcx_tasks.py | 141 +-\n apps/backend/app/services/task_manager.py | 639 +++++++++\n apps/backend/docs/API-REFERENCE.md | 51 +-\n apps/etl/connectors/feiqiu/.env | 2 +-\n .../endpoints/member_balance_changes.md | 17 +-\n .../endpoints/member_stored_value_cards.md | 61 +-\n .../summary/member_balance_changes.md | 10 +-\n .../main/BD_manual_dws_finance_recharge_summary.md | 17 +-\n .../connectors/feiqiu/docs/etl_tasks/dws_tasks.md | 13 +\n apps/miniprogram/project.config.json | 4 +-\n docs/DOCUMENTATION-MAP.md | 208 ++-\n docs/README.md | 14 +-\n docs/audit/audit_dashboard.md | 27 +-\n docs/contracts/openapi/backend-api.json | 1376 +++++++++++++++++++-\n docs/database/BD_Manual_app_schema_rls_views.md | 16 +-\n docs/database/BD_Manual_biz_tables.md | 44 +-\n docs/database/BD_Manual_fdw_etl_setup.md | 21 +-\n docs/database/ddl/etl_feiqiu__app.sql | 31 +\n docs/database/ddl/zqyy_app__biz.sql | 3 +-\n docs/miniprogram-dev/API-OUTPUT-SPEC.md | 545 --------\n test_results.txt | 25 +-\n 37 files changed, 2861 insertions(+), 997 deletions(-)",
|
||
"high_risk_diff": "diff --git a/apps/admin-web/src/api/client.ts b/apps/admin-web/src/api/client.ts\nindex 5e0a009..8e86514 100644\n--- a/apps/admin-web/src/api/client.ts\n+++ b/apps/admin-web/src/api/client.ts\n@@ -65,7 +65,15 @@ function processPendingQueue(token: string | null, error: unknown) {\n }\n \n apiClient.interceptors.response.use(\n- (response) => response,\n+ (response) => {\n+ // 后端 ResponseWrapperMiddleware 将成功响应包装为 { code: 0, data: <原始body> }\n+ // 此处统一解包,使上层代码无需感知包装层\n+ const body = response.data;\n+ if (body && typeof body === \"object\" && \"code\" in body && \"data\" in body) {\n+ response.data = body.data;\n+ }\n+ return response;\n+ },\n async (error: AxiosError) => {\n const originalRequest = error.config as AxiosRequestConfig & {\n _retried?: boolean;\ndiff --git a/apps/backend/app/main.py b/apps/backend/app/main.py\nindex 43f2322..98c30b5 100644\n--- a/apps/backend/app/main.py\n+++ b/apps/backend/app/main.py\n@@ -9,6 +9,13 @@ from contextlib import asynccontextmanager\n \n from fastapi import FastAPI\n from fastapi.middleware.cors import CORSMiddleware\n+from starlette.exceptions import HTTPException as StarletteHTTPException\n+\n+from app.middleware.response_wrapper import (\n+ ResponseWrapperMiddleware,\n+ http_exception_handler,\n+ unhandled_exception_handler,\n+)\n \n from app import config\n # CHANGE 2026-02-19 | 新增 xcx_test 路由(MVP 验证)+ wx_callback 路由(微信消息推送)\n@@ -19,7 +26,10 @@ from app import config\n # CHANGE 2026-02-27 | 新增 xcx_tasks / xcx_notes 路由(小程序核心业务)\n # CHANGE 2026-03-09 | 新增 xcx_ai_chat 路由(AI SSE 对话 + 历史对话)\n # CHANGE 2026-03-09 | 新增 xcx_ai_cache 路由(AI 缓存查询)\n-from app.routers import auth, execution, schedules, tasks, env_config, db_viewer, etl_status, xcx_test, wx_callback, member_retention_clue, ops_panel, xcx_auth, admin_applications, business_day, xcx_tasks, xcx_notes, xcx_ai_chat, xcx_ai_cache\n+# CHANGE 2026-03-18 | 新增 xcx_customers 路由(CUST-1 客户详情、CUST-2 客户服务记录)\n+# CHANGE 2026-03-19 | 新增 xcx_coaches 路由(COACH-1 助教详情)\n+# CHANGE 2026-03-19 | 新增 xcx_board / xcx_config 路由(RNS1.3 三看板 + 技能类型配置)\n+from app.routers import auth, execution, schedules, tasks, env_config, db_viewer, etl_status, xcx_test, wx_callback, member_retention_clue, ops_panel, xcx_auth, admin_applications, business_day, xcx_tasks, xcx_notes, xcx_ai_chat, xcx_ai_cache, xcx_performance, xcx_customers, xcx_coaches, xcx_board, xcx_config\n from app.services.scheduler import scheduler\n from app.services.task_queue import task_queue\n from app.ws.logs import ws_router\n@@ -103,6 +113,14 @@ app.add_middleware(\n allow_headers=[\"*\"],\n )\n \n+# ---- 全局响应包装中间件(在 CORS 之后添加,执行顺序在 CORS 内层) ----\n+# CHANGE 2026-03-16 | RNS1.0 T0-1: 全局响应包装 + 异常处理器\n+app.add_middleware(ResponseWrapperMiddleware)\n+\n+# ---- 全局异常处理器 ----\n+app.add_exception_handler(StarletteHTTPException, http_exception_handler)\n+app.add_exception_handler(Exception, unhandled_exception_handler)\n+\n # ---- 路由注册 ----\n app.include_router(auth.router)\n app.include_router(tasks.router)\n@@ -123,6 +141,11 @@ app.include_router(xcx_tasks.router)\n app.include_router(xcx_notes.router)\n app.include_router(xcx_ai_chat.router)\n app.include_router(xcx_ai_cache.router)\n+app.include_router(xcx_performance.router)\n+app.include_router(xcx_customers.router)\n+app.include_router(xcx_coaches.router)\n+app.include_router(xcx_board.router)\n+app.include_router(xcx_config.router)\n \n \n @app.get(\"/health\", tags=[\"系统\"])\n--- /dev/null\n+++ b/apps/backend/app/middleware/response_wrapper.py\n@@ -0,0 +1 @@\n# -*- coding: utf-8 -*-\n\"\"\"\n全局响应包装中间件 + 异常处理器。\n\nResponseWrapperMiddleware(ASGI 中间件):\n 对 JSON 成功响应(2xx + application/json)自动包装为 { \"code\": 0, \"data\": <原始body> }。\n 跳过条件:text/event-stream(SSE)、非 application/json、非 2xx 状态码。\n\nExceptionHandler 函数(http_exception_handler / unhandled_exception_handler):\n 统一格式化错误响应为 { \"code\": <status_code>, \"message\": <detail> }。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nfrom typing import Any\n\nfrom fastapi import Request\nfrom fastapi.responses import JSONResponse\nfrom starlette.exceptions import HTTPException\nfrom starlette.types import ASGIApp, Message, Receive, Scope, Send\n\nlogger = logging.getLogger(__name__)\n\n\nclass ResponseWrapperMiddleware:\n \"\"\"ASGI 中间件:全局响应包装。\n\n 拦截 http.response.start / http.response.body,对满足条件的响应\n 包装为 { \"code\": 0, \"data\": <原始body> }。\n\n 跳过条件(透传原始响应):\n 1. content-type 为 text/event-stream(SSE 端点)\n 2. content-type 不包含 application/json(文件下载等)\n 3. HTTP 状态码非 2xx(错误响应已由 ExceptionHandler 格式化)\n \"\"\"\n\n def __init__(self, app: ASGIApp) -> None:\n self.app = app\n\n async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n # 仅处理 HTTP 请求,WebSocket / lifespan 等直接透传\n if scope[\"type\"] != \"http\":\n await self.app(scope, receive, send)\n return\n\n # 用于在 send 回调间共享状态\n should_wrap = False\n status_code = 0\n # 缓存 start message,包装时需要修改 content-length\n cached_start: Message | None = None\n # 收集所有 body 分片(more_body 场景)\n body_parts: list[bytes] = []\n\n async def send_wrapper(message: Message) -> None:\n nonlocal should_wrap, status_code, cached_start\n\n if message[\"type\"] == \"http.response.start\":\n status_code = message.get(\"status\", 200)\n headers = dict(\n (k.lower(), v)\n for k, v in (\n (k if isinstance(k, bytes) else k.encode(),\n v if isinstance(v, bytes) else v.encode())\n for k, v in message.get(\"headers\", [])\n )\n )\n content_type = headers.get(b\"content-type\", b\"\").decode(\"latin-1\").lower()\n\n # 判断是否需要包装\n is_2xx = 200 <= status_code < 300\n is_json = \"application/json\" in content_type\n is_sse = \"text/event-stream\" in content_type\n\n if is_2xx and is_json and not is_sse:\n should_wrap = True\n # 缓存 start message,等 body 完整后再发送(需要更新 content-length)\n cached_start = message\n else:\n # 不包装,直接透传\n should_wrap = False\n await send(message)\n return\n\n if message[\"type\"] == \"http.response.body\":\n if not should_wrap:\n # 不包装,直接透传\n await send(message)\n return\n\n # 收集 body 分片\n body_parts.append(message.get(\"body\", b\"\"))\n more_body = message.get(\"more_body\", False)\n\n if not more_body:\n # body 完整,执行包装\n original_body = b\"\".join(body_parts)\n try:\n wrapped = _wrap_success_body(original_body)\n except Exception:\n # 包装失败(如 JSON 解析错误),透传原始响应\n logger.debug(\n \"响应包装失败,透传原始响应\",\n exc_info=True,\n )\n if cached_start is not None:\n await send(cached_start)\n await send({\n \"type\": \"http.response.body\",\n \"body\": original_body,\n })\n return\n\n # 更新 content-length 并发送\n if cached_start is not None:\n new_headers = _update_content_length(\n cached_start.get(\"headers\", []),\n len(wrapped),\n )\n await send({\n \"type\": \"http.response.start\",\n \"status\": cached_start.get(\"status\", 200),\n \"headers\": new_headers,\n })\n await send({\n \"type\": \"http.response.body\",\n \"body\": wrapped,\n })\n # 如果 more_body=True,继续收集,不发送\n return\n\n try:\n await self.app(scope, receive, send_wrapper)\n except Exception:\n # 中间件自身异常不应阻塞请求,但这里是 app 内部异常,\n # 正常情况下由 ExceptionHandler 处理,此处仅做兜底日志\n logger.exception(\"ResponseWrapperMiddleware 捕获到未处理异常\")\n raise\n\n\ndef _wrap_success_body(original_body: bytes) -> bytes:\n \"\"\"将原始 JSON body 包装为 { \"code\": 0, \"data\": <parsed_body> }。\n\n 如果原始 body 为空,data 设为 null。\n \"\"\"\n if not original_body or original_body.strip() == b\"\":\n data: Any = None\n else:\n data = json.loads(original_body)\n\n wrapped = {\"code\": 0, \"data\": data}\n return json.dumps(wrapped, ensure_ascii=False, separators=(\",\", \":\")).encode(\"utf-8\")\n\n\ndef _update_content_length(\n headers: list[tuple[bytes, bytes] | list],\n new_length: int,\n) -> list[list[bytes]]:\n \"\"\"替换 headers 中的 content-length 为新值。\"\"\"\n new_headers: list[list[bytes]] = []\n found = False\n for pair in headers:\n k = pair[0] if isinstance(pair[0], bytes) else pair[0].encode()\n v = pair[1] if isinstance(pair[1], bytes) else pair[1].encode()\n if k.lower() == b\"content-length\":\n new_headers.append([k, str(new_length).encode()])\n found = True\n else:\n new_headers.append([k, v])\n if not found:\n new_headers.append([b\"content-length\", str(new_length).encode()])\n return new_headers\n\n\n# ---------------------------------------------------------------------------\n# Exception Handlers\n# ---------------------------------------------------------------------------\n\n\nasync def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:\n \"\"\"HTTPException → { code: <status_code>, message: <detail> }\"\"\"\n return JSONResponse(\n status_code=exc.status_code,\n content={\"code\": exc.status_code, \"message\": exc.detail},\n )\n\n\nasync def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:\n \"\"\"未捕获异常 → { code: 500, message: \"Internal Server Error\" }\n 完整堆栈写入服务端日志。\"\"\"\n logger.exception(\"未捕获异常: %s\", exc)\n return JSONResponse(\n status_code=500,\n content={\"code\": 500, \"message\": \"Internal Server Error\"},\n )\n\n--- /dev/null\n+++ b/apps/backend/app/routers/xcx_board.py\n@@ -0,0 +1 @@\n\"\"\"\n看板路由:BOARD-1(助教)、BOARD-2(客户)、BOARD-3(财务)。\n\n前缀 /api/xcx/board,由 main.py 注册。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import APIRouter, Depends, Query\n\nfrom app.auth.dependencies import CurrentUser\nfrom app.middleware.permission import require_permission\nfrom app.schemas.xcx_board import (\n AreaFilterEnum,\n BoardTimeEnum,\n CoachBoardResponse,\n CoachSortEnum,\n CustomerBoardResponse,\n CustomerDimensionEnum,\n FinanceBoardResponse,\n FinanceTimeEnum,\n ProjectFilterEnum,\n SkillFilterEnum,\n)\nfrom app.services import board_service\n\nrouter = APIRouter(prefix=\"/api/xcx/board\", tags=[\"xcx-board\"])\n\n\n@router.get(\"/coaches\", response_model=CoachBoardResponse)\nasync def get_coach_board(\n sort: CoachSortEnum = Query(default=CoachSortEnum.perf_desc),\n skill: SkillFilterEnum = Query(default=SkillFilterEnum.all),\n time: BoardTimeEnum = Query(default=BoardTimeEnum.month),\n user: CurrentUser = Depends(require_permission(\"view_board_coach\")),\n):\n \"\"\"助教看板(BOARD-1)。\"\"\"\n return await board_service.get_coach_board(\n sort=sort.value, skill=skill.value, time=time.value,\n site_id=user.site_id,\n )\n\n\n@router.get(\"/customers\", response_model=CustomerBoardResponse)\nasync def get_customer_board(\n dimension: CustomerDimensionEnum = Query(default=CustomerDimensionEnum.recall),\n project: ProjectFilterEnum = Query(default=ProjectFilterEnum.all),\n page: int = Query(default=1, ge=1),\n page_size: int = Query(default=20, ge=1, le=100),\n user: CurrentUser = Depends(require_permission(\"view_board_customer\")),\n):\n \"\"\"客户看板(BOARD-2)。\"\"\"\n return await board_service.get_customer_board(\n dimension=dimension.value, project=project.value,\n page=page, page_size=page_size, site_id=user.site_id,\n )\n\n\n@router.get(\n \"/finance\",\n response_model=FinanceBoardResponse,\n response_model_exclude_none=True,\n)\nasync def get_finance_board(\n time: FinanceTimeEnum = Query(default=FinanceTimeEnum.month),\n area: AreaFilterEnum = Query(default=AreaFilterEnum.all),\n compare: int = Query(default=0, ge=0, le=1),\n user: CurrentUser = Depends(require_permission(\"view_board_finance\")),\n):\n \"\"\"财务看板(BOARD-3)。\"\"\"\n return await board_service.get_finance_board(\n time=time.value, area=area.value, compare=compare,\n site_id=user.site_id,\n )\n\n--- /dev/null\n+++ b/apps/backend/app/routers/xcx_coaches.py\n@@ -0,0 +1 @@\n# -*- coding: utf-8 -*-\n\"\"\"\n小程序助教路由 —— 助教详情(COACH-1)。\n\n端点清单:\n- GET /api/xcx/coaches/{coach_id} — 助教详情(COACH-1)\n\n所有端点均需 JWT(approved 状态)。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import APIRouter, Depends\n\nfrom app.auth.dependencies import CurrentUser\nfrom app.middleware.permission import require_approved\nfrom app.schemas.xcx_coaches import CoachDetailResponse\nfrom app.services import coach_service\n\nrouter = APIRouter(prefix=\"/api/xcx/coaches\", tags=[\"小程序助教\"])\n\n\n@router.get(\"/{coach_id}\", response_model=CoachDetailResponse)\nasync def get_coach_detail(\n coach_id: int,\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"助教详情(COACH-1)。\"\"\"\n return await coach_service.get_coach_detail(\n coach_id, user.site_id\n )\n\n--- /dev/null\n+++ b/apps/backend/app/routers/xcx_config.py\n@@ -0,0 +1 @@\n\"\"\"\n配置路由:CONFIG-1 技能类型。\n\n前缀 /api/xcx/config,由 main.py 注册。\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nfrom fastapi import APIRouter, Depends\n\nfrom app.auth.dependencies import CurrentUser\nfrom app.middleware.permission import require_approved\nfrom app.schemas.xcx_config import SkillTypeItem\nfrom app.services import fdw_queries\n\nlogger = logging.getLogger(__name__)\n\nrouter = APIRouter(prefix=\"/api/xcx/config\", tags=[\"xcx-config\"])\n\n\n@router.get(\"/skill-types\", response_model=list[SkillTypeItem])\nasync def get_skill_types(\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"技能类型配置(CONFIG-1)。查询失败降级返回空数组。\"\"\"\n try:\n from app.database import get_connection\n conn = get_connection()\n try:\n return fdw_queries.get_skill_types(conn, user.site_id)\n finally:\n conn.close()\n except Exception:\n logger.warning(\"CONFIG-1 技能类型查询失败,降级为空数组\", exc_info=True)\n return []\n\n--- /dev/null\n+++ b/apps/backend/app/routers/xcx_customers.py\n@@ -0,0 +1 @@\n# -*- coding: utf-8 -*-\n\"\"\"\n小程序客户路由 —— 客户详情(CUST-1)、客户服务记录(CUST-2)。\n\n端点清单:\n- GET /api/xcx/customers/{customer_id} — 客户详情(CUST-1)\n- GET /api/xcx/customers/{customer_id}/records — 客户服务记录(CUST-2)\n\n所有端点均需 JWT(approved 状态)。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import APIRouter, Depends, Query\n\nfrom app.auth.dependencies import CurrentUser\nfrom app.middleware.permission import require_approved\nfrom app.schemas.xcx_customers import (\n CustomerDetailResponse,\n CustomerRecordsResponse,\n)\nfrom app.services import customer_service\n\nrouter = APIRouter(prefix=\"/api/xcx/customers\", tags=[\"小程序客户\"])\n\n\n@router.get(\"/{customer_id}\", response_model=CustomerDetailResponse)\nasync def get_customer_detail(\n customer_id: int,\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"客户详情(CUST-1)。\"\"\"\n return await customer_service.get_customer_detail(\n customer_id, user.site_id\n )\n\n\n@router.get(\"/{customer_id}/records\", response_model=CustomerRecordsResponse)\nasync def get_customer_records(\n customer_id: int,\n year: int = Query(...),\n month: int = Query(..., ge=1, le=12),\n table: str | None = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"客户服务记录(CUST-2)。\"\"\"\n return await customer_service.get_customer_records(\n customer_id, user.site_id, year, month, table, page, page_size\n )\n\n--- /dev/null\n+++ b/apps/backend/app/routers/xcx_performance.py\n@@ -0,0 +1 @@\n# -*- coding: utf-8 -*-\n\"\"\"\n小程序绩效路由 —— 绩效概览、绩效明细。\n\n端点清单:\n- GET /api/xcx/performance — 绩效概览(PERF-1)\n- GET /api/xcx/performance/records — 绩效明细(PERF-2)\n\n所有端点均需 JWT(approved 状态)。\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import APIRouter, Depends, Query\n\nfrom app.auth.dependencies import CurrentUser\nfrom app.middleware.permission import require_approved\nfrom app.schemas.xcx_performance import (\n PerformanceOverviewResponse,\n PerformanceRecordsResponse,\n)\nfrom app.services import performance_service\n\nrouter = APIRouter(prefix=\"/api/xcx/performance\", tags=[\"小程序绩效\"])\n\n\n@router.get(\"\", response_model=PerformanceOverviewResponse)\nasync def get_performance_overview(\n year: int = Query(...),\n month: int = Query(..., ge=1, le=12),\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"绩效概览(PERF-1)。\"\"\"\n return await performance_service.get_overview(\n user.user_id, user.site_id, year, month\n )\n\n\n@router.get(\"/records\", response_model=PerformanceRecordsResponse)\nasync def get_performance_records(\n year: int = Query(...),\n month: int = Query(..., ge=1, le=12),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n user: CurrentUser = Depends(require_approved()),\n):\n \"\"\"绩效明细(PERF-2)。\"\"\"\n return await performance_service.get_records(\n user.user_id, user.site_id, year, month, page, page_size\n )\n\ndiff --git a/apps/backend/app/routers/xcx_tasks.py b/apps/backend/app/routers/xcx_tasks.py\nindex 3b8a926..acc72fa 100644\n--- a/apps/backend/app/routers/xcx_tasks.py\n+++ b/apps/backend/app/routers/xcx_tasks.py\n@@ -1,35 +1,56 @@\n # -*- coding: utf-8 -*-\n \"\"\"\n-小程序任务路由 —— 任务列表、置顶、放弃、取消放弃。\n+小程序任务路由 —— 任务列表、任务详情、置顶、放弃、取消放弃。\n \n 端点清单:\n-- GET /api/xcx/tasks — 获取活跃任务列表\n+- GET /api/xcx/tasks — 获取任务列表 + 绩效概览(TASK-1)\n+- GET /api/xcx/tasks/{task_id} — 获取任务详情完整版(TASK-2)\n - POST /api/xcx/tasks/{id}/pin — 置顶任务\n - POST /api/xcx/tasks/{id}/unpin — 取消置顶\n - POST /api/xcx/tasks/{id}/abandon — 放弃任务\n-- POST /api/xcx/tasks/{id}/cancel-abandon — 取消放弃\n+- POST /api/xcx/tasks/{id}/restore — 恢复任务\n \n 所有端点均需 JWT(approved 状态)。\n \"\"\"\n \n from __future__ import annotations\n \n-from fastapi import APIRouter, Depends\n+from fastapi import APIRouter, Depends, Query\n \n from app.auth.dependencies import CurrentUser\n from app.middleware.permission import require_approved\n-from app.schemas.xcx_tasks import AbandonRequest, TaskListItem\n+from app.schemas.xcx_tasks import (\n+ AbandonRequest,\n+ TaskDetailResponse,\n+ TaskListResponse,\n+)\n from app.services import task_manager\n \n router = APIRouter(prefix=\"/api/xcx/tasks\", tags=[\"小程序任务\"])\n \n \n-@router.get(\"\", response_model=list[TaskListItem])\n+@router.get(\"\", response_model=TaskListResponse)\n async def get_tasks(\n+ status: str = Query(\"pending\", pattern=\"^(pending|completed|abandoned)$\"),\n+ page: int = Query(1, ge=1),\n+ page_size: int = Query(20, ge=1, le=100),\n user: CurrentUser = Depends(require_approved()),\n ):\n- \"\"\"获取当前助教的活跃任务列表。\"\"\"\n- return await task_manager.get_task_list(user.user_id, user.site_id)\n+ \"\"\"获取任务列表 + 绩效概览。\"\"\"\n+ return await task_manager.get_task_list_v2(\n+ user.user_id, user.site_id, status, page, page_size\n+ )\n+\n+\n+@router.get(\"/{task_id}\", response_model=TaskDetailResponse)\n+async def get_task_detail(\n+ task_id: int,\n+ user: CurrentUser = Depends(require_approved()),\n+):\n+ \"\"\"获取任务详情完整版。\"\"\"\n+ return await task_manager.get_task_detail(\n+ task_id, user.user_id, user.site_id\n+ )\n \n \n @router.post(\"/{task_id}/pin\")\n@@ -38,7 +59,8 @@ async def pin_task(\n user: CurrentUser = Depends(require_approved()),\n ):\n \"\"\"置顶任务。\"\"\"\n- return await task_manager.pin_task(task_id, user.user_id, user.site_id)\n+ result = await task_manager.pin_task(task_id, user.user_id, user.site_id)\n+ return {\"is_pinned\": result[\"is_pinned\"]}\n \n \n @router.post(\"/{task_id}/unpin\")\n@@ -47,7 +69,8 @@ async def unpin_task(\n user: CurrentUser = Depends(require_approved()),\n ):\n \"\"\"取消置顶。\"\"\"\n- return await task_manager.unpin_task(task_id, user.user_id, user.site_id)\n+ result = await task_manager.unpin_task(task_id, user.user_id, user.site_id)\n+ return {\"is_pinned\": result[\"is_pinned\"]}\n \n \n @router.post(\"/{task_id}/abandon\")\n@@ -62,8 +85,8 @@ async def abandon_task(\n )\n \n \n-@router.post(\"/{task_id}/cancel-abandon\")\n-async def cancel_abandon(\n+@router.post(\"/{task_id}/restore\")\n+async def restore_task(\n task_id: int,\n user: CurrentUser = Depends(require_approved()),\n ):\n--- /dev/null\n+++ b/apps/backend/app/schemas/base.py\n@@ -0,0 +1 @@\nfrom pydantic import BaseModel, ConfigDict\nfrom pydantic.alias_generators import to_camel\n\n\nclass CamelModel(BaseModel):\n \"\"\"所有小程序 API 响应 schema 的基类。\n\n - alias_generator=to_camel:JSON 输出字段名自动转 camelCase\n - populate_by_name=True:同时接受 snake_case 和 camelCase 输入\n - from_attributes=True:支持从 ORM 对象/dict 构造\n \"\"\"\n\n model_config = ConfigDict(\n alias_generator=to_camel,\n populate_by_name=True,\n from_attributes=True,\n )\n\ndiff --git a/apps/backend/app/schemas/xcx_auth.py b/apps/backend/app/schemas/xcx_auth.py\nindex 831abdd..329e7b6 100644\n--- a/apps/backend/app/schemas/xcx_auth.py\n+++ b/apps/backend/app/schemas/xcx_auth.py\n@@ -6,17 +6,19 @@\n \n from __future__ import annotations\n \n-from pydantic import BaseModel, Field\n+from pydantic import Field\n+\n+from app.schemas.base import CamelModel\n \n \n # ── 微信登录 ──────────────────────────────────────────────\n \n-class WxLoginRequest(BaseModel):\n+class WxLoginRequest(CamelModel):\n \"\"\"微信登录请求。\"\"\"\n code: str = Field(..., min_length=1, description=\"微信临时登录凭证\")\n \n \n-class WxLoginResponse(BaseModel):\n+class WxLoginResponse(CamelModel):\n \"\"\"微信登录响应。\"\"\"\n access_token: str\n refresh_token: str\n@@ -25,7 +27,7 @@ class WxLoginResponse(BaseModel):\n user_id: int\n \n \n-class DevLoginRequest(BaseModel):\n+class DevLoginRequest(CamelModel):\n \"\"\"开发模式 mock 登录请求(仅 WX_DEV_MODE=true 时可用)。\"\"\"\n openid: str = Field(..., min_length=1, description=\"模拟的微信 openid\")\n status: str | None = Field(None, description=\"模拟的用户状态;为空时保留已有用户的当前状态,新用户默认 new\")\n@@ -33,24 +35,24 @@ class DevLoginRequest(BaseModel):\n \n # ── 开发调试端点(仅 WX_DEV_MODE=true) ─────────────────\n \n- class DevSwitchRoleRequest(BaseModel):\n+ class DevSwitchRoleRequest(CamelModel):\n \"\"\"切换角色请求。替换当前用户在当前门店下的所有角色为指定角色。\"\"\"\n role_code: str = Field(..., description=\"目标角色 code(coach/staff/site_admin/tenant_admin)\")\n \n \n- class DevSwitchStatusRequest(BaseModel):\n+ class DevSwitchStatusRequest(CamelModel):\n \"\"\"切换用户状态请求。\"\"\"\n status: str = Field(..., description=\"目标状态(new/pending/approved/rejected/disabled)\")\n \n \n- class DevSwitchBindingRequest(BaseModel):\n+ class DevSwitchBindingRequest(CamelModel):\n \"\"\"切换人员绑定请求。\"\"\"\n binding_type: str = Field(..., description=\"绑定类型(assistant/staff/manager)\")\n assistant_id: int | None = Field(None, description=\"助教 ID(binding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 ID(binding_type=staff/manager 时必填)\")\n \n \n- class DevContextResponse(BaseModel):\n+ class DevContextResponse(CamelModel):\n \"\"\"开发调试上下文信息。\"\"\"\n user_id: int\n openid: str | None = None\n@@ -67,7 +69,7 @@ class DevLoginRequest(BaseModel):\n \n # ── 用户申请 ──────────────────────────────────────────────\n \n-class ApplicationRequest(BaseModel):\n+class ApplicationRequest(CamelModel):\n \"\"\"用户申请提交请求。\"\"\"\n site_code: str = Field(..., pattern=r\"^[A-Za-z]{2}\\d{3}$\", description=\"球房ID\")\n applied_role_text: str = Field(..., min_length=1, max_length=100, description=\"申请身份\")\n@@ -76,7 +78,7 @@ class ApplicationRequest(BaseModel):\n nickname: str | None = Field(None, max_length=50, description=\"昵称\")\n \n \n-class ApplicationResponse(BaseModel):\n+class ApplicationResponse(CamelModel):\n \"\"\"申请记录响应。\"\"\"\n id: int\n site_code: str\n@@ -89,7 +91,7 @@ class ApplicationResponse(BaseModel):\n \n # ── 用户状态 ──────────────────────────────────────────────\n \n-class UserStatusResponse(BaseModel):\n+class UserStatusResponse(CamelModel):\n \"\"\"用户状态查询响应。\"\"\"\n user_id: int\n status: str\n@@ -99,28 +101,28 @@ class UserStatusResponse(BaseModel):\n \n # ── 店铺 ──────────────────────────────────────────────────\n \n-class SiteInfo(BaseModel):\n+class SiteInfo(CamelModel):\n \"\"\"店铺信息。\"\"\"\n site_id: int\n site_name: str\n roles: list[dict] = []\n \n \n-class SwitchSiteRequest(BaseModel):\n+class SwitchSiteRequest(CamelModel):\n \"\"\"切换店铺请求。\"\"\"\n site_id: int\n \n \n # ── 刷新令牌 ──────────────────────────────────────────────\n \n-class RefreshTokenRequest(BaseModel):\n+class RefreshTokenRequest(CamelModel):\n \"\"\"刷新令牌请求。\"\"\"\n refresh_token: str = Field(..., min_length=1, description=\"刷新令牌\")\n \n \n # ── 人员匹配 ──────────────────────────────────────────────\n \n-class MatchCandidate(BaseModel):\n+class MatchCandidate(CamelModel):\n \"\"\"匹配候选人。\"\"\"\n source_type: str # assistant / staff\n id: int\n@@ -131,38 +133,38 @@ class MatchCandidate(BaseModel):\n \n # ── 管理端审核 ────────────────────────────────────────────\n \n-class ApproveRequest(BaseModel):\n+class ApproveRequest(CamelModel):\n \"\"\"批准申请请求。\"\"\"\n role_id: int\n binding: dict | None = None # {\"assistant_id\": ..., \"staff_id\": ..., \"binding_type\": ...}\n review_note: str | None = None\n \n \n-class RejectRequest(BaseModel):\n+class RejectRequest(CamelModel):\n \"\"\"拒绝申请请求。\"\"\"\n review_note: str = Field(..., min_length=1, description=\"拒绝原因\")\n \n \n # ── 开发调试端点(仅 WX_DEV_MODE=true) ─────────────────\n \n-class DevSwitchRoleRequest(BaseModel):\n+class DevSwitchRoleRequest(CamelModel):\n \"\"\"切换角色请求。替换当前用户在当前门店下的所有角色为指定角色。\"\"\"\n role_code: str = Field(..., description=\"目标角色 code(coach/staff/site_admin/tenant_admin)\")\n \n \n-class DevSwitchStatusRequest(BaseModel):\n+class DevSwitchStatusRequest(CamelModel):\n \"\"\"切换用户状态请求。\"\"\"\n status: str = Field(..., description=\"目标状态(new/pending/approved/rejected/disabled)\")\n \n \n-class DevSwitchBindingRequest(BaseModel):\n+class DevSwitchBindingRequest(CamelModel):\n \"\"\"切换人员绑定请求。\"\"\"\n binding_type: str = Field(..., description=\"绑定类型(assistant/staff/manager)\")\n assistant_id: int | None = Field(None, description=\"助教 ID(binding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 ID(binding_type=staff/manager 时必填)\")\n \n \n-class DevContextResponse(BaseModel):\n+class DevContextResponse(CamelModel):\n \"\"\"开发调试上下文信息。\"\"\"\n user_id: int\n openid: str | None = None\n@@ -178,24 +180,24 @@ class DevContextResponse(BaseModel):\n \n # ── 开发调试端点(仅 WX_DEV_MODE=true) ─────────────────\n \n-class DevSwitchRoleRequest(BaseModel):\n+class DevSwitchRoleRequest(CamelModel):\n \"\"\"切换角色请求。替换当前用户在当前门店下的所有角色为指定角色。\"\"\"\n role_code: str = Field(..., description=\"目标角色 code(coach/staff/site_admin/tenant_admin)\")\n \n \n-class DevSwitchStatusRequest(BaseModel):\n+class DevSwitchStatusRequest(CamelModel):\n \"\"\"切换用户状态请求。\"\"\"\n status: str = Field(..., description=\"目标状态(new/pending/approved/rejected/disabled)\")\n \n \n-class DevSwitchBindingRequest(BaseModel):\n+class DevSwitchBindingRequest(CamelModel):\n \"\"\"切换人员绑定请求。\"\"\"\n binding_type: str = Field(..., description=\"绑定类型(assistant/staff/manager)\")\n assistant_id: int | None = Field(None, description=\"助教 ID(binding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 ID(binding_type=staff/manager 时必填)\")\n \n \n-class DevContextResponse(BaseModel):\n+class DevContextResponse(CamelModel):\n \"\"\"开发调试上下文信息。\"\"\"\n user_id: int\n openid: str | None = None\n@@ -207,4 +209,3 @@ class DevContextResponse(BaseModel):\n permissions: list[str] = []\n binding: dict | None = None\n all_sites: list[dict] = []\n-\n--- /dev/null\n+++ b/apps/backend/app/schemas/xcx_board.py\n@@ -0,0 +1 @@\n\"\"\"三看板接口 Pydantic Schema(BOARD-1/2/3 请求参数枚举 + 响应模型)。\"\"\"\n\nfrom __future__ import annotations\n\nfrom enum import Enum\n\nfrom app.schemas.base import CamelModel\n\n\n# ---------------------------------------------------------------------------\n# 请求参数枚举(Task 2.1)\n# ---------------------------------------------------------------------------\n\nclass CoachSortEnum(str, Enum):\n \"\"\"BOARD-1 排序维度。\"\"\"\n perf_desc = \"perf_desc\"\n perf_asc = \"perf_asc\"\n salary_desc = \"salary_desc\"\n salary_asc = \"salary_asc\"\n sv_desc = \"sv_desc\"\n task_desc = \"task_desc\"\n\n\nclass SkillFilterEnum(str, Enum):\n \"\"\"BOARD-1 技能筛选。\"\"\"\n all = \"all\"\n chinese = \"chinese\"\n snooker = \"snooker\"\n mahjong = \"mahjong\"\n karaoke = \"karaoke\"\n\n\nclass BoardTimeEnum(str, Enum):\n \"\"\"BOARD-1 时间范围。\"\"\"\n month = \"month\"\n quarter = \"quarter\"\n last_month = \"last_month\"\n last_3m = \"last_3m\"\n last_quarter = \"last_quarter\"\n last_6m = \"last_6m\"\n\n\nclass CustomerDimensionEnum(str, Enum):\n \"\"\"BOARD-2 客户维度。\"\"\"\n recall = \"recall\"\n potential = \"potential\"\n balance = \"balance\"\n recharge = \"recharge\"\n recent = \"recent\"\n spend60 = \"spend60\"\n freq60 = \"freq60\"\n loyal = \"loyal\"\n\n\nclass ProjectFilterEnum(str, Enum):\n \"\"\"BOARD-2 项目筛选。\"\"\"\n all = \"all\"\n chinese = \"chinese\"\n snooker = \"snooker\"\n mahjong = \"mahjong\"\n karaoke = \"karaoke\"\n\n\nclass FinanceTimeEnum(str, Enum):\n \"\"\"BOARD-3 时间范围。\"\"\"\n month = \"month\"\n lastMonth = \"lastMonth\"\n week = \"week\"\n lastWeek = \"lastWeek\"\n quarter3 = \"quarter3\"\n quarter = \"quarter\"\n lastQuarter = \"lastQuarter\"\n half6 = \"half6\"\n\n\nclass AreaFilterEnum(str, Enum):\n \"\"\"BOARD-3 区域筛选。\"\"\"\n all = \"all\"\n hall = \"hall\"\n hallA = \"hallA\"\n hallB = \"hallB\"\n hallC = \"hallC\"\n mahjong = \"mahjong\"\n teamBuilding = \"teamBuilding\"\n\n\n# ---------------------------------------------------------------------------\n# BOARD-1 响应 Schema(Task 2.2)\n# ---------------------------------------------------------------------------\n\nclass CoachSkillItem(CamelModel):\n text: str\n cls: str\n\n\nclass CoachBoardItem(CamelModel):\n \"\"\"助教看板单条记录(扁平结构,包含所有维度字段)。\"\"\"\n\n # 基础字段(所有维度共享)\n id: int\n name: str\n initial: str\n avatar_gradient: str\n level: str # star/senior/middle/junior\n skills: list[CoachSkillItem]\n top_customers: list[str] # [\"💖 王先生\", \"💛 李女士\"]\n\n # perf 维度\n perf_hours: float = 0.0\n perf_hours_before: float | None = None\n perf_gap: str | None = None # \"距升档 13.8h\" 或 None\n perf_reached: bool = False\n\n # salary 维度\n salary: float = 0.0\n salary_perf_hours: float = 0.0\n salary_perf_before: float | None = None\n\n # sv 维度\n sv_amount: float = 0.0\n sv_customer_count: int = 0\n sv_consume: float = 0.0\n\n # task 维度\n task_recall: int = 0\n task_callback: int = 0\n\n\nclass CoachBoardResponse(CamelModel):\n items: list[CoachBoardItem]\n dim_type: str # perf/salary/sv/task\n\n\n# ---------------------------------------------------------------------------\n# BOARD-2 响应 Schema(Task 2.3)\n# ---------------------------------------------------------------------------\n\nclass CustomerAssistant(CamelModel):\n name: str\n cls: str\n heart_score: float\n badge: str | None = None\n badge_cls: str | None = None\n\n\nclass CustomerBoardItemBase(CamelModel):\n \"\"\"客户看板基础字段(所有维度共享)。\"\"\"\n id: int\n name: str\n initial: str\n avatar_cls: str\n assistants: list[CustomerAssistant]\n\n\nclass RecallItem(CustomerBoardItemBase):\n ideal_days: int\n elapsed_days: int\n overdue_days: int\n visits_30d: int\n balance: str\n recall_index: float\n\n\nclass PotentialTag(CamelModel):\n text: str\n theme: str\n\n\nclass PotentialItem(CustomerBoardItemBase):\n potential_tags: list[PotentialTag]\n spend_30d: float\n avg_visits: float\n avg_spend: float\n\n\nclass BalanceItem(CustomerBoardItemBase):\n balance: str\n last_visit: str # \"3天前\"\n monthly_consume: float\n available_months: str # \"约0.8个月\"\n\n\nclass RechargeItem(CustomerBoardItemBase):\n last_recharge: str\n recharge_amount: float\n recharges_60d: int\n current_balance: str\n\n\nclass RecentItem(CustomerBoardItemBase):\n days_ago: int\n visit_freq: str # \"6.2次/月\"\n ideal_days: int\n visits_30d: int\n avg_spend: float\n\n\nclass Spend60Item(CustomerBoardItemBase):\n spend_60d: float\n visits_60d: int\n high_spend_tag: bool\n avg_spend: float\n\n\nclass WeeklyVisit(CamelModel):\n val: int\n pct: int # 0-100\n\n\nclass Freq60Item(CustomerBoardItemBase):\n visits_60d: int\n avg_interval: str # \"5.0天\"\n weekly_visits: list[WeeklyVisit] # 固定长度 8\n spend_60d: float\n\n\nclass CoachDetail(CamelModel):\n name: str\n cls: str\n heart_score: float\n badge: str | None = None\n avg_duration: str\n service_count: int\n coach_spend: float\n relation_idx: float\n\n\nclass LoyalItem(CustomerBoardItemBase):\n intimacy: float\n top_coach_name: str\n top_coach_heart: float\n top_coach_score: float\n coach_name: str\n coach_ratio: str # \"78%\"\n coach_details: list[CoachDetail]\n\n\nclass CustomerBoardResponse(CamelModel):\n items: list[dict] # 实际类型取决于 dimension\n total: int\n page: int\n page_size: int\n\n\n# ---------------------------------------------------------------------------\n# BOARD-3 响应 Schema(Task 2.4)\n# ---------------------------------------------------------------------------\n\nclass OverviewPanel(CamelModel):\n \"\"\"经营一览:8 项核心指标 + 各 3 个环比字段(Optional,compare=0 时为 None)。\"\"\"\n occurrence: float\n discount: float # 负值\n discount_rate: float\n confirmed_revenue: float\n cash_in: float\n cash_out: float\n cash_balance: float\n balance_rate: float\n # occurrence 环比\n occurrence_compare: str | None = None\n occurrence_down: bool | None = None\n occurrence_flat: bool | None = None\n # discount 环比\n discount_compare: str | None = None\n discount_down: bool | None = None\n discount_flat: bool | None = None\n # discount_rate 环比\n discount_rate_compare: str | None = None\n discount_rate_down: bool | None = None\n discount_rate_flat: bool | None = None\n # confirmed_revenue 环比\n confirmed_revenue_compare: str | None = None\n confirmed_revenue_down: bool | None = None\n confirmed_revenue_flat: bool | None = None\n # cash_in 环比\n cash_in_compare: str | None = None\n cash_in_down: bool | None = None\n cash_in_flat: bool | None = None\n # cash_out 环比\n cash_out_compare: str | None = None\n cash_out_down: bool | None = None\n cash_out_flat: bool | None = None\n # cash_balance 环比\n cash_balance_compare: str | None = None\n cash_balance_down: bool | None = None\n cash_balance_flat: bool | None = None\n # balance_rate 环比\n balance_rate_compare: str | None = None\n balance_rate_down: bool | None = None\n balance_rate_flat: bool | None = None\n\n\nclass GiftCell(CamelModel):\n value: float\n compare: str | None = None\n down: bool | None = None\n flat: bool | None = None\n\n\nclass GiftRow(CamelModel):\n \"\"\"赠送卡矩阵一行:合计 / 酒水卡 / 台费卡 / 抵用券。\"\"\"\n label: str # \"新增\" / \"消费\" / \"余额\"\n total: GiftCell\n liquor: GiftCell\n table_fee: GiftCell\n voucher: GiftCell\n\n\nclass RechargePanel(CamelModel):\n \"\"\"预收资产板块:储值卡 5 指标 + 赠送卡 3×4 矩阵 + 全卡余额。\"\"\"\n actual_income: float\n first_charge: float\n renew_charge: float\n consumed: float\n card_balance: float\n gift_rows: list[GiftRow] # 3 行\n all_card_balance: float\n # 储值卡各项环比字段\n actual_income_compare: str | None = None\n actual_income_down: bool | None = None\n actual_income_flat: bool | None = None\n first_charge_compare: str | None = None\n first_charge_down: bool | None = None\n first_charge_flat: bool | None = None\n renew_charge_compare: str | None = None\n renew_charge_down: bool | None = None\n renew_charge_flat: bool | None = None\n consumed_compare: str | None = None\n consumed_down: bool | None = None\n consumed_flat: bool | None = None\n card_balance_compare: str | None = None\n card_balance_down: bool | None = None\n card_balance_flat: bool | None = None\n\n\nclass RevenueStructureRow(CamelModel):\n id: str\n name: str\n desc: str | None = None\n is_sub: bool = False\n amount: float\n discount: float\n booked: float\n booked_compare: str | None = None\n\n\nclass RevenueItem(CamelModel):\n label: str\n amount: float\n\n\nclass ChannelItem(CamelModel):\n label: str\n amount: float\n\n\nclass RevenuePanel(CamelModel):\n structure_rows: list[RevenueStructureRow]\n price_items: list[RevenueItem] # 4 项\n total_occurrence: float\n discount_items: list[RevenueItem] # 4 项\n confirmed_total: float\n channel_items: list[ChannelItem] # 3 项\n\n\nclass CashflowItem(CamelModel):\n label: str\n amount: float\n\n\nclass CashflowPanel(CamelModel):\n consume_items: list[CashflowItem] # 3 项\n recharge_items: list[CashflowItem] # 1 项\n total: float\n\n\nclass ExpenseItem(CamelModel):\n label: str\n amount: float\n compare: str | None = None\n down: bool | None = None\n flat: bool | None = None\n\n\nclass ExpensePanel(CamelModel):\n operation_items: list[ExpenseItem] # 3 项\n fixed_items: list[ExpenseItem] # 4 项\n coach_items: list[ExpenseItem] # 4 项\n platform_items: list[ExpenseItem] # 3 项\n total: float\n total_compare: str | None = None\n total_down: bool | None = None\n total_flat: bool | None = None\n\n\nclass CoachAnalysisRow(CamelModel):\n level: str\n pay: float\n share: float\n hourly: float\n pay_compare: str | None = None\n pay_down: bool | None = None\n share_compare: str | None = None\n share_down: bool | None = None\n hourly_compare: str | None = None\n hourly_flat: bool | None = None\n\n\nclass CoachAnalysisTable(CamelModel):\n total_pay: float\n total_share: float\n avg_hourly: float\n total_pay_compare: str | None = None\n total_pay_down: bool | None = None\n total_share_compare: str | None = None\n total_share_down: bool | None = None\n avg_hourly_compare: str | None = None\n avg_hourly_flat: bool | None = None\n rows: list[CoachAnalysisRow] # 4 行:初级/中级/高级/星级\n\n\nclass CoachAnalysisPanel(CamelModel):\n basic: CoachAnalysisTable # 基础课/陪打\n incentive: CoachAnalysisTable # 激励课/超休\n\n\nclass FinanceBoardResponse(CamelModel):\n overview: OverviewPanel\n recharge: RechargePanel | None # area≠all 时为 null\n revenue: RevenuePanel\n cashflow: CashflowPanel\n expense: ExpensePanel\n coach_analysis: CoachAnalysisPanel\n\n\n[TRUNCATED: diff exceeds 30KB]",
|
||
"latest_prompt_log": "- [P20260319-215752] 2026-03-19 21:57:52 +0800\n - summary: 所有小程序都有这个问题,我建立了一个测试小程序apps\\DEMO-miniprogram,在这里进行测试。\n - prompt:\n```text\n所有小程序都有这个问题,我建立了一个测试小程序apps\\DEMO-miniprogram,在这里进行测试。\n```\n"
|
||
} |