Files
Neo-ZQYY/.kiro/state/.audit_context.json

190 lines
56 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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\nResponseWrapperMiddlewareASGI 中间件):\n 对 JSON 成功响应2xx + application/json自动包装为 { \"code\": 0, \"data\": <原始body> }。\n 跳过条件text/event-streamSSE、非 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-streamSSE 端点)\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所有端点均需 JWTapproved 状态)。\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所有端点均需 JWTapproved 状态)。\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所有端点均需 JWTapproved 状态)。\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 所有端点均需 JWTapproved 状态)。\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_camelJSON 输出字段名自动转 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=\"目标角色 codecoach/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=\"助教 IDbinding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 IDbinding_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=\"目标角色 codecoach/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=\"助教 IDbinding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 IDbinding_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=\"目标角色 codecoach/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=\"助教 IDbinding_type=assistant 时必填)\")\n staff_id: int | None = Field(None, description=\"员工 IDbinding_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 SchemaBOARD-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 响应 SchemaTask 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 响应 SchemaTask 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 响应 SchemaTask 2.4\n# ---------------------------------------------------------------------------\n\nclass OverviewPanel(CamelModel):\n \"\"\"经营一览8 项核心指标 + 各 3 个环比字段Optionalcompare=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"
}