{
  "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"
}