From f87998e9b81c17d35d1a73b3ab439ca209ca9b2d Mon Sep 17 00:00:00 2001 From: tanzk <15814458408@qq.com> Date: Thu, 15 Oct 2026 22:56:27 +0800 Subject: [PATCH] =?UTF-8?q?chore(repo):=20#330=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=20.trae/.dsh/.workbuddy=20=E5=8E=BB=E8=B7=9F?= =?UTF-8?q?=E8=B8=AA=20+=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依据《仓库入库规范》C7(工具私有状态不入库): - git rm -r --cached .trae .dsh .workbuddy (15 文件, 仅去跟踪, 磁盘保留) - .gitignore 增补 .trae/ .dsh/ --- .../SKILL.md | 56 - .dsh/skills/csctalon-ui-organization/SKILL.md | 73 -- .dsh/skills/csctalon-workflow-design/SKILL.md | 71 -- .gitignore | 2 + .trae/documents/327-mcp-readonly-export.md | 81 -- .trae/documents/README.md | 9 - .../ai-assistant-performance-optimization.md | 169 --- ...eed-refactor-dual-layer-and-daily-grain.md | 502 -------- .../documents/ai-report-optimization-plan.md | 151 --- .../complaint-and-specialty-improvements.md | 174 --- .../metric-alerting-notification-design.md | 379 ------ .trae/documents/refund-dashboard-redesign.md | 1096 ----------------- .../time-aware-specialty-workload.md | 191 --- .trae/documents/收尾计划-AI治理大任务前.md | 105 -- .trae/specs/191-299-closeout/spec.md | 86 -- .trae/specs/191-299-closeout/tasks.md | 45 - .workbuddy/memory/2026-08-20.md | 18 - 17 files changed, 2 insertions(+), 3206 deletions(-) delete mode 100644 .dsh/skills/csctalon-management-effectiveness/SKILL.md delete mode 100644 .dsh/skills/csctalon-ui-organization/SKILL.md delete mode 100644 .dsh/skills/csctalon-workflow-design/SKILL.md delete mode 100644 .trae/documents/327-mcp-readonly-export.md delete mode 100644 .trae/documents/README.md delete mode 100644 .trae/documents/ai-assistant-performance-optimization.md delete mode 100644 .trae/documents/ai-feed-refactor-dual-layer-and-daily-grain.md delete mode 100644 .trae/documents/ai-report-optimization-plan.md delete mode 100644 .trae/documents/complaint-and-specialty-improvements.md delete mode 100644 .trae/documents/metric-alerting-notification-design.md delete mode 100644 .trae/documents/refund-dashboard-redesign.md delete mode 100644 .trae/documents/time-aware-specialty-workload.md delete mode 100644 .trae/documents/收尾计划-AI治理大任务前.md delete mode 100644 .trae/specs/191-299-closeout/spec.md delete mode 100644 .trae/specs/191-299-closeout/tasks.md delete mode 100644 .workbuddy/memory/2026-08-20.md diff --git a/.dsh/skills/csctalon-management-effectiveness/SKILL.md b/.dsh/skills/csctalon-management-effectiveness/SKILL.md deleted file mode 100644 index 244c4fd..0000000 --- a/.dsh/skills/csctalon-management-effectiveness/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: csctalon-management-effectiveness -description: CSCTalon 客服管理系统管理效果/管理闭环设计规范。定义例外管理、归因不归责、管理成本核算、周报自动化、预警驱动、绩效只汇总不扣分等模式,用于提升"符合管理效果"层面。发现"报表没人看/管理动作重/例外被淹没/绩效扣分争议/周报手工汇总"时使用。 -whenToUse: 设计管理看板、评审报表价值、规划绩效与预警、优化周报/复盘流程、梳理管理层体验时加载。 ---- - -# CSCTalon 管理效果/管理闭环设计规范 - -> 定位: 系统是解决问题的, 不是制造麻烦的。管理层体验 = 看结果不翻过程、管例外不管全量、催闭环不催人。 - -## 1. 例外管理(主管视角核心) - -- 主管首页只突出「需要动作」的项: 异常/超期/待回看/待审/积压, 正常数据折叠不打扰。 -- 看板默认口径: 红色=要处理, 黄色=快到期, 绿色=正常不显示。 -- 每个「例外」可一键跳转到处理页(详情/跟进/审核), 不要求主管跨页面拼信息。 - -## 2. 归因不归责(数据真实的地基) - -- 投诉审计/退款挽回/绩效数据**只做分析汇总, 不做扣分依据**。 -- 审计结论用于改进流程, 不挂钩个人绩效扣罚。 -- 系统内任何"审计/评估"文案都强调改进导向, 避免逼人"优化数据"。 - -## 3. 管理成本核算(砍掉没人看的报表) - -- 每张报表/看板必须回答: **谁看? 看完做什么决策?** 答不上就不上线。 -- 管理动作(登记/审计/跟进/报表)字段能简则简, 抓核心。 -- 已有报表定期审计: 使用频率低的合并/下线, 不累积僵尸页面。 - -## 4. 周报/复盘自动化 - -- 周报 10 项框架数据自动填充(接待/退款/投诉/措施/异常), 人只补结论。 -- 周例会只审到期措施 + 例外项, 不每周重复全量审。 -- 周报确认后固化快照, 可持续批注(活文档), 不下钻不强制派生报告。 - -## 5. 预警驱动(让异常自己浮现) - -- 预警规则可配置(退款激增/工单积压/异常激增/投诉激增), 调度器定时扫描。 -- 预警进待办中心 + 通知中心, 人从"盯数据"变"确认预警"。 -- 预警幂等(同日同规则只一条), 避免刷屏。 - -## 6. 绩效只汇总不扣分 - -- 绩效看板呈现工作量/效率/成功率**汇总与趋势**(纵向比进步), 不排名惩罚。 -- 人效算法口径(专项难度系数/本职转接)文档化, 数据变化可追溯解释。 -- 不把投诉/异常数据直接折算进个人扣分。 - -## 7. 管理效果评审清单(梳理管理层体验时逐项过) - -- [ ] 主管第一屏是否只突出例外(异常/超期/待审)? -- [ ] 是否每个例外一键可达处理页? -- [ ] 报表是否都能回答"谁看/看完做什么决策"? -- [ ] 是否有没人看的僵尸报表可下线? -- [ ] 周报是否自动填充、只补结论? -- [ ] 预警是否驱动处理而非人工盯数据? -- [ ] 绩效是否只汇总不扣分、口径可追溯? -- [ ] 管理动作字段是否最简? diff --git a/.dsh/skills/csctalon-ui-organization/SKILL.md b/.dsh/skills/csctalon-ui-organization/SKILL.md deleted file mode 100644 index aa402d0..0000000 --- a/.dsh/skills/csctalon-ui-organization/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: csctalon-ui-organization -description: CSCTalon 客服管理系统前端界面组织规范(Vue3+ElementPlus)。定义页面分层、四范式结构、菜单导航组织、状态色板、组件复用规则,用于新增页面与整体界面梳理。发现"界面组织混乱/菜单无序/风格不统一/组件重复"时使用。 -whenToUse: 新增前端页面、重构菜单导航、统一页面风格、评审界面组织合理性时加载。 ---- - -# CSCTalon 前端界面组织规范 - -> 适用技术栈: Vue 3 + Vite + Element Plus + ECharts + Pinia。目标是"不培训就会用、新页面照模板拼装"。 - -## 1. 页面分层(内容组织四层) - -任何页面按此四层组织,禁止跳层: - -1. **页头**: 页面标题(左) + 一句话副标题说明用途 + 主操作按钮(右, 右上角)。 -2. **筛选区**: 顶部卡片内放 FilterBar/筛选控件(状态/日期/关键词), 变更即刷新。 -3. **主体区**: 表格(列表页) 或 卡片栅格(看板页); 看板优先复用 dashboard 组件(KpiCard/ChartCard/SectionHeader)。 -4. **页脚区**: 分页 + 总数统计(列表页); 看板页可不设。 - -## 2. 四大交互范式(页面模板) - -| 范式 | 结构 | 适用 | -|------|------|------| -| 列表页 | 页头 + 筛选 + 表格 + 分页 + 导出 + 右上主按钮 | 异常/标签/发票/政策/排班/知识库 | -| 详情页 | 面包屑 + 元信息头(el-descriptions) + 主体 + 侧栏/底部时间线 | 异常详情/投诉审计/报告详情 | -| 表单页 | 必填 ≤3 项置顶 + 其余折叠"更多" + 保存草稿/提交 | 所有登记 | -| 导入页 | 模板下载 + 上传 + 预览复核 + 确认(回执成功/失败) | 标签批量/排班/发票 | - -## 3. 菜单与导航组织(侧边栏) - -- 一级菜单按角色可见性分层: 全员可见(运营看板/工作台/我的待办/数据中心/发票/知识库) → 管理层(决策看板/管理闭环) → 管理员(系统管理)。 -- 同一业务域的页面放同一子菜单(如 管理闭环: 报告/周报/措施/异常/排班/预警/跨部门), 不散落。 -- 菜单名与页面 title 一致; 命名避免歧义(如"设备专项标签"区别于"专项任务")。 -- 隐藏路由(hidden: true)用于详情页/跳转页, 不进菜单。 - -## 4. 全局状态色板(统一语义) - -| 状态族 | 颜色 | 语义 | -|--------|------|------| -| 待处理/待审/待回看 | warning(黄) | 需要动作 | -| 进行中/执行中 | primary(蓝) | 已启动 | -| 已完成/生效/闭环 | success(绿) | 结束 | -| 异常/失效/超期 | danger(红) | 需关注 | -| 归档/废止 | info(灰) | 只读留存 | - -统一封装在 `web/src/utils/statusColor.js`(已存在), 页面禁止私写配色。 - -## 5. 组件复用(禁止重复造轮子) - -- KPI 卡 → `KpiCard`; 图表卡 → `ChartCard` + `BaseChart`; 区块标题 → `SectionHeader`; 筛选条 → `FilterBar`; 三列队列 → `QueuePanel`。 -- 新页面先引入公共组件拼装, 禁止内联 KPI 卡/图表卡样式。 -- 需要新 props 时向后兼容扩展公共组件, 不在使用页私写覆盖类名。 - -## 6. 角色化工作台(第一屏差异化) - -| 角色 | 第一屏 | -|------|--------| -| 系统管理员 | 全局 KPI + 系统告警 + 待审队列 | -| 主管 | 全局 KPI + 例外清单(异常/超期/待回看) + 待审 | -| 专项组长 | 本专项 KPI + 本专项待办 + 本组排班 | -| 专员 | 本人待办 + 本人数据 + 被指派专项 | -| 普通客服 | 待读政策 + 本人接待 + 提交发票入口 | - -## 7. 审核清单(梳理界面时逐项过) - -- [ ] 每个页面是否四层结构完整? -- [ ] 是否落在四大范式之一? -- [ ] 菜单是否按角色分层、业务归组、命名无歧义? -- [ ] 状态色是否用统一色板? -- [ ] 是否复用了公共组件而非内联样式? -- [ ] 必填项是否 ≤3、其余折叠? -- [ ] 空数据是否有 el-empty 提示? -- [ ] 加载是否有 v-loading? diff --git a/.dsh/skills/csctalon-workflow-design/SKILL.md b/.dsh/skills/csctalon-workflow-design/SKILL.md deleted file mode 100644 index 8c57dbb..0000000 --- a/.dsh/skills/csctalon-workflow-design/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: csctalon-workflow-design -description: CSCTalon 客服管理系统办公流程/工作流设计规范。定义状态机、统一待办中心、通知提醒、闭环分级、跨模块联动(异常→措施→回看)等设计模式,用于业务流程规划与评审。发现"流程断点/状态混乱/该闭环未闭环/待办分散/无提醒"时使用。 -whenToUse: 设计新业务流程、评审现有流程是否闭环、规划状态机与待办通知、梳理跨模块联动时加载。 ---- - -# CSCTalon 办公流程/工作流设计规范 - -> 核心原则: 系统第一屏应「推着人走」而非「人到处找事」; 流程要闭环但分级, 不搞一刀切。 - -## 1. 流程设计五问(任何新流程先回答) - -1. **谁发起?** 录入入口是否 ≤30 秒、能自动则自动? -2. **谁处理?** 是否有明确负责人/接收人(缺省=进统一待办中心)? -3. **什么状态?** 状态机是否线性可预期(待处理→处理中→已完成/闭环)? -4. **何时提醒?** 到期/超期/新增是否有通知(站内信+角标)? -5. **如何闭环?** 结束态是否明确(关闭/归档/回看), 结果是否可追溯? - -答不上的流程=不完整, 不上线。 - -## 2. 状态机规范 - -- 状态用 String 枚举(避免 SA Enum 迁移麻烦), Pydantic 层校验。 -- 状态流转线性的: `待处理 → 处理中 → 已完成` 为主轴; `挂起/驳回/退回` 为分支。 -- 每个状态变更落时间线记录(谁/何时/从哪到哪/备注), 复用跟进记录表。 -- 状态色统一用 `statusColor.js` 色板(黄=需动作/蓝=进行中/绿=结束/红=异常/灰=归档)。 - -## 3. 统一待办中心(必做) - -- 汇聚所有「该我处理的事」: 待办事项 / 待跟进异常 / 待回看措施 / 待审发票报销 / 待读政策 / 被指派工单。 -- 每条带: 来源 + 优先级 + 截止 + 一键跳转; 按角色过滤(管理层看全部, 一线看本人)。 -- 新模块的"待处理"数据必须接入统一待办中心, 禁止散落各页面。 - -## 4. 通知提醒(轻量站内信) - -- 触发点: 到期(措施回看/异常跟进)、新增(异常登记/政策发布/审计指派)、状态变化(审核通过/被驳回)。 -- 落 `notifications` 表(recipient_id/type/title/content/ref_type/ref_id/is_read), 前端角标 + 列表。 -- 局域网内站内信即可, 不做外部推送。 - -## 5. 闭环分级(不过度管理) - -| 事项重要度 | 闭环要求 | -|-----------|---------| -| 小事/日常 | 一句跟进记录即闭环(记录>不记录) | -| 中事/影响有限 | 跟进 + 状态流转 + 到期回看 | -| 大事/影响大 | 完整流程: 登记 → 复盘 → 输出措施 → 到期回看 → 结论归档 | - -禁止对小事强制走完整流程(会逼人少登记, 数据失真)。 - -## 6. 跨模块联动(推荐链路) - -``` -异常事故(登记+跟进) - ├─→ 转待办(todo_items) 当需要人执行 - ├─→ 转管理措施(measures) 当需要持续改进/到期回看 - └─→ 转跨部门派发(upstream) 当根因在上游 -投诉审计 → 审计结论 → 转管理措施 -政策发布 → 通知相关角色 + 关联知识库话术同步 -发票提交 → 主管审核 → 报销 → 导出 -``` - -## 7. 流程评审清单(梳理流程时逐项过) - -- [ ] 五问是否全部可答? -- [ ] 状态机是否线性可预期、流转落时间线? -- [ ] 待处理项是否进统一待办中心? -- [ ] 到期/新增是否有通知? -- [ ] 结束态是否明确、可追溯? -- [ ] 是否按重要度分级闭环而非一刀切? -- [ ] 是否与异常/措施/待办/上游正确联动? -- [ ] 录入是否 ≤30 秒、能自动则自动? diff --git a/.gitignore b/.gitignore index ec8d9a3..e8b6d08 100644 --- a/.gitignore +++ b/.gitignore @@ -152,4 +152,6 @@ work-data/ certs/ # C7 工具私有状态: AI 工具记忆/会话状态, 写给工具自己用的不入库 .workbuddy/ +.trae/ +.dsh/ diff --git a/.trae/documents/327-mcp-readonly-export.md b/.trae/documents/327-mcp-readonly-export.md deleted file mode 100644 index f7185ce..0000000 --- a/.trae/documents/327-mcp-readonly-export.md +++ /dev/null @@ -1,81 +0,0 @@ -# #327 MCP 只读数据出口 — 实现方案 - -## 背景与目标 - -#319 P3 最后一步:对外(第三方/AI Agent)提供受控只读数据出口,替代各处临时拼接的不可审计接口。 - -**已确认边界**:SSE 传输 + 首批 2 个工具(device_behavior / recharge_traffic)+ 会话级只读兜底。排班考勤延后。 - -## 技术选型 - -- **不引新依赖**(无 mcp/fastmcp):用 FastAPI 原生 `StreamingResponse` 实现 SSE 端点,自实现 JSON-RPC 2.0 消息循环(MCP over SSE 协议)。 -- **认证**:复用 `external_service.authenticate` + `check_rate_limit` + `touch_last_used`。 -- **只读兜底**:每个工具调用前 `SET TRANSACTION READ ONLY`(PG 会话级硬约束),连接池归还时自动 reset。 -- **审计**:复用 `S.record_external_access`,每次 MCP 工具调用写 `system_events`。 - -## 文件变更 - -### 新增 - -1. **`backend/app/mcp/__init__.py`** — MCP 协议层(空,标记包) -2. **`backend/app/mcp/server.py`** — MCP SSE Server 核心 - - `McpSseServer` 类:管理 SSE 连接、JSON-RPC 消息路由、工具分发 - - `handle_message()`:JSON-RPC 2.0 入口(initialize / tools/list / tools/call) - - `tool_registry`:工具注册表(名称 → handler → schema → scope) - - `set_readonly(db)`:会话级只读兜底 -3. **`backend/app/mcp/tools.py`** — 首批 2 个工具实现 - - `device_behavior(sn)` → `ADS.supply_device_behavior()` - - `recharge_traffic(sn, start, end)` → `ADS.supply_recharge()` - - 每个工具声明:name / description / inputSchema / scope / handler -4. **`backend/app/api/v1/mcp.py`** — FastAPI 路由 - - `GET /mcp/sse` — SSE 端点(客户端连接,返回 `text/event-stream`) - - `POST /mcp/messages` — 客户端→服务器 JSON-RPC 消息(MCP over SSE 标准) - - 认证:从 query param `?token=` 或 `Authorization: Bearer` 取 token(SSE 连接无法带 Header,兼容两种) - - 限流/审计/scope 校验复用 `_auth` 模式 - -### 修改 - -5. **`backend/app/api/v1/router.py`** — `include_router(mcp_router, prefix="/mcp")` -6. **`backend/app/services/external_service.py`** — `SCOPES` 新增 `"mcp"`(MCP 专用 scope,与既有 device/recharge 区分,便于独立授权) - -### 测试 - -7. **`backend/tests/test_mcp_server.py`** — 单元测试 - - MCP initialize 握手 - - tools/list 返回白名单 - - tools/call 无 token → 401 - - tools/call 无效 scope → 403 - - tools/call device_behavior 正常返回 - - tools/call recharge_traffic 正常返回 - - 只读兜底验证(尝试写操作 → 报错) - -## 关键设计决策 - -| 决策 | 理由 | -|---|---| -| 不引 mcp/fastmcp 依赖 | 项目零 MCP 先例,最小实现降低风险;后续若需完整 MCP 生态可再引入 | -| 自实现 JSON-RPC 2.0 | MCP over SSE 协议简单(initialize/list/call 三类消息),200 行内可覆盖 | -| scope 新增 `"mcp"` | 与既有 device/recharge 区分,允许管理员单独授权 MCP 通道(可只给 MCP 不给 REST) | -| 只读兜底用 PG 会话级 | `SET TRANSACTION READ ONLY` 是数据库层硬约束,比应用层判断更可靠;连接池归还自动 reset 无副作用 | -| SSE 端点支持 query token | SSE 连接(EventSource)无法自定义 Header,兼容 `?token=` 与 `Authorization: Bearer` 两种 | - -## 安全约束 - -- 所有工具调用前强制 `SET TRANSACTION READ ONLY` -- 工具白名单硬编码在 `tools.py`,新增工具需显式注册(代码评审把关) -- 审计日志记录 tool_name + 参数摘要 + 调用方 token prefix -- 限流复用 Redis 固定窗口(每 token 独立计数) - -## 验证方案 - -1. **单元测试**:pytest 覆盖握手/鉴权/白名单/只读/正常调用 -2. **本地冒烟**:启动 uvicorn,用 `curl -N` 连 SSE 端点,验证 initialize → tools/list → tools/call 全流程 -3. **生产冒烟**:发布后用一个测试 token 调用 device_behavior,确认返回结构与 REST 端点一致 -4. **回归**:pytest 全量 0 失败 + ruff 通过 + C2 门禁 0 违规 - -## 验收对应 - -- [x] MCP 工具清单与白名单登记文档(`tools.py` 内注册表即文档,头部注释说明) -- [x] external_token 鉴权 + 只读兜底 + 审计日志落地 -- [x] 端到端:外部客户端经 MCP 查询 device_behavior + recharge_traffic 成功 -- [x] pytest 全量 0 失败 + ruff 通过 + 标准发布 diff --git a/.trae/documents/README.md b/.trae/documents/README.md deleted file mode 100644 index 745f1fc..0000000 --- a/.trae/documents/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# 说明 - -本目录下的设计文档已迁移至仓库根目录 `docs/arch/_pending/`(保留 git 历史)。 - -迁移目的:统一"AI 协作治理规范" 4.2 的架构文档资产化管理,设计文档进 `docs/arch/` 并接受架构锁定流程。 - -- 迁移日期:2026-09-01 -- 迁移去向:`docs/arch/_pending/`(状态 PENDING,待人工审核锁定) -- 原 20 份文档经 `git mv` 迁移,历史完整保留 diff --git a/.trae/documents/ai-assistant-performance-optimization.md b/.trae/documents/ai-assistant-performance-optimization.md deleted file mode 100644 index ed66329..0000000 --- a/.trae/documents/ai-assistant-performance-optimization.md +++ /dev/null @@ -1,169 +0,0 @@ -# AI 分析助手性能优化方案(规范流程版) - -## Context(为什么改) - -生产环境(192.168.10.21,DeepSeek `deepseek-v4-flash`)AI 助手用户反馈"分析耗时较长",但实际诉求是"按既定逻辑 + 系统数据做简单判断"。根因是**架构代差**:全量喂模型 + 让模型自由组织中文报告,简单问句也要走完整 LLM 链路。 - -### 生产实测(2026-09-12 ~ 09-15,16 条 `ai task done` 日志) - -| 指标 | 实测范围 | 平均 | 占比 | -|---|---|---|---| -| `build_ms`(数据底座组装) | 360-3042ms | ~700ms(热)/ 2s(冷) | ~15% | -| `llm_ms`(模型生成) | 1479-6893ms | ~4000ms | **~75%** | -| `total_ms` | 2188-9935ms | ~5000ms | 100% | - -**关键发现**(推翻前置假设): -1. `LLM_THINKING` 未显式配置 → 走默认 `disabled`(**非思考模式误配**,先前担心的 low/high 不存在) -2. **`llm_ms` 是绝对大头**(75%),`build_ms` 是次要矛盾 -3. `out_chars` 与 `llm_ms` 强正相关(2308 字符→6484ms;171 字符→1479ms)→ **输出长度直接决定耗时** -4. `AI_SYNC_WAIT_S=3.0` + `total_ms` 平均 5s → 几乎全走轮询 → 前端体感更长 - -**优化优先级**(数据驱动):层 1 降参数 > 层 3 规则层秒回 > 层 2 精确取数。 - ---- - -## 规范流程(按 AGENTS.md 工单/界面/文档纪律) - -| 环节 | 动作 | 凭证 | -|---|---|---| -| ① 工单 | 在 GitHub `tanzk/csctalon` 建 issue,标题"AI 助手性能优化:参数调优+规则层+精确取数",正文含本方案摘要 + 验收标准 | issue 号 `#XX`(实施时填) | -| ② 方案审核 | 本计划经人工终审(用户确认)后开工 | 计划文件 + 用户确认 | -| ③ 实现 | 每层独立 commit,提交信息带 `#XX` | commit sha | -| ④ 测试 | pytest + npm test + 生产灰度对比 | 测试输出 | -| ⑤ 关闭凭证 | 工单关闭前写 `## 关闭凭证核验 (YYYY-MM-DD)` 评论,逐条对应验收标准 + commit sha + 实测命令 | 评论 | - -**文档性质**:本方案属 **REQ**(有功能边界、验收标准、归属工单),非 META。 -**界面纪律**:层 3 规则层不改前端 UI(返回结构兼容),不触发界面设计说明三层落地;若后续加"规则层秒回"角标则需补设计说明。 - ---- - -## 层 1:参数调优(立竿见影,优先级最高) - -生产数据证明 `llm_ms` 是大头,降输入/输出 token + 提同步等待时间见效最快。 - -| 配置项 | 当前默认 | 改为 | 依据 | -|---|---|---|---| -| `AI_MAX_TOKENS` | 1500 | **800** | out_chars 实测多在 1200-2000,800 token 足够;省一半生成时间 | -| `AI_ANALYZE_MAX_CHARS` | 12000 | **6000** | 喂 4000 输入 token 降首 token 延迟 | -| `AI_SYNC_WAIT_S` | 3.0 | **8.0** | total_ms 平均 5s,提到 8s 让多数请求一次返回不走轮询 | -| `LLM_TIMEOUT_S` | 120.0 | 保持 | 云端生成长尾兜底 | - -**改文件**: -- [backend/app/core/config.py](file:///Users/tanzk/dev/csctalon/backend/app/core/config.py#L132-L180)(3 个默认值) -- [deploy/env.production.example](file:///Users/tanzk/dev/csctalon/deploy/env.production.example#L117-L127)(补 3 个示例值,避免生产 .env 与代码默认脱节——本次生产就是 .env 没显式配走默认值) - -**同步改生产**:SSH 192.168.10.21 `/opt/csctalon/.env` 追加 3 行 + `docker compose restart backend`。 - -**测试影响**:`test_ai_assistant.py` L77-L120 只断言结构/字段不断言长度,**不回归**。 - -**验收**:改后同问句 `total_ms` 应从 ~5s 降到 ~2.5s,多数请求走 `success` 不走 `running`。 - ---- - -## 层 2:问题路由 + 精确取数(次优先,build_ms 边际收益小但降输入 token) - -**复用 [ai_focus_service._KEYWORD_TO_SCOPE](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_focus_service.py#L31-L36)**(scope 值 `refund/complaint/reception` 与 feed 顶层段名**逐字符对齐**,见 [external.py:436-438](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L436-L438)),**不复用 `GLOSSARY_ROUTE`**(粒度错位)。 - -**改 [backend/app/services/ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py)**: -1. 新增 `_keyword_to_scope(question) -> str | None`:复用 `_KEYWORD_TO_SCOPE` 映射表(导出复用,勿复制防漂移) -2. 新增 `_select_feed_sections(feed, scope) -> dict`:恒量段(period/date/date_start/date_end/generated_at/_spec/range)始终保留 + 命中 scope 对应业务段;scope=None 回退全量(兼容) -3. `build_messages` 内:`_build_feed` → `_select_feed_sections` → `_trim_feed` - -**缓存策略**:brief 仍走 `_build_brief`(命中 `today:kpi`/`refund:do`/`complaint:*` 既有 key,**铁律 #11**),段选取只在内存 dict 切片零成本。**不**按段单独调聚合函数(today_kpi 一次出 reception+refund,分调反多一次 miss)。 - -**测试**:`test_build_messages_structure`(L78)走全量兜底不断言段,不回归;新增 `test_build_messages_scope_refund_only` 断言问"今日退款"→ feed 不含 complaint 顶层段。 - -**风险**:问"今日退款"保留整段 refund(含 `a_route.litigation_*`)不漏涉诉率。 - ---- - -## 层 3:简单判断走规则层(最贴合诉求,纯取数问句秒回) - -**入口**:[ai.py ai_analyze](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py#L174-L270) 内 `_require_enabled` 之后、`ai_task_manager.submit` 之前**短路**。命中规则 → 直接返回 `R(data={status:success, result_md, **window_info})`,**不入队** task_manager(毫秒级)。 - -**新增 `backend/app/services/ai_rule_engine.py`**,导出: -```python -def try_rule_answer(db: Session, question: str, eff: dict) -> str | None -``` -逻辑:关键词正则匹配 → 调 `ai_feed._build_feed`(命中缓存秒取,含坏缓存自愈)→ 取字段 → 模板拼 Markdown。未命中/取数为空 → 返回 None 回退 LLM 路径。 - -**模板放代码常量**(非 `ai_focus_configs`——那是 admin 可改的"主观偏好",规则模板是"客观取数",混放会让运维误改取数口径,违反 System 铁律精神 [ai_feed.py:22-34](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L22-L34))。 - -**首批规则**(关键词正则 → 模板): -- `今日|昨日|本周|本月.*(退款|提现).*(多少笔|笔数|情况|金额)` → `refund.total_count/total_amount/a_route.litigation_ratio` -- `...投诉.*(积压|登记|处理|多少)` → `complaint.{source}.registered/handled/backlog` -- `...接待.*(量|多少|情况|会话)` → `reception.total/by_specialty` - -**前端兼容**:返回结构与现有 `success` 一致,[AssistantDrawer.vue:328](file:///Users/tanzk/dev/csctalon/web/src/components/AssistantDrawer.vue#L312-L345) `if (d.status === 'success')` 天然命中,**零改**。 - -**测试**: -- `test_rule_engine_refund_count`(问"今日退款多少笔"→ 命中、返回含 total_count) -- `test_rule_engine_miss_falls_back_to_llm`(问"分析下经营策略"→ 返回 None) -- `test_rule_engine_respects_window`(问"昨日投诉积压"→ _AiWindow 解析昨日窗口,规则层取昨日数据) - -**风险**:模板数字必须来自 feed(禁编造);取数 None 时模板写"暂无"不写 0;规则层在 `_build_feed` 坏缓存自愈([ai_feed.py:70-81](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L70-L81))之后取数(同函数天然安全)。 - ---- - -## 实施顺序 - -1. **建工单** `#XX`(实施时填实际 issue 号) -2. **层 1**(1 commit):改 config.py + env.production.example + 同步生产 .env 重启 → 看日志 llm_ms 是否降 -3. **层 2**(1 commit):改 ai_feed.py + 新增测试 → pytest -4. **层 3**(1 commit):新增 ai_rule_engine.py + 改 ai.py 短路 + 新增测试 → pytest -5. **前端测试**:`npm test -- AssistantDrawer.spec.js` 不回归 -6. **生产灰度**:层 1 上线后对比同问句 total_ms;层 3 上线后"今日退款多少笔"应 <500ms -7. **关闭凭证核验**:工单评论逐条对应验收标准 + commit sha + 实测命令 - ---- - -## 验证方案 - -**后端**: -```bash -cd backend && pytest tests/test_ai_assistant.py -v -# 期望:现有用例全过 + 新增 3 个规则层/段选取用例过 -``` - -**前端**: -```bash -cd web && npm test -- AssistantDrawer.spec.js -# 期望:L105-L166 轮询/失败重试用例不回归 -``` - -**生产灰度**(层 1 后): -```bash -ssh tanzk@192.168.10.21 'cd /opt/csctalon && tail -f logs/backend/app.log | grep "ai task .* done"' -# 期望:llm_ms 从 ~4000 降到 ~2000,total_ms 从 ~5000 降到 ~2500 -``` - -**端到端**(层 3 后): -```bash -curl -X POST http://192.168.10.21:8000/api/v1/ai/analyze \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"question":"今日退款多少笔","period":"daily"}' -# 期望:<500ms 返回 success,不走 LLM -``` - ---- - -## 风险汇总 - -| 风险 | 缓解 | -|---|---| -| 规则层模板数字编造 | 模板取数必须来自 feed,None→"暂无" | -| 精确取数漏段 | refund 整段保留,`a_route.litigation_*` 天然在内 | -| 缓存 key 漂移 | 全部复用 `today:kpi`/`refund:do`/`complaint:*`,勿新建(铁律 #11) | -| 800 token 截尾 | 层 2 输入变小后输出同步收缩;复杂问句仍走 LLM | -| 生产 .env 改动风险 | 改前备份,改后 restart backend,失败可回滚 | - -## Critical Files - -- [backend/app/api/v1/ai.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py) — 层 3 规则层短路入口 -- [backend/app/services/ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) — 层 2 段选取 -- [backend/app/services/ai_focus_service.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_focus_service.py) — 复用 `_KEYWORD_TO_SCOPE`(L31) -- [backend/app/core/config.py](file:///Users/tanzk/dev/csctalon/backend/app/core/config.py) — 层 1 参数默认值(L132-L180) -- [deploy/env.production.example](file:///Users/tanzk/dev/csctalon/deploy/env.production.example) — 层 1 生产示例 -- `backend/app/services/ai_rule_engine.py` — 层 3 新增 -- [backend/tests/test_ai_assistant.py](file:///Users/tanzk/dev/csctalon/backend/tests/test_ai_assistant.py) — 测试基线 + 新增用例 -- `/opt/csctalon/.env`(生产)— 层 1 同步 diff --git a/.trae/documents/ai-feed-refactor-dual-layer-and-daily-grain.md b/.trae/documents/ai-feed-refactor-dual-layer-and-daily-grain.md deleted file mode 100644 index 757cccf..0000000 --- a/.trae/documents/ai-feed-refactor-dual-layer-and-daily-grain.md +++ /dev/null @@ -1,502 +0,0 @@ -# AI 助手数据底座重构:双层结构 + 日颗粒度 + DB 化 Prompt Seed - -## Context(为什么改) - -工单 #98 性能优化已上线(参数调优 + 段选取 + 规则层),但用户实测"昨日整体经营分析"报告暴露**数据源层 + Prompt 配置化**问题: - -1. **专项分布脏数据**:报告显示"内部投诉/外部投诉 暂无(lead 为空)0 会话/0 设备",根因是 [external.py:624-709](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L624) `_reception_specialty` 只过滤 `is_active=True`,但 specialties 表里这两条历史专项(已合并到"投诉专项")的 `is_active` 仍为 True。 -2. **数据不同源**:`ai_feed._build_feed` ([ai_feed.py:46-88](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L46)) 走 `external._build_brief`,自拼 `_reception_specialty`/`_complaint_enhanced`,与今日值班看板前端用的 `A.today_kpi`/`A.today_reception_breakdown`/`A.today_staff_efficiency` 不同源 → "看板一个数、AI 报告另一个数"。 -3. **缓存 TTL bug**:[external.py:419](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L419) `cached_call(..., 60, ...)` 固定 60s,历史日 brief 也 60s 过期重算。 -4. **AI 主观推断**:"处理量 238 > 登记量 190 → 在消化存量"是无证据归因。现有 `focus-complaint` seed 里甚至写了"处理滞后于登记说明积压恶化"——这条规则本身就是错的,需要 DB 层修正。 -5. **硬编码**:`_BUCKET` 映射 (external.py:633-640) 把专项名硬编码到 today_staff_efficiency 字段名;`LEGACY_COMPLAINT_SPECIALTIES` 常量靠名字硬编码过滤。 -6. **业务判定基准缺失**:AI 只列数字不下好坏判断;缺工作时间说明(08:00~23:59,非工作时间 AI 协助);这些本应是运营可改的 prompt seed,但当前写死在代码里就改不了。 - -用户确认方案:① 日报双层结构(顶层=今日看板原始数据,底层=AI 增量分析);② 周/月/区间按日颗粒度(汇总+日走势);③ 历史日懒加载快照(首次访问重算+24h 缓存,后续命中);④ **业务判定基准/工作时间/防主观推断铁律/AI 增量分析段全部 DB 化(运营可改可排序可启停),不写死代码**;⑤ **数据层去硬编码**(specialty is_active 治理 + efficiency_field 字段);⑥ 一次性完整改造 P0+P1。 - -**分层原则**(对齐既有设计文档 `docs/design/20260907-ai-assistant-focus-configurable-0.md`): -- **代码层(不可改)**:`_SYSTEM_PROMPT` 防幻觉底线 + `ai_glossary.GLOSSARY` 技术口径定义(三源分类/四路/登记处理口径/时区/金额单位/专项 vs 行政组长) -- **DB 层(可改)**:`ai_focus_configs` 表的 Focus seed(运营关注点/业务判定/工作时间/防主观推断指导/AI 增量分析段) - -## 关键文件 - -- [backend/app/services/ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) — `_build_feed` 改造 + `_select_feed_sections` 重写 + `_SYSTEM_PROMPT` 兜底铁律 + 周/月 `_range_feed` 新增 -- [backend/app/services/ai_glossary.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_glossary.py) — 不改动(已是技术口径代码常量,按设计分层保留) -- [backend/app/api/v1/external.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py) — `_reception_specialty` 移除 `_BUCKET` 硬编码改为读 `s.efficiency_field` + `_build_brief` TTL bug (line 419) -- [backend/app/services/cache_warmer.py](file:///Users/tanzk/dev/csctalon/backend/app/services/cache_warmer.py) — 新增 AI 路径 `today:staff:...:200:...` 预热条目 -- [backend/app/models/specialty.py](file:///Users/tanzk/dev/csctalon/backend/app/models/specialty.py) — Specialty 模型加 `efficiency_field` 字段 -- [backend/app/api/v1/ai.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py#L173) — `/ai/analyze` 入口(不改动,仅对齐参数) -- [backend/app/services/ai_focus_service.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_focus_service.py) — 不改动(现有 `_load_all`/`focus_for_question`/CRUD 已支持 DB 化 prompt seed 注入,新增 seed 自动生效) - -复用既有函数:`A.today_kpi` (analytics.py:3480) / `A.today_reception_breakdown` (analytics.py:4305) / `A.today_refund_breakdown` (analytics.py:5457) / `A.today_staff_efficiency` (analytics.py:5825) / `complaints.complaint_summary` / `complaints.complaint_backlog` / `cache_ttl_for` (redis_client.py) / `focus_for_question` (ai_focus_service.py:83) - ---- - -## A. 数据层去硬编码(P0 必做) - -### A.1 DB 治理:specialty is_active 标记 - -通过 alembic migration 把 specialties 表"内部投诉"/"外部投诉"两条记录的 `is_active` 置 False(DB 治理),代码层 `_reception_specialty` 只过滤 `is_active=True` 即可,不再依赖 `LEGACY_COMPLAINT_SPECIALTIES` 常量名硬编码。 - -**保留 `LEGACY_COMPLAINT_SPECIALTIES` / `COMPLAINT_SPECIALTIES_ALL` 常量**([analytics.py:1457-1461](file:///Users/tanzk/dev/csctalon/backend/app/services/analytics.py#L1457)):用于 SCD 历史归因查询(analytics.py:3087 / 3110),识别"曾经属于投诉类"的专项(包括已合并的),这是合理的口径定义不是硬编码。 - -### A.2 specialties 表加 `efficiency_field` 字段 - -[external.py:633-640](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L633) `_BUCKET` dict 把专项名硬编码到 today_staff_efficiency 的 sp_*/pp_* 字段名。改为 DB 字段: - -- Specialty 模型加 `efficiency_field: Mapped[str | None]` (String(64), nullable, comment="对应 today_staff_efficiency 的字段名, 格式 'sp_xxx,pp_xxx'") -- Alembic migration 给 6 个正式专项填值: - - 咨询接待 → `sp_zixun,pp_zixun` - - 套餐专项 → `sp_taocan,pp_taocan` - - 退款挽回 → `sp_recovery,pp_recovery` - - 退款处理 → `sp_processing,pp_processing` - - 投诉专项 → `sp_internal,pp_internal`(投诉类合并到此桶) - - 内部投诉/外部投诉 → NULL(is_active=False 不参与) -- `_reception_specialty` 改为从 `s.efficiency_field` 读字段名,移除 `_BUCKET` dict -- 未配置 `efficiency_field` 的专项兜底跳过 sessions/devices 统计(不再硬兜底到"咨询接待"桶,避免错误归并) - -### A.3 修复 `_reception_specialty` 兜底逻辑 - -原 [external.py:645-646](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L645) 注释里说"临时专项兜底到咨询接待桶"——这是错误归并。改为:`efficiency_field` 为空的专项(如临时专项、未配置字段)sessions/devices 显示 0 并在 `_spec` 注释说明"未配置 efficiency_field, 不计入会话/设备统计",不再硬兜底。 - ---- - -## B. 数据源切换(P0 必做:日报双层结构) - -### B.1 `ai_feed._build_feed` 改造 - -废弃走 `_build_brief` 路径,直接调今日看板服务函数,与前端今日看板页逐字段对齐。 - -```python -# ai_feed.py 新 _build_feed(db, period, date, start, end) -> dict -win = _window(period, date, start=start, end=end) # 复用 external._window -s, e = win["start"], win["end"] -today_data = { - "period": win["period"], "date": win.get("date"), - "date_start": win["date_start"], "date_end": win["date_end"], - "generated_at": _now_cn().isoformat(), - "kpi": cached_call(f"today:kpi:None:{s}:{e}", cache_ttl_for(s,e,60), A.today_kpi, db, None, s, e), - "reception": cached_call(f"today:reception:None:{s}:{e}", cache_ttl_for(s,e,60), A.today_reception_breakdown, db, None, s, e), - "refund": cached_call(f"today:refund:None:{s}:{e}", cache_ttl_for(s,e,60), A.today_refund_breakdown, db, None, s, e), - "complaint": _complaint_block(db, s, e), # 调 complaints.summary + backlog 路由本体 - "staff": _compact_specialty_staff(db, s, e), # today_staff_efficiency + _compact_specialty 压缩 - "_spec": _spec_brief(), -} -``` - -关键点: -- **同源同缓存**:复用今日看板路由层 key(`today:kpi:None:{s}:{e}` 等),与前端共享 Redis,"与看板完全一致"。 -- **TTL 自动感知**:`cache_ttl_for(s, e, 60)` 已实现历史日 24h、今天 60s。 -- **`_complaint_block`**:调 `complaints.complaint_summary` + `complaints.complaint_backlog`(已 60s 缓存),与今日看板投诉接口同源。 -- **`_compact_specialty_staff`**:调 `A.today_staff_efficiency(topn=200, aggregate_by="person")`(前端默认 topn=30,AI 用 200 拿更多人),再用现有 `_compact_specialty` 压缩 by_specialty 段(去逐人名单)。 -- **`today_queue` 不进 daily feed**:实时队列历史日为空,AI 不需要;在 `_spec.today_queue` 注释"实时队列, 历史日为空, AI 应跳过"。 - -### B.2 顶层 `today_data` 结构示意 - -```json -{ - "period": "daily", "date": "2026-09-14", - "date_start": "2026-09-14T00:00:00+08:00", "date_end": "2026-09-14T23:59:59+08:00", - "generated_at": "2026-09-15T10:00:00+08:00", - "kpi": { - "reception": {"value": 5850, "yoy_value": 6368, "yoy_pct": -8.1, "device_count": 2913, "breakdown": {...}, "ai_takeover": {...}}, - "refund_count": {"value": 256, "yoy_value": 230, "yoy_pct": 11.3}, - "refund_amount": {"value": 34016.20, "yoy_value": 27372.76, "yoy_pct": 24.3}, - "actual_cashflow": {"value": ..., "yoy_value": ...}, - "cash_routes_today": {"route_a": {...}, "route_b": {...}, "route_c": {...}, "route_d": {...}, "operational": ..., "diff": ...}, - "complaint": {"value": 190, "yoy_value": ...}, - "staff_on_duty": {"value": 8, "yoy_value": 9}, - "ticket_done": {"value": 200, "real_done_today": 50, "real_done_yoy": 45}, - "metric_levels": {"refund_amount": {"level": "warning", "target": ..., "reason": ...}} - }, - "reception": {"by_specialty": [{"specialty": "投诉专项", "lead": "洪佳", "lead_role": "专项负责人", "member_count": 5, "sessions": 9, "devices": 5}]}, - "refund": {"total_amount": {...}, "routes": {"route_a": {...}, ...}, "a_route": {"litigation_ratio": {...}, "litigation_amount": ...}}, - "complaint": {"total": {"registered": 190, "handled": 238, "backlog": 77}, "by_source": [{...}, {...}, {...}]}, - "staff": {"by_specialty": [...]}, - "_spec": {"money_unit": "元", "complaint": "registered=窗口内新登记...", "today_queue": "历史日为空"} -} -``` - -### B.3 `_select_feed_sections` 重写 - -新顶层结构变为 `kpi`/`reception`/`refund`/`complaint`/`staff`。`kpi` 内已含所有指标汇总+环比,单独切 `kpi` 就够答大多数问句。 - -新映射: -- `scope="reception"` → 保留 `kpi`(只含 reception 相关子字段)+ `reception` -- `scope="refund"` → `kpi`(refund_count/refund_amount/cash_routes_today/actual_cashflow)+ `refund` -- `scope="complaint"` → `kpi`(complaint)+ `complaint` -- `scope=None` → 全量 - -实现:`_select_feed_sections` 内对 `kpi` 做子字段过滤(按 scope 选 key),其余顶层段按 scope 直接切片。`_CONSTANT_FEED_KEYS` 扩展加入 `kpi`(裁剪后)。 - -### B.4 `_build_brief` 不删 - -[external.py:312-325](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L312) `external_brief` 路由对外开放(鉴权+scope 校验+access log),依赖 `_build_brief`。**保留 `_build_brief` 但做最小修复**:修 TTL bug (line 419);`_reception_specialty` 的修复已在 A 节完成(移除 `_BUCKET` + DB is_active 治理自动生效)。`ai_feed` 不再调它即可,二者解耦。 - ---- - -## C. 历史日懒加载快照 - -### C.1 复用 `today:*` 命名空间 - -不新建 `ai:snapshot:{date}`。理由: -1. `today:*` 已被今日看板前端、`cache_warmer` 预热,历史日首次访问大概率已命中。 -2. 设计目标"与看板完全一致"——共享 key 是强约束,隔离反而可能漂移。 -3. `cache_ttl_for` 已实现历史日 24h、今天 60s 自动切换。 - -### C.2 流程 - -``` -1. 用户问"昨天的情况" → ai.py /ai/analyze 解析出 date=昨天 -2. _build_feed 调 cached_call("today:kpi:None:2026-09-14T00:00:00:2026-09-14T23:59:59", 24h, A.today_kpi, ...) -3. Redis 未命中 → A.today_kpi 重算(_resolve_windows 内部 date=历史日 → is_realtime=False, 整天窗口) -4. cached_call 写回 Redis TTL=24h -5. 后续访问直接命中缓存, 毫秒级返回 -``` - -无需额外代码,复用 `cache_ttl_for` 即可。 - -### C.3 `today_queue` 等实时接口 - -历史日天然返回空(实时队列只看当前未结)。在 `_spec.today_queue` 注释明示"历史日为空,AI 应跳过该段不陈述"。`today_staff_efficiency` 历史日有值(按人聚合窗口内数据),正常用。 - ---- - -## D. 周/月/区间按日颗粒度 - -### D.1 双层结构 - -```python -range_data = { - "period": "weekly|monthly|range", - "date_start": win.start, "date_end": win.end, - "summary": _range_summary(db, s, e), # 窗口总量(复用 today_kpi 多日模式) - "daily_trend": _daily_trend(db, s, e), # 按日时序数组 - "_spec": _spec_brief(), -} -``` - -### D.2 汇总:复用 `today_kpi` 多日模式 - -`today_kpi(db, start=s, end=e)` 已支持多日窗口([analytics.py:3491](file:///Users/tanzk/dev/csctalon/backend/app/services/analytics.py#L3491) 调 `_resolve_windows` 走 start+end 分支),返回的 `value` 即窗口总量、`yoy_value` 即上一等长周期总量。**直接复用,不重新做 sum 各日**(避免双计/口径漂移)。 - -```python -def _range_summary(db, s, e): - return cached_call(f"today:kpi:None:{s}:{e}", cache_ttl_for(s,e,60), A.today_kpi, db, None, s, e) -``` - -### D.3 日走势数组 - -```python -def _daily_trend(db, s, e): - days = _days_range(s, e) # 北京时区日历日列表 - out = [] - for d in days: - ds, de = f"{d}T00:00:00", f"{d}T23:59:59" - k = cached_call(f"today:kpi:None:{ds}:{de}", cache_ttl_for(ds,de,60), A.today_kpi, db, None, ds, de) - c = _complaint_block(db, ds, de) # 复用 complaint 段 - out.append({ - "date": d, - "reception": k["reception"]["value"], - "devices": k["reception"]["device_count"], - "ai_takeover_pct": k["reception"]["ai_takeover"]["session_pct"], - "refund_count": k["refund_count"]["value"], - "refund_amount": k["refund_amount"]["value"], - "actual_cashflow": k["actual_cashflow"]["value"], - "complaint_registered": c["total"]["registered"], - "complaint_handled": c["total"]["handled"], - "complaint_backlog": c["total"]["backlog"], - }) - return out -``` - -JSON 样例: - -```json -"daily_trend": [ - {"date":"2026-09-08","reception":1200,"devices":800,"ai_takeover_pct":15.0,"refund_count":40,"refund_amount":9500.00,"actual_cashflow":9000.00,"complaint_registered":12,"complaint_handled":15,"complaint_backlog":28}, - {"date":"2026-09-09","reception":1180,"devices":790,"ai_takeover_pct":16.2,"refund_count":38,"refund_amount":8800.00,"actual_cashflow":8600.00,"complaint_registered":10,"complaint_handled":14,"complaint_backlog":24} -] -``` - -### D.4 月报 30 天 token 控制 - -30 天 × 9 字段 ≈ 270 数字 + JSON 结构字符 ≈ 2-3k token,可控。**但前提是不带成员名单/专项明细**——日走势只放关键指标,明细走 `summary` 段(一份)。 - -降级策略(超过 `AI_ANALYZE_MAX_CHARS`): -- 30 天 → 前 14 天逐日 + 后 16 天按周聚合(4 周),结构不变,date 标签换 `W37(09-08~09-14)`。 -- 仍超 → 只保留 6 个核心指标(reception / refund_amount / complaint_registered / complaint_handled / complaint_backlog / ai_takeover_pct),砍掉 devices / refund_count / actual_cashflow。 - -### D.5 与 `today_kpi` weekly/monthly 关系 - -复用 `today_kpi` 自身的多日模式拿窗口总量(start+end 一把调);日走势是额外的按日循环,不冲突。`today_kpi` 不需要改。 - ---- - -## E. Prompt 层 DB 化(核心改动:去硬编码 → ai_focus_configs seed) - -### E.1 分层原则(对齐既有设计文档) - -- **代码层(不可改)**:`_SYSTEM_PROMPT` 防幻觉底线 + `ai_glossary.GLOSSARY` 技术口径定义 -- **DB 层(可改)**:`ai_focus_configs` 表的 Focus seed(运营可改可排序可启停) - -### E.2 新增 4 条 ai_focus_configs seed(通过 alembic migration 插入) - -**Seed 1: business-benchmark**(业务判定基准) -```python -{ - "code": "business-benchmark", "scope_key": "global", "sort_order": 2, - "title": "业务判定基准", - "content": ( - "业务判定基准(方向性判据, 用于判定'好/坏'与'风险/机会'): " - "退款越少越好(金额/笔数); 均单价越低越好; 退款单数越少越好; " - "接待量越多越好; 接待会话数越多越好; " - "投诉越少越好; 投诉严重性 外部投诉 > 商户号投诉 > 内部投诉(即外部投诉 1 件 > 商户号 10 件); " - "投诉处理越及时越好(handle - register 间隔越短越好); " - "退款挽回率越高越好(挽回成功/分母); " - "积压投诉越低越好(窗口内登记未闭环). " - "陈述'环比涨跌'后必须按此基准标注'利好/利空', 不得只报数字不评判." - ), -} -``` - -**Seed 2: work-time-ai-assist**(工作时间与 AI 协助) -```python -{ - "code": "work-time-ai-assist", "scope_key": "global", "sort_order": 3, - "title": "工作时间与 AI 协助", - "content": ( - "客服工作时间 08:00~23:59; 非工作时间(00:00~07:59)由 AI 协助处理. " - "解读'接待量为 0 / 完工单为 0'时, 必须先核对窗口是否落在非工作时间, " - "不得直接判定'无人值班'或'系统故障'. " - "AI 接管率(ai_takeover)是可控指标前置: 接待量可升可控, AI 接管率上升=利好." - ), -} -``` - -**Seed 3: inference-discipline**(防主观推断指导) -```python -{ - "code": "inference-discipline", "scope_key": "global", "sort_order": 4, - "title": "防主观推断指导", - "content": ( - "防主观推断指导: " - "① 不同口径的数字(如 registered=窗口内新登记 vs handled=窗口内完成含历史单)禁止直接相减或比较大小; " - "必须显式标注口径差异后再下结论. " - "② 禁止做'处理量 > 登记量 → 消化存量'这类无证据因果推断; " - "如需描述关系, 只能说'本期 handled 高于 registered, 差额源于窗口外登记、窗口内办结的历史单'等可验证陈述. " - "③ 数据未提供的归因(如'退款上升因 XX 活动')禁止编造, 一律写'原因需结合运营侧确认'. " - "④ yoy_value/yoy_pct 是上一等长周期汇总, 不是另一天的独立明细, 不得当独立日数据展开陈述." - ), -} -``` - -**Seed 4: ai-incremental-analysis**(AI 增量分析任务段,开放题触发) -```python -{ - "code": "ai-incremental-analysis", "scope_key": "global", "sort_order": 5, - "title": "AI 增量分析任务", - "content": ( - "当用户问'分析/总览/怎么看/风险/异常'类开放题时(纯取数问句跳过本段), 增量分析任务: " - "- 口径差异提醒: 当数据里出现 registered 与 handled 这类不同口径数字(见口径卡), 必须显式标注口径, 禁止直接相减或推断因果; " - "- 环比异常预警: yoy_pct 超 ±30% 的指标, 必须点名并给出可能原因方向(数据/运营/外部, 不下结论); " - "- 风险与机会: 按 business_benchmark 卡的方向判定(如'退款金额上升=风险'), 每类最多 1 条; " - "- 下钻建议: 给出'看哪个看板卡片/下钻到哪一层'的可执行建议(不重复数据)." - ), -} -``` - -### E.3 更新现有 seed(修正错误口径) - -**修正 focus-complaint seed**(现有内容里有"处理滞后于登记说明积压恶化"是错误推断,且专项列表过时): -- 原文:"②登记(窗口新增)与处理(真实办结)要分开看, 处理滞后于登记说明积压恶化" -- 改为:"②登记(窗口新增)与处理(真实办结)是不同口径, 禁止直接相减或推断因果; 处理>登记属正常(含历史单办结), 不必然意味'消化存量'" - -**修正 focus-reception seed**(专项列表过时,内部/外部投诉已合并): -- 原文:"②六个专项(咨询/套餐/退款处理/退款挽回/内部投诉/外部投诉)的会话与设备规模" -- 改为:"②五个正式专项(咨询接待/套餐专项/退款处理/退款挽回/投诉专项)的会话与设备规模, 历史专项(内部投诉/外部投诉)已合并到投诉专项" - -### E.4 `_SYSTEM_PROMPT` 兜底铁律(不可改,防幻觉底线) - -在 [ai_feed.py:23-35](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L23) `_SYSTEM_PROMPT` 末尾追加(一句话兜底,详细在 Focus seed): - -``` -8. 防主观推断底线: 不同口径数字(如投诉 registered/handled)禁止直接相减或做因果推断; 禁止"处理量高→消化存量"这类无证据归因. -9. 业务判定方向: 退款↓利好 / 接待↑利好 / 投诉↓利好 / 挽回↑利好 / 积压↓利好; 比较后必须标注利好/利空. -10. 工作时间 08:00~23:59; 非工作时间由 AI 协助; 解读空值前先核对窗口时段. -``` - -### E.5 不改动 `ai_glossary.GLOSSARY` - -现有 8 个 key(complaint_sources/complaint_reg_handled/refund_routes/refund_litigation/reception_metric/timezone/money_unit/lead_systems)属于技术口径定义,按设计分层保留代码常量。`glossary_for_question` 的"基础卡强制注入"逻辑也保留(路由逻辑不是 seed 内容)。 - -### E.6 现有 `focus_for_question` 自动加载(无需改服务层) - -`ai_focus_service._load_all(db)` ([ai_focus_service.py:43-60](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_focus_service.py#L43)) 已实现 60s 进程缓存 + 按 scope_key 路由 + global 兜底全带。新 seed 写进 DB 后自动被加载,无需改服务层代码。运营通过 `/ai/focus-configs` REST 路由([ai.py:306-329](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py#L306))增删改启停。 - ---- - -## F. 前序修复(P0 必做) - -### F.1 修 `today_kpi` 缓存 TTL bug - -[external.py:419](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L419): - -```python -# 改前 -kpi = cached_call(f"today:kpi:None:{start_s}:{end_s}", 60, A.today_kpi, db, None, start_s, end_s) -# 改后 -kpi = cached_call(f"today:kpi:None:{start_s}:{end_s}", cache_ttl_for(start_s, end_s, 60), A.today_kpi, db, None, start_s, end_s) -``` - -第 424 行 `refund_decision_overview` 已正确用了 `cache_ttl_for`,对照即可。修后历史日 brief 不再 60s 过期重算。 - -### F.2 数据源切换是否自动解决 LEGACY 问题 - -部分解决:`ai_feed` 改走 `A.today_reception_breakdown` / `A.today_staff_efficiency`(这俩内部按 specialty_id 聚合,不依赖 LEGACY 常量),AI 报告里的专项分布自动干净。但 `external /brief` 仍依赖 `_reception_specialty`,必须靠 A 节的 DB 治理(is_active=False)+ efficiency_field 字段移除 `_BUCKET` 硬编码。 - ---- - -## G. 风险与回归 - -### G.1 `_build_brief` 依赖方 - -- `external_brief` 路由(external.py:312-325):对外开放,鉴权+access log,**不能删**。 -- 修复策略:保留 `_build_brief` 函数体,只修 TTL bug (F.1);`_reception_specialty` 的修复靠 A 节 DB 治理自动生效;`ai_feed` 不再调它即可,二者解耦。 -- 回归:`curl -H "Authorization: ..." /api/v1/external/brief?period=daily` 验证专项分布不再出"内部投诉/外部投诉"。 - -### G.2 LLM token 上限 - -- 单日 feed:`today_kpi` 全量约 1-2k 字符 + reception/refund/complaint/staff 各几百字符,压缩后约 4-5k 字符(< `AI_ANALYZE_MAX_CHARS=6000` 默认),可控。 -- 月报 30 天日走势:约 2-3k token,加 summary 段共约 5-6k token,仍可控。 -- 风险点:`today_staff_efficiency` 的 `data` 数组(200 人 × 多字段)会爆。**日走势里不带 staff.data**,只带 `summary.staff_on_duty`;staff 明细只在 daily 模式且 scope=reception 时带(且经 `_compact_specialty` 压缩)。 - -### G.3 缓存 key 冲突 - -`ai_feed` 改造后**共享**今日看板的 `today:*` key(设计目标"与看板完全一致"),不冲突。但要注意: -- `today:staff:None:{s}:{e}:200:0:person:None:None:None` 这个 key 与前端今日看板 staff 接口默认参数(topn=30)对齐不一致——AI 用 topn=200 会生成不同 key,**这是设计上的隔离**(AI 要更多人),需在 `cache_warmer.py` 加预热条目(否则 AI 路径冷加载)。 -- 不需要在 `ai_feed` 里再做"_compact_specialty 自愈删坏缓存"逻辑([ai_feed.py:62-82](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L62))——`today_kpi` 直接调,无 `_build_brief` 的 routes 拼装环节,坏缓存场景消失。但保留自愈无害,可留作安全网。 - -### G.4 DB 治理的回滚风险 - -- 把"内部投诉"/"外部投诉"两条记录的 `is_active=False` 是**数据修复**,对应 alembic migration 的 downgrade 应该把 is_active 恢复为 True。 -- specialty 表加 `efficiency_field` 字段是 nullable,未配置的专项不影响现有逻辑(_reception_specialty 兜底跳过 sessions/devices 统计)。 -- 现有 `COMPLAINT_SPECIALTIES_ALL` 常量仍包含 LEGACY 名字,用于 SCD 历史归因查询——保持不变,不影响。 - -### G.5 Focus seed 注入顺序 - -新增 4 条 seed 的 sort_order:focus-global=1 / business-benchmark=2 / work-time-ai-assist=3 / inference-discipline=4 / ai-incremental-analysis=5。`focus_for_question` 按 sort_order 排序注入,确保基准/工作时间/防推断/增量段顺序合理。 - ---- - -## H. 实施步骤(一次性完整改造 P0+P1) - -### 步骤 1: DB 治理 alembic migration(数据层去硬编码) - -新建 alembic migration: -- upgrade:① specialties 表加 `efficiency_field` 字段(String(64), nullable);② 给 6 个正式专项填 efficiency_field 值;③ 把"内部投诉"/"外部投诉"两条记录的 is_active 置 False;④ ai_focus_configs 表插入 4 条新 seed(business-benchmark / work-time-ai-assist / inference-discipline / ai-incremental-analysis);⑤ UPDATE focus-complaint / focus-reception 两条现有 seed 修正口径。 -- downgrade:反向操作。 - -验证:`docker exec csctalon-db psql -U postgres -d csctalon -c "SELECT name, is_active, efficiency_field FROM specialties ORDER BY display_order;"` — 应见 5 条 is_active=True + 2 条 is_active=False;`SELECT code, scope_key, sort_order, enabled FROM ai_focus_configs ORDER BY sort_order;` — 应见 8 条 seed。 - -### 步骤 2: Specialty 模型加 efficiency_field 字段 - -改 [backend/app/models/specialty.py](file:///Users/tanzk/dev/csctalon/backend/app/models/specialty.py) Specialty 模型,加 `efficiency_field: Mapped[str | None]` (String(64), nullable, comment="对应 today_staff_efficiency 的字段名, 格式 'sp_xxx,pp_xxx'")。 - -### 步骤 3: 修 `_reception_specialty` 移除 `_BUCKET` 硬编码 - -改 [external.py:624-709](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L624) `_reception_specialty`: -- 移除 `_BUCKET` dict (line 633-640) -- 改为从 `s.efficiency_field` 读字段名(如 `sp_zixun,pp_zixun` 分割得到 sp_field/pp_field) -- `efficiency_field` 为空的专项 sessions/devices 显示 0,不再硬兜底到"咨询接待"桶 -- 由于 DB 已把"内部投诉"/"外部投诉" is_active=False,`select(Specialty).where(Specialty.is_active.is_(True))` 自动排除,不再需要 `~Specialty.name.in_(LEGACY_COMPLAINT_SPECIALTIES)` 过滤 - -### 步骤 4: 修 `today_kpi` 缓存 TTL bug - -改 [external.py:419](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py#L419):`60` → `cache_ttl_for(start_s, end_s, 60)`。 - -### 步骤 5: `ai_feed._build_feed` 改造为直接调今日看板服务 - -改 [ai_feed.py:46-88](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L46): -- 替换 `_build_brief` 调用为直接调 `A.today_kpi` / `A.today_reception_breakdown` / `A.today_refund_breakdown` / `complaints` summary/backlog -- 新增 `_complaint_block(db, s, e)` helper(调 complaints 路由本体) -- 新增 `_compact_specialty_staff(db, s, e)` helper(调 today_staff_efficiency topn=200 + _compact_specialty) -- 保留 `_compact_specialty`(仍需压缩 staff by_specialty) -- 重写 `_select_feed_sections` 按 B.3 新映射 - -### 步骤 6: `_SYSTEM_PROMPT` 追加兜底铁律 - -改 [ai_feed.py:23-35](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py#L23) `_SYSTEM_PROMPT` 末尾追加铁律 8/9/10(E.4)。 - -### 步骤 7: 周/月/区间按日颗粒度 - -改 [ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) 新增: -- `_range_feed(db, period, start, end)` → 组装 summary + daily_trend + _spec -- `_daily_trend(db, s, e)` → 按日循环调 today_kpi + complaint_block -- `_days_range(s, e)` → 北京时区日历日列表 helper -- `build_messages` 按 period 分流:daily → `_build_feed`;weekly/monthly/range → `_range_feed` -- 月报 token 降级策略(D.4) - -### 步骤 8: cache_warmer 预热 AI 路径的 staff topn=200 key - -改 [cache_warmer.py](file:///Users/tanzk/dev/csctalon/backend/app/services/cache_warmer.py) `_warm_analytics` 加 `today:staff:None:{s}:{e}:200:0:person:None:None:None` 预热条目。 - -### 步骤 9: 部署 + 验证 - -```bash -# 部署到生产 -ssh tanzk@192.168.10.21 "cd /opt/csctalon && sudo ./upgrade.sh -git" - -# 验证 1: 专项分布修复 -ssh tanzk@192.168.10.21 "sudo docker exec csctalon-backend python -c \" -from app.api.v1.external import _reception_specialty -from app.core.database import SessionLocal -db = SessionLocal() -r = _reception_specialty(db, '2026-09-14T00:00:00', '2026-09-14T23:59:59') -print([(s['specialty'], s['sessions']['value'], s['devices']['value']) for s in r]) -\"" -# 期望:5 条正式专项(咨询接待/套餐专项/退款处理/退款挽回/投诉专项),不再含'内部投诉'/'外部投诉' - -# 验证 2: TTL 修复 -ssh tanzk@192.168.10.21 "sudo docker exec csctalon-db redis-cli TTL 'today:kpi:None:2026-09-14T00:00:00:2026-09-14T23:59:59'" -# 期望:86400 附近 - -# 验证 3: Focus seed 注入 -ssh tanzk@192.168.10.21 "sudo docker exec csctalon-db psql -U postgres -d csctalon -c \"SELECT code, scope_key, sort_order, enabled FROM ai_focus_configs ORDER BY sort_order;\"" -# 期望:8 条 seed(4 条原有 + 4 条新增),全 enabled=true - -# 验证 4: AI 报告不再有"消化存量"推断 + 带利好/利空标注 -# 前端发"昨日整体经营分析"问句,检查 AI 输出: -# - 含"registered 与 handled 不同口径"显式说明 -# - 含利好/利空标注(如"退款金额+24.3% 利空") -# - 不再出现"处理量高→消化存量"推断 -# - 含工作时间核对(如"08:00~23:59 工作时间") - -# 验证 5: AI 数据与今日看板一致 -# 前端今日看板同窗口对比 AI 报告数字 - -# 验证 6: 周/月报按日颗粒度 -# 前端发"本周接待量走势",检查 AI 输出 7 天日走势 + 窗口总量 -# 前端发"本月经营总览",检查 AI 输出 30 天日走势或降级为前 14 天 + 后周聚合 - -# 验证 7: cache_warmer 预热 AI 路径 -ssh tanzk@192.168.10.21 "sudo docker exec csctalon-db redis-cli KEYS 'today:staff:None:*:200:0:person:None:None:None'" -# 期望:返回 1 个 key(今日)+ 1 个 key(昨日,历史日 24h 缓存) -``` - ---- - -## 关键文件清单 - -### 必须修改 -- /Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py — `_build_feed` 改造 + `_select_feed_sections` 重写 + `_SYSTEM_PROMPT` 兜底铁律 + 周/月 `_range_feed`/`_daily_trend`/`_days_range` 新增 -- /Users/tanzk/dev/csctalon/backend/app/api/v1/external.py — `_reception_specialty` 移除 `_BUCKET` 改读 `s.efficiency_field` (line 624-709) + `_build_brief` TTL bug (line 419) -- /Users/tanzk/dev/csctalon/backend/app/models/specialty.py — Specialty 模型加 `efficiency_field` 字段 -- /Users/tanzk/dev/csctalon/backend/app/services/cache_warmer.py — 新增 AI 路径 staff topn=200 预热条目 - -### 新建 -- /Users/tanzk/dev/csctalon/backend/alembic/versions/xxx_ai_feed_refactor.py — DB 治理 migration(specialty 字段+数据修复 + ai_focus_configs 新 seed + 现有 seed 修正) - -### 不修改(已对齐分层) -- /Users/tanzk/dev/csctalon/backend/app/services/ai_glossary.py — 技术口径代码常量(按设计分层保留) -- /Users/tanzk/dev/csctalon/backend/app/services/ai_focus_service.py — DB 化 Focus 服务(已支持,无需改) -- /Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py — `/ai/analyze` 入口(参数已对齐) diff --git a/.trae/documents/ai-report-optimization-plan.md b/.trae/documents/ai-report-optimization-plan.md deleted file mode 100644 index 1ccafdf..0000000 --- a/.trae/documents/ai-report-optimization-plan.md +++ /dev/null @@ -1,151 +0,0 @@ -# AI 报告系统优化方案 - -## Context - -当前 AI 报告系统已完成多 provider 框架(#127-#131):122b 专供预生成、27b 供实时问答。但存在三个核心问题: -1. **对比类报告含当天不完整数据**——周报/月报用"本周一到此刻"做窗口,当天才过了几小时就拿来和昨天全天比,口径不对等 -2. **命中=返回全文,未命中=实时跑27b**——用户只想看退款部分却拿到整份报告,或未命中走27b实时分析(质量不如122b预生成草稿) -3. **缺少统一 AI 管理入口**——模板、模型、报告分散在不同页面 - -用户确认分三批实施,本方案覆盖全部三批。 - ---- - -## 批次 A:对比报告截止前一天 + trigger_at 调半夜 - -### A1. 新增窗口计算模块 - -**新建** `backend/app/services/ai_report_window.py`: -- `compute_pregen_window(period, ref_date=None) -> dict`:纯计算,无 IO - - weekly: 本周周一 ~ 昨天;若今天是周一→退化为上周一~上周日 - - monthly: 本月1日 ~ 昨天;若今天是1号→退化为上月1日~上月末 - - daily: 返回 None(日报保持现行 date=None 实时数据) - - 返回 `{start, end, snapshot_date, label}` -- `compute_compare_window(start, end) -> tuple[str, str]`:等长前对比窗口(紧贴 start 之前取同长度天数) - -### A2. 改 generate_for_template - -**改** [ai_report_worker.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_report_worker.py) L196-211: - -weekly/monthly 模板不再走 `build_messages(period=template.period, date=None)`,改为: -- 调 `compute_pregen_window(template.period)` 算出 start/end -- 调 `build_messages(period="range", date=None, start=start, end=end)` - - `_window` 在 start/end 模式下 end < today → `date_end = f"{e}T23:59:59+08:00"` 整天,`is_realtime=False`([external.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/external.py) L360-366) -- 落库 `save_report(start=start, end=end, snapshot_date=今日)`,snapshot_date 保持今日让 matcher 命中 -- daily 模板保持现行 `date=None`(随同步刷新,当天实时数据不变) - -### A3. trigger_at 调半夜 + warm_idle 时段放宽 - -**改** [ai_report_worker.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_report_worker.py): -- `warm_idle()` L438:移除 `_DAYTIME_HOURS` 限制(当前8-23才扫描,凌晨不触发 trigger_at) -- `warm_on_startup()` L405:保持白天限制(避免重启时大量并发) -- **Alembic 迁移**:`UPDATE ai_report_templates SET trigger_at='1-02:00' WHERE code IN ('weekly_compare_last','monthly_overview')` -- daily_general_morning 保持 `09:00`(refresh_on_sync 覆盖白天实时刷新) - -### A4. feed 增对比段(可选,A 批先不接入) - -`_build_feed` 已支持 `start/end` 透传([ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) L92),`_enrich_with_daily_trend` 在 start/end 模式下自动走日走势路径(L124-130)。等长对比窗口在批次 B 随草稿结构化一起接入。 - ---- - -## 批次 B:草稿结构化 + 摘取层 + 三级语义命中 - -### B1. 草稿结构化标记 - -**段落锚点(全树统一)**: -- `## 接待分析` / `## 退款分析` / `## 投诉分析` / `## 趋势对比` / `## 结论建议` - -**改** [ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) `_SYSTEM_PROMPT` L35-58 第3条:增加"周报/月报必须按 H2 锚点结构化输出"约束。 - -**Alembic 迁移**: -- `UPDATE weekly_compare_last SET max_tokens=4000`(当前2000不够完整草稿) -- `UPDATE monthly_overview SET max_tokens=5000` -- `UPDATE daily_general_morning SET max_tokens=2500` - -### B2. 新增摘取服务 - -**新建** `backend/app/services/ai_extract_service.py`: -- `split_sections(answer_md) -> dict[str, str]`:按 `## ` H2 锚点切分,兜底按段落分 -- `extract_section(db, question, report, template) -> dict`: - 1. split_sections 切分草稿 - 2. 关键词路由:`keywords_to_scopes(question)` → scope → 段落(复用 [ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) L497 `_select_feed_sections` 同款逻辑) - 3. 调 27b(`provider_code=None` 走 default 链,`timeout_s=30`,`max_tokens=1200`):从命中段落摘取+按问句排版 - 4. 输出 JSON `{result_md, confidence, need_clarify, clarify_options}` - -### B3. 新增语义匹配服务 - -**新建** `backend/app/services/ai_semantic_matcher.py`: -- `semantic_match(db, question, candidates) -> dict`:27b 判定问句与候选报告的语义相关性,输出 `{hit_report, confidence}` - -### B4. 改命中链路 - -**改** [ai.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/ai.py) L272-299: - -``` -L1 关键词匹配(现行 match_template_report 不变) - → 命中 → 走摘取层(不返回全文) - → 未命中 ↓ -L2 27b 语义匹配(semantic_match, confidence≥0.6 命中) - → 命中 → 走摘取层 - → 未命中 ↓ -L3 兜底: 返回 clarify_options(当日报告 H2 标题列表) - 或走原实时 LLM 路径(保留兼容) -``` - -### B5. 前端改造 - -**改** [AssistantDrawer.vue](file:///Users/tanzk/dev/csctalon/web/src/components/AssistantDrawer.vue): -- 新增 `status === "clarify"` 状态:渲染 `clarify_options` 为可点按钮,点击后重新发 /ai/analyze - ---- - -## 批次 C:月报逐天小时粒度 + AI 管理页面 - -### C1. feed 逐天小时粒度 - -**改** [ai_feed.py](file:///Users/tanzk/dev/csctalon/backend/app/services/ai_feed.py) `_enrich_with_daily_trend`: -- 在 `span > 14`(月报路径)下,对 daily_trend 每条额外调 `cached_call(f"today:hourly:None:{d}:{d}", ..., A.today_hourly_trend, ...)` 取当日 hourly -- 新增 `_compact_daily_hourly_summary(day_hourly) -> dict`:每天只取工作时段合计 + 峰谷小时(~6标量/天 × 30天 = 180字段,~6KB) - -### C2. AI 管理页面 - -**新建** `web/src/views/system/AiAdmin.vue`(5 Tab): -- Tab 1 报告列表:复用 `/admin/ai/reports` API([admin_ai.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/admin_ai.py) L160) -- Tab 2 模板管理:内嵌 [AiTemplates.vue](file:///Users/tanzk/dev/csctalon/web/src/views/system/AiTemplates.vue) -- Tab 3 模型/算力节点:内嵌 LlmProviders 管理组件([llm_providers.py](file:///Users/tanzk/dev/csctalon/backend/app/api/v1/llm_providers.py) API) -- Tab 4 提示词管理:内嵌 AiFocusConfigs + 模板 question_template 编辑 -- **设计说明三层落地**(AGENTS.md 铁律 7):代码注释 + 页面 ⓘ + review-checklist - -**改** `web/src/router/index.js`:新增 `system/ai-admin` 路由 - -### C3. 维度扩充 - -先与用户确认扩充清单再实施(AGENTS.md:不确定停下来提问)。 - ---- - -## 工单规划 - -| 工单 | 批次 | 标题 | 关键文件 | -|---|---|---|---| -| #132 | A | 周报/月报预生成窗口截止前一天 | ai_report_worker.py, ai_report_window.py(新) | -| #133 | A | trigger_at 调半夜 + warm_idle 凌晨可触发 | ai_report_worker.py, 迁移 f8g9h0i1j2k3 | -| #134 | B | 草稿结构化标记 + 模板 max_tokens 放宽 | ai_feed.py(_SYSTEM_PROMPT), 迁移 g3h4i5j6k7l8 | -| #135 | B | 摘取层 + 三级语义命中链路 | ai_extract_service.py(新), ai_semantic_matcher.py(新), ai.py, AssistantDrawer.vue | -| #136 | C | 月报逐天小时粒度 | ai_feed.py(_compact_daily_hourly_summary) | -| #137 | C | AI 管理页面(5 Tab) | AiAdmin.vue(新), router/index.js | -| #138 | C | 数据维度扩充(先确认清单) | ai_feed.py | - -**依赖**:#132→#133(批次A) | #134→#135(批次B) | #136/#137/#138 独立(批次C) - -## 验证方式 - -每批完成后: -1. **ruff + pytest**:`ruff check backend/ && pytest -k ai` -2. **本地 docker cp 到 csctalon-backend 容器验证** -3. **部署生产**:`ssh tanzk@192.168.10.21 "sudo /opt/csctalon/current/deploy/upgrade.sh --git"` -4. **端到端实测**: - - A:手动触发周报预生成,查 `ai_reports.start/end` 截止昨天、trigger_at=02:00 凌晨自动触发 - - B:用户提问"退款情况"→命中预生成→只返回退款段落(非全文);提问模糊→返回 clarify 选项 - - C:月报 feed 含 `hourly_summary`;AI 管理页面 5 Tab 可用 -5. **凭证评论关闭工单**(逐条可复现核验方式) diff --git a/.trae/documents/complaint-and-specialty-improvements.md b/.trae/documents/complaint-and-specialty-improvements.md deleted file mode 100644 index 396a0cb..0000000 --- a/.trae/documents/complaint-and-specialty-improvements.md +++ /dev/null @@ -1,174 +0,0 @@ -# 投诉与专项优化实施方案 - -## Context - -用户反馈运营看板和人员管理的四个问题: -1. 人员编辑的专项可选择项不全(只有3个硬编码选项),需做成可配置字典 -2. 投诉子类分布将内部/商户号/外部平台混在一起,需分开展示 -3. 投诉处理人排行只显示内部投诉,需单列外部投诉表 -4. 未闭环投诉显示"200条"但列表为空,数据不一致 - ---- - -## 问题1: 人员编辑专项选项不全 → 改为动态字典 - -### 现状 -- 后端 `STANDARD_SPECIALTIES` (specialty.py:25-32) 定义6个专项,API `/specialties` 已返回完整列表 -- 前端 `Persons.vue` 已加载 `specialtiesList` 用于筛选下拉框(第95行) -- 但任职分配表单(第470-472行)和批量编辑表单(第533-534行)硬编码了3个选项 - -### 修改 -**文件: `web/src/views/system/Persons.vue`** - -1. 第470-472行,将硬编码替换为动态选项: -```html - - - -``` - -2. 第533-534行,同样替换: -```html - - - -``` - -### 验证 -- 打开人员管理 → 点击编辑 → 专项下拉框应显示6个选项(咨询接待/套餐专项/退款处理/退款挽回/内部投诉/外部投诉) -- 批量编辑对话框同样包含完整选项 - ---- - -## 问题2: 投诉子类分布 → 按三大类分开三张饼图 - -### 现状 -- 后端 `complaint_subs` 数据已包含 `source_type` 字段(内部投诉/商户号投诉/外部投诉) -- 前端 `complaintSubPieOption` (Today.vue:1437-1486) 已按 source_type 分组着色,但所有子类混在一个饼图中 - -### 修改 -**文件: `web/src/views/dashboards/Today.vue`** - -1. 模板中(第227-252行),将单个饼图替换为三列并排: - - 内部投诉子类(商品/服务)→ 蓝色系 - - 外部投诉-商户号(微信投诉)→ 紫色系 - - 外部投诉-平台(12315/工信部/消协等)→ 红色系 - -2. 将原 `complaintSubPieOption` 替换为三个 computed: - - `complaintSubInternalPieOption` — 过滤 `source_type === '内部投诉'` - - `complaintSubMerchantPieOption` — 过滤 `source_type === '商户号投诉'` - - `complaintSubExternalPieOption` — 过滤 `source_type === '外部投诉'` - -3. 提取公共饼图构建函数 `buildSubPieOption(rows, colorPalette, sourceLabel)` 避免重复 - -4. 投诉根因柱图移至投诉处理人排行上方独占一行 - -### 验证 -- 运营看板选择有投诉数据的日期,确认显示三个独立饼图 -- 内部投诉饼图:商品投诉、服务投诉 -- 商户号投诉饼图:微信投诉 -- 外部平台投诉饼图:全国12315平台、工信部投诉、公共服务平台、消费者协会 - ---- - -## 问题3: 投诉处理人排行 → 拆分为内部/外部两表 - -### 现状 -- 后端 SQL(analytics.py:3249-3319)UNION ALL 三张表,返回 `source_type` 区分来源 -- 前端显示单个表格,包含"按来源"列(内部/外部/卡商) -- 用户要求因数量级不同分开展示 - -### 修改 - -**后端: `backend/app/services/analytics.py`** - -1. 在现有 handler 聚合逻辑(第3323-3366行)之后,新增按 source_type 拆分: - - `complaint_internal_handlers` — 仅 `source_type == '内部投诉'` - - `complaint_external_handlers` — `source_type == '外部投诉'` + `'卡商群投诉'` - -2. 在返回字典中(第3654-3665行)新增两个字段: - - `complaint_internal_handlers` - - `complaint_external_handlers` - - 保留原 `complaint_handlers` 向后兼容 - -**前端: `web/src/views/dashboards/Today.vue`** - -3. complaintData 初始化(第2121行)新增字段: - - `complaint_internal_handlers` - - `complaint_external_handlers` - -4. 模板中(第265-332行)替换为两个并列表格: - - 左:内部投诉处理人排行(Top 15) - - 右:外部投诉处理人排行(Top 15) - -### 验证 -- 运营看板确认显示两个独立处理人排行表 -- 内部表只含 service_tickets 内部投诉处理人 -- 外部表含 external_complaints + card_complaints 处理人 - ---- - -## 问题4: 未闭环投诉列表为空 - -### 现状 -- 用户看到"未闭环 200条"标签但列表无数据 -- 后端 `complaint_pending_total` 和 `complaint_pending_list` 来自同一变量 `complaint_pending`,理论上应一致 -- `_INCLUDE_COMPLAINT_FILES = False`(analytics.py:17)影响 complaint_sub/roots 聚合,但不影响 complaint_pending 的三表直接查询 - -### 修改 - -**后端: `backend/app/services/analytics.py`** - -1. 在 complaint_pending 构建完成后(第3644行),增加数据一致性校验日志: -```python -import logging -_log = logging.getLogger(__name__) -if complaint_pending_total > 0 and len(complaint_pending) == 0: - _log.warning(f"complaint_pending 数据不一致: total={complaint_pending_total}, list空") - complaint_pending_total = 0 -``` - -2. 在 complaint_pending 查询的 external_complaints/card_complaints 部分(第3495行前)增加注释,说明这些查询不受 `_INCLUDE_COMPLAINT_FILES` 影响 - -**前端: `web/src/views/dashboards/Today.vue`** - -3. complaintData 赋值(第2129-2130行)增加防御性校验: -```javascript -complaint_pending_total: rdata.complaint_pending_total ?? null, -complaint_pending_list: (() => { - const list = rdata.complaint_pending_list || [] - const total = rdata.complaint_pending_total - if (total > 0 && list.length === 0) { - console.warn('[complaint_pending] 数据不一致: total=' + total + ', list为空') - } - return list -})(), -``` - -4. 模板中(第369-376行之后)增加边界情况提示:当 `complaint_pending_total > 0` 但 `complaint_pending_list` 为空时,显示警告提示并提供刷新按钮 - -### 验证 -- 清除浏览器缓存后重新加载运营看板 -- 选择今日视图,确认"未闭环 N 条"标签与下方列表数据一致 -- 若不一致,应看到新增的警告提示(而非空白表格) -- 检查后端日志,确认无 `complaint_pending 数据不一致` 警告 - ---- - -## 变更文件汇总 - -| 文件 | 问题 | 变更类型 | -|------|------|----------| -| `web/src/views/system/Persons.vue` | 问题1 | 修改第470-472行、第533-534行,硬编码→动态选项 | -| `web/src/views/dashboards/Today.vue` | 问题2 | 重构第227-252行模板,新增3个computed替代原pie | -| `web/src/views/dashboards/Today.vue` | 问题3 | 拆分第265-332行处理人表,新增complaintData字段 | -| `web/src/views/dashboards/Today.vue` | 问题4 | 第2129-2130行防御性校验,新增边界提示模板 | -| `backend/app/services/analytics.py` | 问题3 | 新增按source_type拆分的handler聚合+返回字段 | -| `backend/app/services/analytics.py` | 问题4 | 数据一致性校验日志+注释 | - -## 实施顺序 - -1. **问题1** — 最简单,仅改前端两处模板,无后端依赖 -2. **问题4** — 防御性修改,后端+前端增加校验,不影响现有功能 -3. **问题3** — 后端拆分handler聚合 + 前端新增表格,前后端协调 -4. **问题2** — 前端重构饼图区域,UI布局调整较大,在问题3基础上叠加 \ No newline at end of file diff --git a/.trae/documents/metric-alerting-notification-design.md b/.trae/documents/metric-alerting-notification-design.md deleted file mode 100644 index d5ee686..0000000 --- a/.trae/documents/metric-alerting-notification-design.md +++ /dev/null @@ -1,379 +0,0 @@ -# 运营指标阈值化与预警通知机制 设计方案 - -> 版本: v0.3 · 2026-08-21 -> 状态: M1~M3 已实施(最小闭环), M4 通道架构已就绪(站内信已接, 群机器人/邮件可配置接入), M5 扩展待定 -> 范围: 指标注册表 + 多级阈值(优秀/达标/预警/告警) + 预警引擎 + 通知通道抽象 - -## 0. 实施状态(2026-08-21) - -| 里程碑 | 状态 | 说明 | -|---|---|---| -| M1 指标与阈值基础 | ✅ 已实施 | 5 个指标注册(recovery_rate/refund_count/ticket_backlog/external_complaint_7d/internal_complaint_daily) + 四级阈值(metric_thresholds 留痕) | -| M2 预警引擎 | ✅ 已实施 | scan_alerts 注册表驱动: 四级分级/区间幂等/升级/恢复自愈/数据健康检查; 定时每小时自动扫描 | -| M3 站内通知 | ✅ 已实施 | 告警触发/恢复 → 站内信(notify) + 投递记录(alert_deliveries) | -| M4 推送通道 | 🟡 架构已就绪 | notification_channels 表 + Channel 抽象(站内信已接; webhook 群机器人/邮件通道类已实现, 配置即用); 前端通道管理+测试按钮 | -| M5 扩展联动 | ⏳ 待定 | 指标扩充/聚合/与 Incident/Measure 联动 | - -**实现与设计差异(§8.1)**: 告警记录复用现有 `alerts` 表(扩展 metric_code/metric_value/threshold_value/period_start/period_end/data_ok/recovered_at 字段), 未新建 alert_events 表。 - -**新增后端文件**: `app/models/metric.py` / `app/models/notification_channel.py` / `app/services/metric_registry_service.py` / `app/services/notification_channel.py` / `app/api/v1/metrics.py` / `app/api/v1/channels.py`; 迁移 `n9a8b7c6d5e4f_add_metric_alerting.py`(含指标/阈值/站内信种子数据)。 - -**前端**: `web/src/views/management/Alerts.vue` 升级为四 Tab(指标水位/预警规则/预警记录/通知通道); 新增 `web/src/api/metric.js`、`web/src/api/channel.js`。 - ---- - -## 1. 背景与目标 - -当前系统的看板(运营看板/今日看板/投诉分析)已实时计算大量运营指标(退款挽回率、三源投诉、积压工单、首响时长等),但**指标的"好坏"完全靠人肉看数字判断**——没有"低于多少要盯、低于多少要报警"的系统化机制。 - -目标: - -1. **指标化监测**: 运营指标有明确的"正常/预警/告警"水位线,系统按周期自动评估 -2. **阈值参数化**: 优秀/达标/预警/告警四级阈值全部由系统参数配置(可改、可留痕),不硬编码 -3. **预警自动生成**: 指标越线自动生成预警记录,走完整生命周期(待确认→已确认→已恢复) -4. **通知多渠道**: 预警触发后站内信通知(已有),并预留企业微信/钉钉/飞书群机器人、邮件等推送通道(可扩充) - -核心原则:**预警监测的指标口径必须与看板同源同口径**,先有指标注册表,再有阈值与规则,避免"看板一个数、预警另一个数"。 - ---- - -## 2. 现状盘点 - -### 2.1 已有基础(可复用) - -| 模块 | 位置 | 现状 | -|---|---|---| -| 预警规则/记录 | `app/models/alert.py` AlertRule/Alert | 已有: name/type/condition(JSON)/threshold/enabled; 记录含 level/status(pending/confirmed/ignored) | -| 预警扫描 | `app/api/v1/alerts.py` scan_alerts | 4 种硬编码规则(退款激增/工单积压/事件激增/投诉激增), 单一 threshold, level 由 threshold×2 推断, **当日幂等** | -| 定时调度 | `app/services/bms_scheduler.py` | 已有每小时预警扫描钩子(当日幂等), 可复用 | -| 站内信 | `app/services/notifier.py` notify() | 统一入口, 支持按用户/角色, Notification 表, 前端有角标 | -| 前端预警中心 | `web/src/views/management/Alerts.vue` | 规则 CRUD + 预警记录 + 手动扫描 + 确认/忽略 | -| 指标计算 | `app/services/analytics.py` | 各看板指标已实时计算(如 refund recovery), 口径已沉淀 | - -### 2.2 缺口 - -1. **阈值单一数值** → 无法表达"达标(绿)/预警(黄)/告警(红)"三级水位 -2. **规则类型硬编码** → 加一个新指标预警必须改 scan_alerts 代码, 不可配置 -3. **只支持"≥阈值"的激增类判断** → 无法表达"挽回率 < 60%"这类"越低越差"指标 -4. **指标无注册表** → 口径散落, 规则无法引用统一指标定义 -5. **预警只入库不通知** → 不调 notify(), 无任何推送通道 -6. **无告警生命周期闭环** → 无"恢复/解除"概念, 无效告警会堆积 -7. **无数据健康保护** → SCRM 同步失败导致指标=0 会误报"恶化告警" - ---- - -## 3. 总体架构 - -``` -┌──────────────────────────────────────────────────────┐ -│ 指标注册表 (metric_registry) │ -│ 每个指标: code / 口径函数 / 周期 / 方向 / 依赖数据源 │ -└───────────────┬──────────────────────────────────────┘ - │ 引用 -┌───────────────▼──────────────────────────────────────┐ -│ 阈值参数 (metric_thresholds) │ -│ metric_code + 优秀线/达标线/预警线/告警线 + 生效区间 │ -└───────────────┬──────────────────────────────────────┘ - │ 计算 -┌───────────────▼──────────────────────────────────────┐ -│ 预警引擎 (alert_engine) │ -│ 周期扫描 → 数据健康检查 → 指标求值 → 与阈值比较 │ -│ → 幂等/升级/静默 → 生成告警记录 │ -└───────────────┬──────────────────────────────────────┘ - │ 触发 -┌───────────────▼──────────────────────────────────────┐ -│ 通知分发 (notification_channel) │ -│ 站内信(先) → 企业微信/钉钉/飞书群机器人 → 邮件(后) │ -└──────────────────────────────────────────────────────┘ -``` - -分层职责: - -- **指标注册表**: 定义"算什么"(口径), 是唯一事实源, 看板与预警共用 -- **阈值参数**: 定义"多好算好"(水位线), 可配置可留痕 -- **预警引擎**: 定义"何时喊"(触发), 与指标/阈值解耦, 新增指标无需改引擎 -- **通知通道**: 定义"喊给谁、怎么喊", 通道可插拔 - ---- - -## 4. 指标注册表(核心) - -### 4.1 设计要点 - -- 每个指标注册一条元数据:**计算函数** + **周期** + **方向** + **依赖数据源** -- 指标值由统一求值器调用注册的计算函数得到, **与看板共用同一函数**(同源同口径) -- 注册方式: 代码注册(函数式) + 数据库元数据(阈值引用), 避免重复实现 - -### 4.2 首批指标清单(试点范围) - -| code | 指标名 | 方向 | 周期 | 依赖数据源 | 口径来源 | -|---|---|---|---|---|---| -| recovery_rate | 退款挽回率 | 越高越好 | 日 | SCRM cs_record | analytics 退款挽回计算 | -| refund_count | 当日退款笔数 | 越低越好 | 日 | refund | 看板 refund kpi | -| ticket_backlog | 待处理投诉工单积压 | 越低越好 | 日 | service_tickets | 投诉 backlog.total | -| external_complaint_7d | 近7天外部投诉量 | 越低越好 | 日 | external_complaints | 投诉 supplementary | -| internal_complaint_daily | 当日内部投诉量 | 越低越好 | 日 | service_tickets | 投诉 internal.total | -| first_response_sec | 首次响应时长(均值) | 越低越好 | 日 | scrm_session | SCRM 绩效 | -| transfer_rate | 会话转接率 | 双向 | 日 | scrm_session_transfer | SCRM 绩效 | -| sync_health | 数据同步成功率 | 越高越好 | 日 | scrm_sync_log/bms_sync_log | 同步日志 | - -> 指标清单按需扩充: 值班在线率、工作效率值、四路退款占比、套餐充值量等。 -> 重要:**先有指标, 后有阈值**; 未注册的指标不参与预警。 - ---- - -## 5. 三级阈值与规则模型 - -### 5.1 阈值语义(四级: 优秀/达标/预警/告警) - -| 级别 | 含义 | 运营动作 | 颜色 | -|---|---|---|---| -| 优秀(excellent) | 指标超出预期, 明显优于正常水位 | 通报表扬, 提炼可复制的经验 | 蓝 | -| 达标(green) | 指标处于正常区间 | 无需处理 | 绿 | -| 预警(yellow) | 指标偏离正常,需关注 | 盯一盯,排查原因 | 黄 | -| 告警(red) | 指标严重偏离,需干预 | 立即处理,升级 | 红 | - -- **越高越好型**(如挽回率): 阈值 = 下限线。`value ≥ 优秀线` → 优秀; `value < 预警线` → 预警; `value < 告警线` → 告警; 其余 → 达标 -- **越低越好型**(如退款笔数/积压): 阈值 = 上限线。`value ≤ 优秀线` → 优秀; `value > 预警线` → 预警; `value > 告警线` → 告警; 其余 → 达标 -- **优秀线可选**: 不配置优秀线则退化为"达标/预警/告警"三级, 不产生"优秀"状态 -- **仅 预警/告警 触发告警记录与通知**; 优秀/达标仅展示水位状态(优秀可另行配置"通报表扬"通知, 默认不推) -- 方向由指标注册表声明, 阈值表只存数值, 引擎按方向解释 - -### 5.2 规则 = 指标 × 阈值 × 调度 - -规则配置项: - -``` -metric_code 引用指标注册表 -warning_threshold 预警线 -critical_threshold 告警线 -period 日/周/月 -enabled 是否启用 -channels 通知通道列表(默认站内信) -recipient_roles 通知角色(admin/supervisor)或指定用户 -silence_window 静默时段(如 00:00-08:00 不推送,可选) -``` - -> 兼容现有 AlertRule: 规则仍存 alert_rules 表, 字段扩展(见 §8), 现有 4 类硬编码规则迁移为注册表指标+阈值。 - ---- - -## 6. 预警引擎 - -### 6.1 扫描流程 - -``` -1. 数据健康检查(防假告警) - - 指标依赖的数据源今日同步是否成功(sync_health) - - 数据量是否合理(如 SCRM 会话量低于前3日均值 20% → 视为数据异常) - - 数据异常 → 跳过该指标, 生成"数据异常"提示(不产生告警) -2. 指标求值(调用注册表计算函数, 与看板同口径) -3. 与多级阈值比较 → 确定级别(优秀/达标/预警/告警; 无越线时若之前有告警则生成"已恢复"记录) -4. 幂等去重: 同规则同周期同级别最多一条 pending -5. 升级: 连续 N 个周期处于预警且未处理 → 升级为告警 -6. 生成 Alert 记录 + 通知分发(§7) -``` - -### 6.2 关键规则 - -| 规则 | 说明 | -|---|---| -| 当日幂等 | 同规则同周期同级别不重复生成(复用现有机制) | -| 连续恶化升级 | 预警持续 N 天未处理且指标继续恶化 → 自动升级告警 | -| 恢复自愈 | 指标回到达标线 → 自动生成/更新"已恢复"状态并通知 | -| 静默窗口 | 配置的时段内不推送(仅入库), 避免夜间打扰 | -| 数据保护 | 数据不完整/异常时不告警, 单独标记(防假告警, 见 6.3) | -| 批量聚合 | 同批多条同类预警合并为一条摘要(可选, 后期) | - -### 6.3 数据健康保护(重要) - -- 预警求值前先跑 `sync_health` 检查: 今日 SCRM/BMS 是否同步成功 -- 指标值出现"断崖式"变化(如为 0 或骤降 80%)时, 先判定为数据异常而非业务恶化 -- 数据异常与业务告警分开展示, 避免"假告警"干扰决策 - ---- - -## 7. 通知通道抽象 - -### 7.1 通道接口 - -```python -class NotificationChannel(Protocol): - def send(self, ctx: AlertContext, recipient: list) -> ChannelResult: ... -``` - -- `站内信(InAppChannel)` — 复用现有 `notifier.notify()`, **首发实现** -- `企业微信群机器人 / 钉钉 / 飞书群机器人(WebhookChannel)` — 二期 -- `邮件(EmailChannel)` — 三期 - -### 7.2 配置模型 - -- 通道注册表: `notification_channels` 表, 每个通道有 type + config(webhook URL/密钥, 加密存储) -- 规则级配置: 每条规则选择通知通道 + 接收对象(角色/指定用户) -- 测试能力: 每个通道提供"发送测试"按钮(前端), 验证配置可用 -- 发送失败降级: 群机器人失败 → 自动降级为站内信, 并记发送日志 - -### 7.3 通知去重 - -- 同一告警不重复推送: 以 Alert.id 为幂等键, 推送状态记录在 alert_deliveries 表 -- 恢复通知与触发通知各推送一次 - ---- - -## 8. 数据模型(表结构草案) - -### 8.1 新建 - -```sql --- 指标注册表(元数据; 计算函数在代码层注册) -CREATE TABLE metric_registry ( - id BIGSERIAL PRIMARY KEY, - code VARCHAR(64) UNIQUE NOT NULL, -- 指标code, 与代码注册对应 - name_cn VARCHAR(128) NOT NULL, - direction VARCHAR(8) NOT NULL, -- higher_better / lower_better - period VARCHAR(8) NOT NULL DEFAULT 'day', -- day/week/month - data_sources JSONB DEFAULT '[]', -- 依赖数据源, 供健康检查 - description TEXT, - created_at TIMESTAMPTZ DEFAULT now() -); - --- 阈值参数(四级水位: 优秀/达标/预警/告警, 可留痕) -CREATE TABLE metric_thresholds ( - id BIGSERIAL PRIMARY KEY, - metric_code VARCHAR(64) REFERENCES metric_registry(code), - excellent_value DOUBLE PRECISION, -- 优秀线(可选, 不配则无优秀状态) - warning_value DOUBLE PRECISION NOT NULL, -- 预警线 - critical_value DOUBLE PRECISION NOT NULL, -- 告警线 - unit VARCHAR(16), -- % / 笔 / 秒 ... - valid_from TIMESTAMPTZ, -- 生效区间(支持历史调参留痕) - valid_to TIMESTAMPTZ, - updated_by INTEGER REFERENCES users(id), - updated_at TIMESTAMPTZ DEFAULT now() -); - --- 告警记录(扩展现有 alert_alerts 或新建 alert_events) -CREATE TABLE alert_events ( - id BIGSERIAL PRIMARY KEY, - metric_code VARCHAR(64), - rule_id BIGINT, -- 关联 alert_rules - level VARCHAR(16), -- warning / critical / recovered - metric_value DOUBLE PRECISION, - threshold_value DOUBLE PRECISION, - period_start TIMESTAMPTZ, - period_end TIMESTAMPTZ, - status VARCHAR(16) DEFAULT 'pending', -- pending/confirmed/ignored/recovered - data_ok BOOLEAN DEFAULT TRUE, -- 数据健康标记(防假告警) - detail TEXT, - created_at TIMESTAMPTZ DEFAULT now() -); - --- 通知通道配置 -CREATE TABLE notification_channels ( - id BIGSERIAL PRIMARY KEY, - code VARCHAR(32) UNIQUE NOT NULL, -- inapp / wecom_robot / dingtalk_robot / feishu_robot / email - name_cn VARCHAR(64), - channel_type VARCHAR(16), - config JSONB, -- webhook等, 敏感字段加密 - enabled BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT now() -); - --- 通知投递记录(幂等) -CREATE TABLE alert_deliveries ( - id BIGSERIAL PRIMARY KEY, - alert_id BIGINT, - channel VARCHAR(32), - status VARCHAR(16), -- success / failed / degraded - detail TEXT, - created_at TIMESTAMPTZ DEFAULT now() -); - --- 指标历史值(回溯"为什么触发/持续多久") -CREATE TABLE metric_daily_snapshots ( - id BIGSERIAL PRIMARY KEY, - metric_code VARCHAR(64), - value DOUBLE PRECISION, - period_date DATE, - level VARCHAR(16), -- excellent/green/warning/critical - UNIQUE (metric_code, period_date) -); -``` - -### 8.2 复用与兼容 - -- `alert_rules` 扩展字段: metric_code / warning_value / critical_value / channels / recipient_roles / silence_window(JSONB), 现有 4 类规则迁移 -- `Alert` 记录增加 metric_code/level 对齐(或迁移到 alert_events) -- 前端 Alerts.vue 扩展: 规则管理升级为"指标+阈值"配置, 预警记录增加级别/恢复状态 - ---- - -## 9. API 与前端规划 - -### 9.1 API(后端) - -``` -GET /api/v1/metrics 指标注册表列表 -GET /api/v1/metrics/{code}/current 指标当前值+当前级别(前端展示水位) -PUT /api/v1/metrics/{code}/thresholds 配置/修改三级阈值(留痕) -POST /api/v1/alerts/scan 手动触发扫描(已有, 扩展) -POST /api/v1/alerts/{id}/recover 手动标记恢复 -GET /api/v1/alerts/events 告警事件列表(含数据异常) -GET /api/v1/channels 通道列表 -POST /api/v1/channels/{code}/test 发送测试通知 -``` - -### 9.2 前端(Alerts.vue 升级) - -1. **指标水位视图**: 指标列表 + 当前值 + 优秀/达标/预警/告警状态徽标 + 趋势 -2. **阈值配置**: 多级阈值可视化编辑, 修改留痕 -3. **告警事件流**: 级别/状态/恢复时间/数据异常标记 -4. **通道管理**: 启停 + 测试发送 -5. **通知中心**: 已有站内信角标复用; 预警类通知可点击跳转预警详情 - ---- - -## 10. 与现有模块联动 - -| 联动 | 说明 | -|---|---| -| 影响事件(Incident) | 高严重度告警(告警级)可一键/自动生成影响事件, 今日看板横幅已支持 | -| 管理措施(Measure) | 告警可转"管理措施"或"待办", 形成"发现→措施→回看"闭环 | -| 数据同步(sync) | 预警依赖数据同步; 同步失败本身也是监测指标(sync_health) | -| 工作台 | 待处理告警进入工作台待办, 管理端可见 | - ---- - -## 11. 实施里程碑 - -| 阶段 | 内容 | 交付物 | -|---|---|---| -| M1 指标与阈值基础 | metric_registry + metric_thresholds 表; 指标求值器(复用 analytics 函数); 首批 3~5 个指标注册(含退款挽回率) | 指标列表 API + 当前值/级别 | -| M2 预警引擎 | 扩展 scan_alerts 为注册表驱动; 三级阈值比较; 当日幂等 + 恢复自愈; 数据健康检查; 定时扫描接入 | 告警事件 + 恢复通知 | -| M3 站内通知 | 告警生成/恢复 → notify() 站内信; 前端 Alerts.vue 升级(水位视图+阈值配置+事件流) | 站内信闭环 | -| M4 推送通道 | notification_channels 表 + 通道抽象 + 企业微信/钉钉/飞书群机器人 + 邮件; 测试发送; 失败降级 | 多通道推送 | -| M5 扩展 | 指标清单扩充; 连续升级/聚合; 与 Incident/Measure 联动 | 运营指标全覆盖 | - -> M1~M3 是"最小闭环"(用户先只做站内信, 推送通道架构先抽象、后续接入)。 - ---- - -## 12. 风险与权衡 - -| 风险 | 应对 | -|---|---| -| 指标口径与看板不一致 | 指标注册表强制复用看板计算函数, 不另写 | -| 假告警(数据未同步) | 数据健康检查前置, 数据异常与业务告警分离 | -| 告警风暴/打扰 | 当日幂等 + 静默窗口 + 恢复自愈 + (后期)聚合 | -| 硬编码规则迁移成本 | 现有 4 类规则映射到注册表指标, 阈值表迁移, 保留 condition 字段兼容 | -| 阈值配置失误 | 阈值修改留痕(valid_from/valid_to + updated_by), 可回看可回滚 | -| 推送通道失败 | 发送失败降级站内信, 投递记录可查 | -| 性能 | 指标求值缓存(复用 Redis), 日终批量计算 + 快照表 | - ---- - -## 待评审问题 - -1. 首批指标清单(§4.2)是否合理? 是否有遗漏的高优先级指标? -2. ~~三级阈值是否够用~~ → **已确认改为四级(优秀/达标/预警/告警)**; 是否再引入"红线"级(合规/安全等硬性底线)待定, 运营指标暂不引入 -3. 告警是否需要在运营看板/今日看板顶部展示(类似影响事件横幅)? -4. M1~M3 最小闭环是否按此顺序推进? diff --git a/.trae/documents/refund-dashboard-redesign.md b/.trae/documents/refund-dashboard-redesign.md deleted file mode 100644 index f117aa0..0000000 --- a/.trae/documents/refund-dashboard-redesign.md +++ /dev/null @@ -1,1096 +0,0 @@ -# 退款看板重设计 Spec - -> 版本:v1.0 · 2026-08-12 -> 依据:运营看板已验证的 6 专项分类 / T+7 判定 / staff_type 三分类 / reason 归一化 7 大类口径 -> 目标:把退款看板从"汇总监控"升级为"人员×设备×类型 深度分析 + 下钻审计" - ---- - -## 一、背景与目标 - -### 1.1 现状问题 - -当前退款看板(RefundDashboard.vue)只有两个 Tab: -- **今日视图**:11 张 KPI 卡 + 小时/日趋势 + 失败原因 7 类 + 专员接待排名 -- **综合看板**:催单风险 5 视图 + 挽回专项 6 视图 + 现金流 7 视图 - -**核心缺失**: -1. 看到汇总数后无法下钻到具体设备/人员 -2. 没有人员成功率趋势(只有全员趋势) -3. 没有类型维度的趋势对比 -4. 设备维度完全缺失(反向视角) -5. AuditView 审计组件是孤岛,未与退款分析打通 -6. staff_detail 已有 rich 字段(department/position/staff_type/refund_op_amount)但前端只展示了 6 列 - -### 1.2 设计目标 - -把退款看板打造成"退款深度分析中枢": -- **人员维度**:查看每个人员周期内接待的所有设备 + 挽回成功率 + 具体挽回/流失的设备清单 -- **设备维度**:反向视角,识别高风险设备(复退款/跨人员/高金额) -- **类型维度**:退款原因 7 大类 + problem_type 细分的接待量/成功率/趋势 -- **下钻审计**:从汇总数 → 设备清单 → AuditView 时序,形成完整审计链路 - -### 1.3 设计原则 - -1. **复用已验证口径**:所有分母/分子/失败判定/T+7 窗口/staff_type 分类的逻辑,与 `refund_recovery_analysis` 保持完全一致,不重新发明 -2. **汇总→下钻模式**:退款看板做"批量对比+定位",AuditView 做"单点深挖",两者通过跳转打通 -3. **数据已有优先**:优先使用 refund_wide / cs_records / refunds / service_tickets 现有数据;SCRM 质量数据本期不接入 -4. **不过度设计**:每个图表都要能回答一个具体业务问题 - ---- - -## 二、整体架构 - -### 2.1 Tab 结构(5 Tab) - -``` -退款看板 (RefundDashboard.vue) -├── Tab1: 概览 ← 原今日视图强化,小幅优化 -├── Tab2: 人员分析 ← 新增(P0 核心) -├── Tab3: 设备分析 ← 新增(P0 核心) -├── Tab4: 类型×趋势 ← 新增(P0 核心) -└── Tab5: 综合看板 ← 原 overview,保留微调 -``` - -### 2.2 数据流 - -``` -后端 前端 -───────────────────────────────────────────────────── -refund_recovery_analysis (已有,复用) ──→ Tab1 概览 -refund_personnel_analysis (新增) ──→ Tab2 人员分析 -refund_device_analysis (新增) ──→ Tab3 设备分析 -refund_type_trend (新增) ──→ Tab4 类型×趋势 -executeDashboard (已有,复用) ──→ Tab5 综合看板 -audit_timeline (已有,AuditView) ←── Tab2/Tab3 下钻跳转 -``` - -### 2.3 路由与权限 - -- 路由不变:`/dashboards/refund`(code=preset_dashboard_refund) -- 权限不变:仅 isManager=true 可见(决策看板下) -- Tab 切换更新 URL query:`?tab=overview|today|personnel|device|type` - ---- - -## 三、Tab1:概览(今日视图强化) - -### 3.1 保留内容(不变) - -- 11 张 KPI 卡(total_sn/success_sn/failed_sn/success_rate/abandon/fail/success/recharge/target_total/actual_refund/fee_ratio) -- 今日按小时挽回趋势(双 Y 轴:柱=SN 数,线=成功率) -- 近 7 天按日挽回趋势(双 Y 轴) -- 失败原因 7 大类分布(柱=单量,线=占比) -- 退款专员接待排名表(现有 8 列) - -### 3.2 新增内容 - -#### 3.2.1 Top5 高风险设备快照(当日失败金额倒序) - -**位置**:KPI 卡下方,左侧半屏(12 col) - -**数据来源**:新增后端字段 `top_risk_devices`(在 refund_recovery_analysis 返回中追加) - -**展示**: -``` -┌─ 今日高风险设备 Top5 (失败金额倒序) ──────────┐ -│ SN | 失败原因 | 退款金额 | 接待人 │ -│ ABC123 | 不想用了 | ¥1,280 | 叶珍凤 │ -│ DEF456 | 设备故障 | ¥980 | 朱海洋 │ -│ ... │ -│ [查看全部] → 跳转 Tab3 设备分析 │ -└──────────────────────────────────────────────────┘ -``` - -**交互**:点击 SN 行 → 跳转 Tab3 并自动选中该 SN → 展开设备详情抽屉 - -#### 3.2.2 金额分桶饼图 - -**位置**:KPI 卡下方,右侧半屏(12 col) - -**数据来源**:新增后端字段 `amount_buckets` - -**展示**:失败 SN 按退款金额分桶 -- 小额(<100 元) -- 中额(100-500 元) -- 大额(500-2000 元) -- 超大额(>2000 元) - -每桶显示:SN 数 + 占比% + 金额合计 - -### 3.3 后端改动 - -在 `refund_recovery_analysis` 返回中追加 2 个字段: - -```python -# analytics.py refund_recovery_analysis 返回追加: -{ - ...原有字段..., - "top_risk_devices": [ - { - "device_sn": "ABC123", - "reason": "不想用了", # 归一化原因 - "amount": 1280.00, - "operator": "企微-叶珍凤", # 接待人(首次接待) - "operator_name": "叶珍凤", - "refund_time": "2026-08-12T14:30:00", - "status": "已退款" - }, - ... # Top5 - ], - "amount_buckets": [ - {"bucket": "小额(<100)", "sn_cnt": 12, "amount": 960.00, "ratio": 40.0}, - {"bucket": "中额(100-500)", "sn_cnt": 10, "amount": 2800.00, "ratio": 33.3}, - {"bucket": "大额(500-2000)", "sn_cnt": 6, "amount": 5400.00, "ratio": 20.0}, - {"bucket": "超大额(>2000)", "sn_cnt": 2, "amount": 5600.00, "ratio": 6.7}, - ] -} -``` - ---- - -## 四、Tab2:人员分析(P0 核心) - -### 4.1 布局 - -``` -┌─ 筛选栏 ────────────────────────────────────────────────────┐ -│ [部门:全部▼] [分组:全部▼] [专项:全部▼] [人员类型:全部▼] │ -│ [时间范围:近30天▼] [自定义日期] [刷新] │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ① 人员对比表(强化版) ────────────────────────────────────┐ -│ 见 4.2 字段清单 │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ② 人员成功率趋势(多线) ──────┐ ┌─ ③ 人员接待量趋势(堆叠)─┐ -│ 12 col │ │ 12 col │ -└─────────────────────────────────┘ └─────────────────────────────┘ - -┌─ ④ 人员×原因 热力图矩阵 ────────────────────────────────────┐ -│ 24 col │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ⑤ 人员详情抽屉(点击某行触发) ────────────────────────────┐ -│ 见 4.3 │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 4.2 ① 人员对比表(强化版 staff_detail) - -**数据来源**:新增后端接口 `GET /api/v1/analytics/refund/personnel-analysis` - -**字段清单**(在现有 staff_detail 基础上扩展): - -| 列名 | 字段 | 类型 | 说明 | -|---|---|---|---| -| 姓名 | name | string | Person.display_name | -| 部门 | department | string | 企微/店铺/研发 | -| 分组 | group | string | 5 组之一 | -| 人员类型 | staff_type | tag | 专职挽回(绿)/退款操作(蓝)/其他人员挽回(灰) | -| 接待SN | total_sn | int | CS 接待去重 SN 数 | -| 成功 | success_sn | int | 挽留成功 SN 数(绿色) | -| 失败 | failed_sn | int | 挽留失败 SN 数(红色) | -| 成功率 | success_rate | tag% | ≥38%绿/30-38%黄/<30%红 | -| 退款操作SN | refund_op_cnt | int | 作为 refunds.operator 处理的 SN 数 | -| 退款操作金额 | refund_op_amount | money | 退款操作金额合计 | -| 挽回估算金额 | estimated_save_amount | money | success_sn × 同类失败平均金额(新增) | -| 首次接待 | first_reception | datetime | 周期内首次接待时间(新增) | -| 末次接待 | last_reception | datetime | 周期内末次接待时间(新增) | -| 操作 | - | button | [详情] [时序] | - -**交互**: -- 表头支持排序(接待SN/成功率/退款操作金额/挽回估算金额) -- 点击 [详情] → 展开右侧抽屉(见 4.3) -- 点击 [时序] → 新窗口打开 AuditView,person_id 已带入 -- 行可多选(checkbox),选中后联动 ②③ 趋势图 - -### 4.3 ⑤ 人员详情抽屉(核心下钻) - -**触发**:点击人员对比表的 [详情] 按钮 - -**布局**: - -``` -┌─ 抽屉(右侧滑出,宽度 60vw) ────────────────────────────────┐ -│ │ -│ ┌─ 人员概要 ──────────────────────────────────────────────┐│ -│ │ 叶珍凤 | 企微 | 朱海洋组 | 专职挽回 ││ -│ │ 接待SN: 45个 | 成功: 28个 | 失败: 17个 | 成功率: 62.2% ││ -│ │ 退款操作: 12个 SN | 退款金额: ¥8,400 | 挽回估算: ¥6,720 ││ -│ │ [查看完整时序] → 跳转 AuditView ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 接待设备清单 ──────────────────────────────────────────┐│ -│ │ [成功 Tab(28)] [失败 Tab(17)] [全部 Tab(45)] ││ -│ │ ││ -│ │ 筛选: [失败原因▼] [搜索SN] ││ -│ │ ││ -│ │ ┌─ 表格 ─────────────────────────────────────────────┐ ││ -│ │ │ SN | 首次接待 | 末次接待 | 接待次数 | 失败原因 │ ││ -│ │ │ | 退款金额 | 退款时间 | 最终状态 | [时序] │ ││ -│ │ └────────────────────────────────────────────────────┘ ││ -│ │ ││ -│ │ 每行点击 [时序] → AuditView(person+device_sn) ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 该人员的成功率趋势(单线,日粒度) ────────────────────┐│ -│ │ 双 Y 轴:柱=接待SN,线=成功率 ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 该人员×原因 分布(饼图) ──────────────────────────────┐│ -│ │ 该人员接待的 SN 按失败原因 7 大类分布 ││ -│ └──────────────────────────────────────────────────────────┘│ -└──────────────────────────────────────────────────────────────┘ -``` - -**设备清单字段**: - -| 列名 | 字段 | 说明 | -|---|---|---| -| 设备SN | device_sn | | -| 首次接待 | first_reception_time | 该人员首次接待此 SN 的时间 | -| 末次接待 | last_reception_time | 该人员末次接待此 SN 的时间 | -| 接待次数 | reception_count | 该人员接待此 SN 的 cs_record 条数 | -| 结果 | result | 成功(绿)/失败(红) | -| 失败原因 | fail_reason | 归一化 7 大类(成功则为空) | -| 退款金额 | refund_amount | 失败时显示,成功为 0 | -| 退款时间 | refund_time | 失败时显示 | -| 最终状态 | final_status | 已退款/申请中/无需退款等 | -| 操作 | - | [时序] 跳转 AuditView | - -### 4.4 ② 人员成功率趋势(多线对比) - -**图表**:ECharts 折线图 - -**数据来源**:新增后端接口 `GET /api/v1/analytics/refund/personnel-trend` - -**交互**: -- 默认展示成功率 Top3 + Bottom3 人员(共 6 条线) -- 可通过表格行多选自定义对比人员 -- X 轴:日期(按日/周粒度切换) -- Y 轴:成功率% -- 悬浮显示:姓名 + 日期 + 成功率 + 接待SN数 - -### 4.5 ③ 人员接待量趋势(堆叠柱) - -**图表**:ECharts 堆叠柱状图 - -**数据来源**:同 ② 接口 - -**展示**: -- X 轴:日期 -- Y 轴:SN 数 -- 堆叠:每个选中人员一层(颜色区分) -- 与 ② 联动:选中人员一致 - -### 4.6 ④ 人员×原因 热力图矩阵 - -**图表**:ECharts heatmap - -**数据来源**:新增后端接口 `GET /api/v1/analytics/refund/person-reason-matrix` - -**展示**: -- 行:人员(按接待量倒序,默认 Top15) -- 列:7 大原因(不想用了/充值错误/设备故障/网卡卡无网络/套餐问题/卡商问题/其他) -- 值:该人员在该原因下的**成功率**(颜色:绿=高/红=低) -- 单元格悬浮:姓名 + 原因 + 接待N个 + 成功M个 + 成功率X% - -**交互**: -- 点击单元格 → 展开抽屉,自动筛选该人员+该原因的设备清单 -- 可切换"成功率/接待量/失败数"三种色阶 - -### 4.7 后端接口 - -#### 接口1:人员分析汇总 - -``` -GET /api/v1/analytics/refund/personnel-analysis -参数: - start?: string (YYYY-MM-DD) - end?: string (YYYY-MM-DD) - department?: string (企微/店铺/研发) - group?: string (分组名) - specialty?: string (专项名) - staff_type?: string (专职挽回/退款操作/其他人员挽回) - -返回: -{ - "staff_detail": [ - { - "operator": "企微-叶珍凤", - "name": "叶珍凤", - "department": "企微", - "group": "朱海洋组", - "position": "组员", - "staff_type": "专职挽回", - "total_sn": 45, - "success_sn": 28, - "failed_sn": 17, - "success_rate": 62.22, - "refund_op_cnt": 12, - "refund_op_amount": 8400.00, - "estimated_save_amount": 6720.00, // success_sn × 同类失败平均金额 - "first_reception": "2026-08-01T09:00:00", - "last_reception": "2026-08-12T15:30:00", - }, - ... - ], - "summary": { - "total_persons": 15, - "total_sn": 320, - "avg_success_rate": 45.6, - "total_refund_op_amount": 128000.00, - } -} -``` - -**实现要点**: -- 复用 `refund_recovery_analysis` 的人员分母/失败 SN 逻辑 -- 新增 `estimated_save_amount` 计算:对每个成功 SN,用同原因类别的失败 SN 平均金额估算 -- 新增 `first_reception / last_reception` 从 cs_records 聚合 -- 支持 department/group/specialty/staff_type 筛选 - -#### 接口2:人员接待设备清单 - -``` -GET /api/v1/analytics/refund/person-devices -参数: - operator: string (必填,人员别名如 "企微-叶珍凤") - start?: string - end?: string - result?: string (success/failed/all,默认 all) - -返回: -{ - "operator": "企微-叶珍凤", - "name": "叶珍凤", - "devices": [ - { - "device_sn": "ABC123", - "first_reception_time": "2026-08-05T10:00:00", - "last_reception_time": "2026-08-08T14:00:00", - "reception_count": 3, - "result": "failed", // success/failed - "fail_reason": "不想用了", // 归一化原因,成功则为 null - "refund_amount": 280.00, // 失败时显示 - "refund_time": "2026-08-09T12:00:00", - "final_status": "已退款", - "refund_no": "RF20260809123", // 退款单号(失败时) - }, - ... - ], - "summary": { - "total": 45, - "success": 28, - "failed": 17, - "total_refund_amount": 8400.00, - } -} -``` - -**实现要点**: -- 从 cs_records 查该 operator 接待的 problem_category='退款' 的 SN 集合 -- 与 global_fail_sns 交集判断 success/failed -- 失败 SN 关联 refunds 表取金额/时间/单号 -- 按 first_reception_time 倒序 - -#### 接口3:人员趋势 - -``` -GET /api/v1/analytics/refund/personnel-trend -参数: - start?: string - end?: string - operators?: string (逗号分隔,如 "企微-叶珍凤,企微-朱海洋") - grain?: string (day/week,默认 day) - -返回: -{ - "dates": ["2026-08-01", "2026-08-02", ...], - "persons": [ - { - "operator": "企微-叶珍凤", - "name": "叶珍凤", - "trend": [ - {"date": "2026-08-01", "total_sn": 5, "success_sn": 3, "failed_sn": 2, "success_rate": 60.0}, - ... - ] - }, - ... - ] -} -``` - -**实现要点**: -- 复用 refund_recovery_analysis 的 daily_trend 逻辑,但按 operator 拆分 -- 默认返回 Top10 人员的趋势(按接待量),支持 operators 参数自定义 - -#### 接口4:人员×原因矩阵 - -``` -GET /api/v1/analytics/refund/person-reason-matrix -参数: - start?: string - end?: string - top?: int (默认 15,按接待量倒序取 Top N 人员) - -返回: -{ - "reasons": ["不想用了", "充值错误", "设备故障", "网卡卡无网络", "套餐问题", "卡商问题", "其他"], - "persons": [ - { - "operator": "企微-叶珍凤", - "name": "叶珍凤", - "cells": [ - {"reason": "不想用了", "total_sn": 12, "success_sn": 8, "failed_sn": 4, "success_rate": 66.7}, - {"reason": "充值错误", "total_sn": 5, "success_sn": 2, "failed_sn": 3, "success_rate": 40.0}, - ... - ] - }, - ... - ] -} -``` - -**实现要点**: -- 对每个人员的接待 SN,按失败原因分组(成功 SN 的原因从 cs_record.problem_type 归一化) -- 成功 SN 也需要归一化原因(从 cs_record 取 problem_type → 归一化) - ---- - -## 五、Tab3:设备分析(P0 核心,反向视角) - -### 5.1 布局 - -``` -┌─ 筛选栏 ────────────────────────────────────────────────────┐ -│ [失败原因:全部▼] [金额区间:全部▼] [接待次数:全部▼] │ -│ [时间范围:近30天▼] [自定义日期] [刷新] │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ① 设备风险分布 KPI ────────────────────────────────────────┐ -│ 4 张卡片:复退款设备数 | 跨人员接待设备数 | 高金额设备数 │ -│ | 总失败设备数 │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ② Top20 高风险设备表(失败金额倒序) ──────────────────────┐ -│ 见 5.2 │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ③ 接待→流失时差分布 ─────┐ ┌─ ④ 设备接待次数分布 ────────┐ -│ 12 col │ │ 12 col │ -└─────────────────────────────┘ └────────────────────────────────┘ - -┌─ ⑤ 设备详情抽屉(点击某行触发) ────────────────────────────┐ -│ 见 5.3 │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 5.2 ② Top20 高风险设备表 - -**数据来源**:新增后端接口 `GET /api/v1/analytics/refund/device-analysis` - -**字段清单**: - -| 列名 | 字段 | 说明 | -|---|---|---| -| 设备SN | device_sn | | -| 接待次数 | reception_count | 被 cs_record 接待的总次数 | -| 接待人员数 | receptionist_count | 被几个不同人员接待过(跨人员) | -| 接待人员 | receptionists | 逗号分隔的姓名列表 | -| 失败原因 | fail_reason | 归一化 7 大类 | -| 退款金额 | refund_amount | | -| 退款时间 | refund_time | | -| 最终状态 | final_status | 已退款/申请中/无需退款 | -| 退款单号 | refund_no | | -| 操作 | - | [详情] [时序] | - -**交互**: -- 默认按退款金额倒序 -- 点击 [详情] → 展开设备详情抽屉(见 5.3) -- 点击 [时序] → 新窗口打开 AuditView,device_sn 已带入 - -### 5.3 ⑤ 设备详情抽屉 - -**触发**:点击设备表的 [详情] 按钮 - -**布局**: - -``` -┌─ 抽屉(右侧滑出,宽度 60vw) ────────────────────────────────┐ -│ │ -│ ┌─ 设备画像 ──────────────────────────────────────────────┐│ -│ │ SN: ABC123 | 设备型号: xxx ││ -│ │ 首次接待: 2026-08-01 09:00 | 末次接待: 2026-08-08 14:00 ││ -│ │ 接待次数: 3 | 接待人员: 2人(叶珍凤,朱海洋) ││ -│ │ 最终结果: 失败(已退款) | 退款金额: ¥1,280 ││ -│ │ 退款时间: 2026-08-09 12:00 | 退款单号: RF20260809123 ││ -│ │ [查看完整时序] → 跳转 AuditView ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 接待记录时间线 ────────────────────────────────────────┐│ -│ │ 时间 | 接待人 | 问题描述 | 处理结果 | 接待时长 ││ -│ │ 2026-08-01 09:00 | 叶珍凤 | 不想用了 | 沟通挽回中 | 15分 ││ -│ │ 2026-08-05 14:00 | 叶珍凤 | 仍想退款 | 继续沟通 | 20分 ││ -│ │ 2026-08-08 14:00 | 朱海洋 | 坚持退款 | 转退款流程 | 10分 ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 退款记录 ──────────────────────────────────────────────┐│ -│ │ 退款单号 | 退款金额 | 退款方式 | 状态 | 操作人 | 审核人 ││ -│ │ RF20260809123 | ¥1,280 | 线上 | 已退款 | sys | 张蕾 ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 关联工单(若有) ──────────────────────────────────────┐│ -│ │ 工单号 | 服务项 | 服务类别 | 登记时间 | 完成时间 | 处理人││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ┌─ 钱包提现记录(若有) ──────────────────────────────────┐│ -│ │ 提现时间 | 提现金额 | 操作类型 ││ -│ └──────────────────────────────────────────────────────────┘│ -└──────────────────────────────────────────────────────────────┘ -``` - -### 5.4 ③ 接待→流失时差分布 - -**图表**:柱状图 - -**数据来源**:新增后端字段 `loss_time_dist` - -**展示**: -- X 轴:时差分桶(当天/1-3天/3-7天/7-15天/15天+) -- Y 轴:失败 SN 数 -- 颜色:按金额深浅区分(金额越大颜色越深) - -**业务意义**:识别"快流失"vs"慢流失"设备,指导挽回节奏 - -### 5.5 ④ 设备接待次数分布 - -**图表**:柱状图 + 折线(双 Y 轴) - -**展示**: -- X 轴:接待次数(1次/2次/3次/4次/5次+) -- 柱:设备数 -- 线:该桶失败率% -- 业务意义:接待次数越多失败率是否越高(沟通成本 vs 成功率) - -### 5.6 ① 设备风险分布 KPI(4 卡) - -| 卡片 | 含义 | 颜色 | -|---|---|---| -| 复退款设备数 | 同一 SN 在周期内出现≥2次退款诉求 | 红色 | -| 跨人员接待设备数 | 被≥2个不同人员接待过的 SN | 橙色 | -| 高金额设备数 | 退款金额>1000 元的 SN | 紫色 | -| 总失败设备数 | 周期内失败 SN 总数 | 灰色 | - -### 5.7 后端接口 - -#### 接口5:设备分析 - -``` -GET /api/v1/analytics/refund/device-analysis -参数: - start?: string - end?: string - reason?: string (失败原因筛选) - amount_min?: number - amount_max?: number - reception_count_min?: int - limit?: int (默认 20,Top N) - -返回: -{ - "risk_summary": { - "repeat_refund_devices": 8, // 复退款设备数 - "cross_person_devices": 12, // 跨人员接待设备数 - "high_amount_devices": 5, // 金额>1000 的设备数 - "total_failed_devices": 45, - }, - "top_devices": [ - { - "device_sn": "ABC123", - "reception_count": 3, - "receptionist_count": 2, - "receptionists": ["叶珍凤", "朱海洋"], - "fail_reason": "不想用了", - "refund_amount": 1280.00, - "refund_time": "2026-08-09T12:00:00", - "final_status": "已退款", - "refund_no": "RF20260809123", - }, - ... // Top20 - ], - "loss_time_dist": [ - {"bucket": "当天", "sn_cnt": 5, "amount": 1200.00}, - {"bucket": "1-3天", "sn_cnt": 12, "amount": 4800.00}, - {"bucket": "3-7天", "sn_cnt": 15, "amount": 7200.00}, - {"bucket": "7-15天", "sn_cnt": 8, "amount": 3600.00}, - {"bucket": "15天+", "sn_cnt": 5, "amount": 2400.00}, - ], - "reception_count_dist": [ - {"bucket": "1次", "device_cnt": 20, "fail_rate": 35.0}, - {"bucket": "2次", "device_cnt": 15, "fail_rate": 45.0}, - {"bucket": "3次", "device_cnt": 7, "fail_rate": 60.0}, - {"bucket": "4次", "device_cnt": 2, "fail_rate": 75.0}, - {"bucket": "5次+", "device_cnt": 1, "fail_rate": 100.0}, - ] -} -``` - -#### 接口6:设备详情 - -``` -GET /api/v1/analytics/refund/device-detail?device_sn=ABC123 -参数: - device_sn: string (必填) - start?: string - end?: string - -返回: -{ - "device_sn": "ABC123", - "device_type": "xxx", // 来自 devices 表(若有) - "first_reception": "2026-08-01T09:00:00", - "last_reception": "2026-08-08T14:00:00", - "reception_count": 3, - "receptionists": ["叶珍凤", "朱海洋"], - "result": "failed", - "fail_reason": "不想用了", - "refund_amount": 1280.00, - "refund_time": "2026-08-09T12:00:00", - "refund_no": "RF20260809123", - "final_status": "已退款", - "reception_timeline": [ - { - "time": "2026-08-01T09:00:00", - "operator": "企微-叶珍凤", - "operator_name": "叶珍凤", - "problem_type": "不想用了", - "description": "...", // cs_record 描述 - "execution_result": "...", - "consult_time": 15, // 接待时长(分钟) - }, - ... - ], - "refund_records": [ - { - "refund_no": "RF20260809123", - "refund_amount": 1280.00, - "refund_method": "线上", - "status": "已退款", - "operator": "sys", - "auditor": "张蕾", - "created_at_raw": "2026-08-09T12:00:00", - } - ], - "tickets": [ - { - "ticket_no": "GD20260808001", - "service_item": "不想用了", - "service_category": "退款", - "register_time": "2026-08-08T16:00:00", - "finish_time": "2026-08-09T12:00:00", - "handler": "企微-朱海洋", - } - ], - "wallet_records": [ - { - "wallet_id": "W001", - "op_time": "2026-08-09T13:00:00", - "op_amount": 1280.00, - "op_type": "用户提现", - } - ] -} -``` - -**实现要点**: -- 设备画像从 cs_records + refunds 聚合 -- 接待时间线从 cs_records 按时间正序 -- 退款/工单/钱包记录从 refund_wide 关联(已物化) -- 若 refund_wide 无该 SN,实时查 refunds/service_tickets/wallet_withdrawals - ---- - -## 六、Tab4:类型×趋势(P0 核心) - -### 6.1 布局 - -``` -┌─ 筛选栏 ────────────────────────────────────────────────────┐ -│ [时间范围:近30天▼] [粒度:日/周/月▼] [刷新] │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ① 各原因类型 接待量&成功率(双Y轴) ───────────────────────┐ -│ 24 col │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ② 各人员类型 接待量&成功率 ────────────────────────────────┐ -│ 24 col │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ③ 日期×原因 矩阵热力图 ────────────────────────────────────┐ -│ 24 col │ -└──────────────────────────────────────────────────────────────┘ - -┌─ ④ problem_type 细分 Top10 ──────┐ ┌─ ⑤ 组间对比 ──────────┐ -│ 12 col │ │ 12 col │ -└────────────────────────────────────┘ └──────────────────────────┘ -``` - -### 6.2 ① 各原因类型 接待量&成功率 - -**图表**:双 Y 轴(堆叠柱 + 多线) - -**展示**: -- X 轴:日期(按粒度切换) -- 左 Y 轴:SN 数(堆叠柱,7 大原因各一层) -- 右 Y 轴:成功率%(7 条线,每原因一条) -- 悬浮:日期 + 原因 + 接待N个 + 成功M个 + 成功率X% - -**业务意义**:看各原因类型的接待量变化 + 成功率波动 - -### 6.3 ② 各人员类型 接待量&成功率 - -**图表**:双 Y 轴(分组柱 + 多线) - -**展示**: -- X 轴:日期 -- 左 Y 轴:SN 数(分组柱:专职挽回/其他人员挽回/退款操作) -- 右 Y 轴:成功率%(3 条线) - -**业务意义**:对比专职挽回 vs 其他人员挽回的能力差异 - -### 6.4 ③ 日期×原因 矩阵热力图 - -**图表**:ECharts heatmap - -**展示**: -- 行:日期(近 30 天) -- 列:7 大原因 -- 值:失败 SN 数(颜色深浅) -- 可切换"失败数/接待数/成功率"三种色阶 - -### 6.5 ④ problem_type 细分 Top10 - -**图表**:横向柱状图 - -**展示**: -- 取周期内接待量 Top10 的 problem_type 原始值 -- 每条柱:接待 SN 数(按成功/失败堆叠) -- 旁注:成功率% - -**业务意义**:7 大类太粗,看细分哪些 problem_type 流失率高 - -### 6.6 ⑤ 组间对比 - -**图表**:横向分组柱 + 表格 - -**展示**: -- 5 个组(董梦玲组/吴晓龙组/叶珍凤组/朱海洋组/张蕾组) -- 每组:接待SN | 成功率 | 退款金额 | 人均挽回估算 | 失败数 - -### 6.7 后端接口 - -#### 接口7:类型趋势 - -``` -GET /api/v1/analytics/refund/type-trend -参数: - start?: string - end?: string - grain?: string (day/week/month,默认 day) - dimension?: string (reason/staff_type,默认 reason) - -返回(dimension=reason): -{ - "dates": ["2026-08-01", "2026-08-02", ...], - "reasons": ["不想用了", "充值错误", "设备故障", "网卡卡无网络", "套餐问题", "卡商问题", "其他"], - "series": [ - { - "reason": "不想用了", - "trend": [ - {"date": "2026-08-01", "total_sn": 5, "success_sn": 3, "failed_sn": 2, "success_rate": 60.0}, - ... - ] - }, - ... - ] -} - -返回(dimension=staff_type): -{ - "dates": [...], - "staff_types": ["专职挽回", "其他人员挽回", "退款操作"], - "series": [ - { - "staff_type": "专职挽回", - "trend": [...] - }, - ... - ] -} -``` - -**实现要点**: -- 按 reason:对每日每个 SN 归一化原因(从 cs_record.problem_type 或 refund.refund_reason / service_ticket.service_item 归一化) -- 按 staff_type:对每日每个 SN 取接待人的 staff_type -- 成功率复用 refund_recovery 的失败判定逻辑 - -#### 接口8:problem_type 细分 - -``` -GET /api/v1/analytics/refund/problem-type-detail -参数: - start?: string - end?: string - top?: int (默认 10) - -返回: -{ - "items": [ - { - "problem_type": "不想用了", // cs_record.problem_type 原始值 - "normalized_reason": "不想用了", // 归一化后的 7 大类 - "total_sn": 45, - "success_sn": 28, - "failed_sn": 17, - "success_rate": 62.2, - "total_amount": 8400.00, // 失败 SN 金额合计 - }, - ... - ] -} -``` - -#### 接口9:组间对比 - -``` -GET /api/v1/analytics/refund/group-comparison -参数: - start?: string - end?: string - -返回: -{ - "groups": [ - { - "group": "朱海洋组", - "person_cnt": 3, - "total_sn": 120, - "success_sn": 72, - "failed_sn": 48, - "success_rate": 60.0, - "total_refund_amount": 24000.00, - "estimated_save_amount": 18000.00, - "avg_success_rate": 55.5, // 人均成功率 - }, - ... - ] -} -``` - ---- - -## 七、Tab5:综合看板(保留微调) - -### 7.1 保留内容 - -原有三段结构不变: -- 区块1:催单风险(5 视图) -- 区块2:退款挽回专项(6 视图) -- 区块3:现金流与退款基础(7 视图) - -### 7.2 微调 - -- 挽回专项区块右上角增加"组间对比"入口按钮 → 点击跳转 Tab4 的 ⑤ -- 现金流区块增加"金额分桶"小图(复用 Tab1 的 amount_buckets) - ---- - -## 八、与 AuditView 的打通 - -### 8.1 跳转规则 - -| 来源 | 目标 | 参数 | -|---|---|---| -| Tab2 人员表 [时序] | AuditView(person) | person_id + 时间范围 | -| Tab2 设备清单 [时序] | AuditView(person+device) | person_id + device_sn + 时间范围 | -| Tab3 设备表 [时序] | AuditView(device) | device_sn + 时间范围 | -| Tab1 Top5设备 [查看全部] | Tab3 设备分析 | 切换 Tab | -| Tab2 热力图单元格 | Tab2 抽屉(带筛选) | person + reason | - -### 8.2 AuditView 改造 - -现有 AuditView 已支持 person/device 两种模式,需补充: -- 支持 URL query 同时带 person_id + device_sn(双维度过滤) -- 时间范围自动从 URL query 继承 -- 新窗口打开(不离开退款看板) - -### 8.3 路由 - -AuditView 路由已存在(在运营看板下),退款看板跳转时用新窗口: -```js -const url = `/workbench/audit?person_id=${pid}&device_sn=${sn}&start=${start}&end=${end}` -window.open(url, '_blank') -``` - ---- - -## 九、前端组件清单 - -### 9.1 新增组件 - -| 组件 | 位置 | 说明 | -|---|---|---| -| PersonnelAnalysis.vue | views/dashboards/refund/ | Tab2 人员分析主体 | -| DeviceAnalysis.vue | views/dashboards/refund/ | Tab3 设备分析主体 | -| TypeTrend.vue | views/dashboards/refund/ | Tab4 类型×趋势主体 | -| PersonDetailDrawer.vue | views/dashboards/refund/ | Tab2 人员详情抽屉 | -| DeviceDetailDrawer.vue | views/dashboards/refund/ | Tab3 设备详情抽屉 | -| HeatMapChart.vue | components/ | 通用热力图(人员×原因、日期×原因) | - -### 9.2 修改组件 - -| 组件 | 改动 | -|---|---| -| RefundDashboard.vue | 新增 3 个 Tab,调整 Tab 切换逻辑,URL query 同步 | -| AuditView.vue | 支持双维度(person+device)URL query | - -### 9.3 复用组件 - -- BaseChart.vue(已有,ECharts 封装) -- ViewItemCard.vue(已有,综合看板视图卡片) -- el-table / el-drawer / el-tag(Element Plus) - ---- - -## 十、后端实现要点 - -### 10.1 新增接口清单 - -| 接口 | 方法 | 路径 | 说明 | -|---|---|---|---| -| 1 | GET | /api/v1/analytics/refund/personnel-analysis | 人员分析汇总 | -| 2 | GET | /api/v1/analytics/refund/person-devices | 人员接待设备清单 | -| 3 | GET | /api/v1/analytics/refund/personnel-trend | 人员趋势 | -| 4 | GET | /api/v1/analytics/refund/person-reason-matrix | 人员×原因矩阵 | -| 5 | GET | /api/v1/analytics/refund/device-analysis | 设备分析 | -| 6 | GET | /api/v1/analytics/refund/device-detail | 设备详情 | -| 7 | GET | /api/v1/analytics/refund/type-trend | 类型趋势 | -| 8 | GET | /api/v1/analytics/refund/problem-type-detail | problem_type 细分 | -| 9 | GET | /api/v1/analytics/refund/group-comparison | 组间对比 | - -### 10.2 复用逻辑 - -所有接口复用 `refund_recovery_analysis` 的核心逻辑: -- 分母:cs_records.problem_category='退款' 的去重 SN -- 失败判定:T+7 窗口 + refunds + service_tickets + cs_record 关键词 -- 原因归一化:`_normalize_refund_reason` 7 大类 -- 人员匹配:PersonAlias → Person -- staff_type 分类:专职挽回/退款操作/其他人员挽回 -- 系统过滤:SYS_OP_BLACKLIST_RR + SYS_KW 描述过滤 - -### 10.3 性能考虑 - -- 人员分析、设备分析接口可复用 refund_recovery_analysis 的中间结果(若同请求已计算) -- type-trend 接口按日期×原因拆分,数据量较大,建议加缓存(5 分钟) -- device-detail 接口单设备查询,响应快 -- 所有接口支持 start/end 参数,默认近 30 天 - -### 10.4 文件改动 - -| 文件 | 改动 | -|---|---| -| backend/app/services/analytics.py | 新增 9 个函数(接口1-9),复用 refund_recovery_analysis 的辅助函数 | -| backend/app/api/v1/analytics.py | 新增 9 个路由 | -| 无需新增 model | 全部用现有表(cs_records/refunds/service_tickets/refund_wide/persons/person_alias/devices) | - ---- - -## 十一、实施计划 - -### Phase 1:P0 核心(3 个 Tab) - -1. **后端**:实现接口 1-4(人员分析相关) -2. **前端**:Tab2 人员分析 + 人员详情抽屉 -3. **后端**:实现接口 5-6(设备分析相关) -4. **前端**:Tab3 设备分析 + 设备详情抽屉 -5. **后端**:实现接口 7-9(类型趋势相关) -6. **前端**:Tab4 类型×趋势 -7. **联调**:AuditView 跳转打通 - -### Phase 2:Tab1 强化 - -1. **后端**:refund_recovery_analysis 追加 top_risk_devices + amount_buckets -2. **前端**:Tab1 新增 Top5 设备快照 + 金额分桶饼图 - -### Phase 3:Tab5 微调 - -1. **前端**:综合看板增加组间对比入口 + 金额分桶小图 - ---- - -## 十二、验收标准 - -### 12.1 功能验收 - -- [ ] Tab2 人员对比表展示所有字段(含估算金额/首末接待时间) -- [ ] Tab2 点击 [详情] 弹出抽屉,展示该人员接待的设备清单(成功/失败可切) -- [ ] Tab2 人员趋势图支持多选对比,日/周粒度切换 -- [ ] Tab2 人员×原因热力图可点击单元格下钻 -- [ ] Tab3 设备表展示 Top20,支持金额/原因/次数筛选 -- [ ] Tab3 点击 [详情] 弹出抽屉,展示接待时间线 + 退款记录 + 工单 + 钱包 -- [ ] Tab4 各原因类型趋势图(堆叠柱+多线)正常 -- [ ] Tab4 人员类型趋势图正常 -- [ ] Tab4 日期×原因热力图正常 -- [ ] Tab4 problem_type 细分 Top10 正常 -- [ ] Tab4 组间对比正常 -- [ ] AuditView 跳转携带 person_id/device_sn/时间范围参数 - -### 12.2 数据一致性 - -- [ ] Tab2 人员接待SN总数 = Tab1 KPI 的 total_sn -- [ ] Tab3 失败设备总数 = Tab1 KPI 的 failed_sn -- [ ] Tab4 各原因成功率与 Tab1 reason_dist 一致 -- [ ] 所有口径与运营看板 refund-recovery 接口一致 - -### 12.3 性能 - -- [ ] 各接口响应时间 < 3 秒(近 30 天数据) -- [ ] 热力图渲染 < 1 秒 -- [ ] 抽屉打开 < 1 秒 - ---- - -## 附录 A:7 大原因归一化规则 - -(来自 analytics.py `_REASON_NORMALIZE_RULES`) - -| 归一化桶 | 关键词 | -|---|---| -| 不想用了 | 不想用/不想要/没用了/不需要/退订/取消 | -| 充值错误 | 充错/充值错/充多/误充/错充 | -| 设备故障 | 设备/坏了/故障/损坏/不能用/用不了/死机/卡机 | -| 网卡卡无网络 | 网卡/卡慢/网速/网络慢/网慢/没网/无网/没信号/信号差/掉线 | -| 套餐问题(流量虚涨价升级) | 套餐/流量/升级/涨价/资费/太贵 | -| 卡商问题 | 卡商/渠道/代理/上游 | -| 其他(填写备注) | 兜底 | - -## 附录 B:staff_type 分类规则 - -(来自 analytics.py) - -| staff_type | 判定条件 | -|---|---| -| 专职挽回 | Person.specialty='退款挽回'(通过 _hr_map 映射) | -| 退款操作 | 无 CS 接待,仅作为 refunds.operator/service_tickets.handler | -| 其他人员挽回 | 非专职人员(组长/主管/咨询/套餐/投诉等)接待退款类 SN | - -## 附录 C:失败判定三路合并(T+7 窗口) - -| 来源 | 表 | 条件 | -|---|---|---| -| 退款单 | refunds | status ∉ (无需退款,驳回),created_at_raw 在 [首次接待, 首次接待+7天] | -| 退款工单 | service_tickets | service_category='退款',status≠终止,register_time 在 T+7 窗口 | -| CS描述关键词 | cs_records | description/execution_result/solution 含(退款说明/退款后重新充值/要求退回微信/非挽回用户) | diff --git a/.trae/documents/time-aware-specialty-workload.md b/.trae/documents/time-aware-specialty-workload.md deleted file mode 100644 index e9b4f22..0000000 --- a/.trae/documents/time-aware-specialty-workload.md +++ /dev/null @@ -1,191 +0,0 @@ -# 按天匹配专项的工作量分计算改造 - -## Context - -当前人均效率计算在 `today_staff_efficiency` 中,使用 `Person.specialty`(单一当前值)判定所有工作桶的"本职/转接"系数。如果人员在查询周期内变更了专项(如 8月1-10日咨询接待 → 8月11日起退款挽回),旧工作会被错误地按新专项系数计算。 - -改造目标:按天匹配专项,对查询周期内每一天使用该天实际生效的专项系数。 - -## 方案:按天加权平均系数 - -### 核心思路 - -不改变 SQL 聚合粒度(仍是按人汇总),而是在 Python 侧预建**按天专项映射表**,对每个专项桶计算**天数加权平均系数**。 - -**前提假设**:工作分布在各天大致均匀(对于多天查询,这个近似足够准确;对于单天查询,系数完全精确)。 - -### 具体步骤 - -#### 1. 预建按天专项映射表 - -在 `today_staff_efficiency` 的工作量计算循环之前(约 line 4801),新增一段代码: - -```python -# 1. 收集所有有工作记录的人员的 person_id -work_person_ids = set() -for r in rows: - if r.person_id: - work_person_ids.add(r.person_id) - -# 2. 查询 PersonSpecialtyAssignment,获取查询周期内每天的专项 -from datetime import date as _date_cls, timedelta -query_start = ts.date() if hasattr(ts, 'date') else _date_cls.fromisoformat(str(ts)[:10]) -query_end = te.date() if hasattr(te, 'date') else _date_cls.fromisoformat(str(te)[:10]) - -# 生成日期列表 -date_list = [] -d = query_start -while d <= query_end: - date_list.append(d) - d += timedelta(days=1) - -# 一次性查出所有 work_person_ids 的专项分配历史 -specialty_assignments = db.execute( - select( - PersonSpecialtyAssignment.person_id, - PersonSpecialtyAssignment.effective_from, - PersonSpecialtyAssignment.effective_to, - Specialty.name.label("specialty_name"), - Specialty.difficulty_weight, - ) - .select_from(PersonSpecialtyAssignment) - .join(Specialty, PersonSpecialtyAssignment.specialty_id == Specialty.id) - .where( - PersonSpecialtyAssignment.person_id.in_(work_person_ids), - PersonSpecialtyAssignment.is_primary == True, - # 专项分配区间与查询区间有交集 - or_( - PersonSpecialtyAssignment.effective_to.is_(None), - PersonSpecialtyAssignment.effective_to >= query_start, - ), - PersonSpecialtyAssignment.effective_from <= query_end, - ) -).all() - -# 3. 构建 person_id → {date: specialty_name} 映射 -person_daily_specialty: dict[int, dict[_date_cls, str]] = {} -for sa in specialty_assignments: - pid = sa.person_id - sp_name = sa.specialty_name - eff_from = sa.effective_from - eff_to = sa.effective_to or query_end - if pid not in person_daily_specialty: - person_daily_specialty[pid] = {} - for d in date_list: - if eff_from <= d <= eff_to: - # 如果同一天有多个专项分配,取 is_primary=True 的(上面已过滤) - person_daily_specialty[pid][d] = sp_name - -# 4. 构建 person_id → {specialty_name: days_count} -person_specialty_days: dict[int, dict[str, int]] = {} -for pid, daily_map in person_daily_specialty.items(): - counts: dict[str, int] = {} - for sp in daily_map.values(): - counts[sp] = counts.get(sp, 0) + 1 - person_specialty_days[pid] = counts -``` - -#### 2. 改造工作量分系数计算 - -在现有循环中(约 line 4864-4887),将单一 `hr_cat` 替换为按桶计算的加权系数。 - -**修改前**(line 4864-4887): -```python -hr_cat = _hr_map(r.sp, position) -mgmt = (position in ("主管", "组长", "副主管")) or hr_cat == "管理层" - -for (bucket_key, cat_name, sp_set) in CAT_BUCKETS: - sv, pv = cat_data[bucket_key] - if sv == 0 and pv == 0: - continue - if mgmt: - own = True - coef = CAT_DEFAULT_WEIGHT[cat_name] - elif hr_cat is not None and hr_cat in sp_set: - own = True - coef = WEIGHT[hr_cat] - else: - own = False - coef = TRANSFER_PENALTY - wp = sv * coef - work_score += wp -``` - -**修改后**: -```python -position = r.pos or "" -# 管理岗判定不变 -mgmt = (position in ("主管", "组长", "副主管")) -# 当前主专项(用于展示,不用于计算) -hr_cat = _hr_map(r.sp, position) - -# 获取该人员的按天专项分布 -pid = r.person_id -sp_days = person_specialty_days.get(pid, {}) if pid else {} -total_days = sum(sp_days.values()) if sp_days else 0 - -for (bucket_key, cat_name, sp_set) in CAT_BUCKETS: - sv, pv = cat_data[bucket_key] - if sv == 0 and pv == 0: - bd[cat_name] = {...} - continue - - if mgmt: - # 管理岗: 跨类豁免,用大类默认系数(不变) - own = True - coef = CAT_DEFAULT_WEIGHT[cat_name] - elif total_days > 0: - # 按天加权平均系数 - weighted_coef = 0.0 - own_days = 0 - for sp_name, days in sp_days.items(): - sp_cat = _hr_map(sp_name, position) - if sp_cat and sp_cat in sp_set: - weighted_coef += WEIGHT.get(sp_cat, 1.0) * days - own_days += days - else: - weighted_coef += TRANSFER_PENALTY * days - coef = weighted_coef / total_days - own = own_days > total_days / 2 # 大部分天数是本职则标记为本职 - else: - # 无按天数据,回退到原逻辑 - if hr_cat is not None and hr_cat in sp_set: - own = True - coef = WEIGHT.get(hr_cat, 1.0) - else: - own = False - coef = TRANSFER_PENALTY - - wp = sv * coef; ep = pv * coef - work_score += wp; eff_score += ep - bd[cat_name] = { - "sessions": sv, "people": pv, - "weight": round(coef, 3), "coef": round(coef, 3), "is_own": own, - "work_contrib": round(wp, 1), "eff_contrib": round(ep, 1), - } -``` - -### 关键设计决策 - -1. **天数加权平均**:假设工作在各天均匀分布,用天数比例加权。对于单天查询,结果完全精确;对于多天查询,误差在可接受范围。 - -2. **管理岗不变**:主管/组长/副主管的跨类豁免逻辑不变,仍使用 `CAT_DEFAULT_WEIGHT`。 - -3. **回退兼容**:当 `PersonSpecialtyAssignment` 无数据时(新人/未分配专项),回退到原单一 `hr_cat` 逻辑。 - -4. **性能**:一次 SQL 查询 + Python 字典构建,不增加 SQL 查询次数。 - -## 变更文件 - -| 文件 | 变更 | -|------|------| -| `backend/app/services/analytics.py` | 在 `today_staff_efficiency` 函数中新增按天专项映射构建 + 改造系数计算逻辑 | - -## 验证 - -1. 修改覃克泳的专项:8月1-10日=咨询接待,8月11日起=退款挽回 -2. 查询8月1-14日人均效率,确认: - - 咨询接待桶的系数接近 1.0(大部分天数本职) - - 退款挽回桶的系数接近 2.2(大部分天数本职) -3. 查询8月11日单天,确认退款挽回桶系数=2.2 -4. 管理岗人员的系数不受影响 \ No newline at end of file diff --git a/.trae/documents/收尾计划-AI治理大任务前.md b/.trae/documents/收尾计划-AI治理大任务前.md deleted file mode 100644 index 72ee1b3..0000000 --- a/.trae/documents/收尾计划-AI治理大任务前.md +++ /dev/null @@ -1,105 +0,0 @@ -# 收尾计划:AI 治理大任务前的尾巴清理 - -## Context - -用户即将启动 AI 数据治理框架(#162~#165,四张工单、体量较大)。开工前需要把前面工作留下的尾巴收干净,避免"旧账+新账"混在一起。 - -用户原话:**"因为后续的任务比较大,前面的工作要及时收一下尾巴,然后持续执行"**。 - -本轮已确认范围(用户拍板): -- **只收尾巴,然后转大任务**(不做 C-P1-4 权限矩阵、不做 C-P1-8 容器非 root) -- wallet 备份:**表 + dump 都留到 09-26 到期日统一清** - -## 收尾前的现状核查(本轮已完成,均为只读) - -| 事项 | 核查结论 | -|---|---| -| wallet 备份 dump 可用性 | ✅ 已验证:`wallet_withdrawals_20260919.sql.gz` 解出 69691 行,其中 `COPY` 数据段 **69455 行**,与 `wallet_withdrawals_bak_20260919` 表行数**完全一致** → 备份可恢复 | -| #161 修复后是否仍有新重复 | ✅ 主表按 `(account_id,op_type,op_time,operator,op_amount) WHERE device_sn IS NULL` 分组重复组 = **0**;部分索引 `uq_wallet_withdrawals_null_sn` 已生效 | -| 同类风险(可空列进唯一键) | ✅ 已扫描生产库 59 个唯一索引(含可空列 19 个),报出 4 处"重复"经**逐组甄别全部为 NULL 组**(PG 对 NULL 不去重是规范行为,非违约)→ **生产无真违约**,无需修复 | -| 生产定时备份(C-P1-2) | ❌ **真实缺口**:root/tanzk crontab 均空、无 systemd timer、`/etc/cron.d` 无 csctalon 项 → **生产当前没有任何自动备份** | -| 备份目录占用 | ⚠️ `/opt/csctalon/backups` = **5.2G**(10 个文件,多为升级时 `upgrade.sh` 自动产生的全量包 760M+/个);磁盘可用 184G | - -## 任务 1:补生产每日定时备份(C-P1-2 核心缺口) - -**为什么**:`deploy/backup.sh` 早已写好且支持 `--env-file/--output/--keep`,`deploy/README.md §5.1` 也明确建议配 cron 每日跑 —— 但生产**从未配置**。当前仅靠 `upgrade.sh` 升级时顺带备份,意味着"不升级就没有备份",且无异地留存。这是审计 C-P1-2「无备份/恢复方案」的实际缺口。 - -**做法**(不新增脚本,复用既有 `backup.sh`): - -1. 在生产服务器 `/etc/cron.d/csctalon-backup` 落一个 cron 文件(比 `crontab -e` 更可审计、可版本化): - ``` - # 每日 03:30 备份数据库+附件+登录态, 保留最近 14 份 - 30 3 * * * root /opt/csctalon/current/deploy/backup.sh --env-file /opt/csctalon/.env --output /opt/csctalon/backups --keep 14 >> /var/log/csctalon-backup.log 2>&1 - ``` - - 用 `current` 软链 → 随版本升级自动用最新脚本 - - `--keep 14`(README 推荐值):每日一份 × 14 天 -2. 同步 `deploy/README.md §5.1`:把"建议配置"改为"已配置"的现状说明(一行),并补 `/etc/cron.d/` 方式(原文档只写了 `crontab -e`)。 -3. 加 logrotate(可选但推荐):`/etc/logrotate.d/csctalon-backup` 轮转 `/var/log/csctalon-backup.log`,防日志无限增长。 - -**验证**: -- `sudo /opt/csctalon/current/deploy/backup.sh --env-file /opt/csctalon/.env --output /opt/csctalon/backups --keep 14` 手动跑一次 → 产出新包且健康 -- `sudo -n cat /etc/cron.d/csctalon-backup` 确认落盘 -- 检查 `run-parts`/cron 生效:`sudo systemctl status cron` + 次日看 `/var/log/csctalon-backup.log` -- 抽验备份包可恢复:`tar tzf <新包>` 列出 `csctalon.dump / uploads / browser_states / backup.info` - -**风险控制**: -- `--keep 14` 会清理超出的 `csctalon-backup-*.tar.gz`(不匹配该前缀的 `pre-org-*.dump`、`wallet_*.sql.gz`、`ai_reports_*.dump` **不受影响**,因 `ls -1t csctalon-backup-*.tar.gz` 只匹配该前缀)→ 需在验证时确认手工 dump 仍在 -- 5.2G 现状:新 cron 跑起来后靠 `--keep 14` 自然收敛(每日增量约 770M,14 份约 10G,磁盘 184G 可用,可接受) - -## 任务 2:wallet 备份到期清理机制 - -**用户决定**:表 + dump 都留到 **2026-09-26** 统一清。 - -**做法**: -- 不在本轮删除任何东西 -- 在 `/etc/cron.d/csctalon-backup` 同文件追加一条**一次性到期清理**,或在 `docs/审查行动项登记.md` 登记"09-26 清理"待办 -- 推荐:**登记到行动项台账**(C-P1-7 备注补一行),比藏一条 cron 更可见;到期由人确认后删(备份删除是不可逆操作,人工确认更稳) - -清理对象(09-26 执行): -```sql -DROP TABLE IF EXISTS wallet_withdrawals_bak_20260919; -- 69455 行 -``` -```bash -sudo rm -f /opt/csctalon/backups/wallet_withdrawals_20260919.sql.gz # 2.5M -sudo rm -f /opt/csctalon/backups/ai_reports_pre166_20260920.dump # 195K (#166 清理的备份) -sudo rm -f /opt/csctalon/backups/pre-org-20260914.dump # 108M (09-14 组织调整, 已过期) -``` - -## 任务 3:登记同类风险排查结论(闭环 C-P1-7 的"同类排查"尾巴) - -**做法**:更新 `docs/审查行动项登记.md` 的 C-P1-7 行,备注补: -- 同类风险已全库扫描(59 个唯一索引 / 19 个含可空列) -- 4 处报警经甄别**全为 NULL 组**(PG 规范允许),**无真违约** -- 扫描脚本方法论(v2 复现索引真实表达式 + 部分条件,修正 v1 裸列名误报) - -同时在 `docs/design/AI数据治理框架-20260919.md` 的探测器设计(D1)中,把本次扫描脚本沉淀为 D1 的实现依据(D1 = 唯一索引重复探测器,正好复用 v2 逻辑)。 - -## 任务 4:清理文档/工单尾巴 - -- `docs/审查行动项登记.md`:C-P1-7 状态由「待处理」→「✅ 已完成(2026-09-19~20)」,引用 #160/#161 -- 确认 #162~#165 工单正文齐备(已建,open),本轮不动 - -## 不做(明确记录取舍) - -| 项 | 为什么不做 | -|---|---| -| C-P1-4 权限矩阵收紧 | 跨后端 7 类端点 + 前端角色控制 + 测试,属独立工作包;用户选择先转大任务 | -| C-P1-8 容器非 root | Playwright/Chromium/Xvfb 非 root 运行需 `--no-sandbox` 与写盘权限验证,风险最高,需单独排期 | -| 删除 wallet 备份 | 未到 09-26 到期日,且删除不可逆 | -| 异地 rsync 备份 | README 有建议但需目标机;本轮先保证"有本地每日备份",异地另议 | - -## 验证清单 - -1. 手动跑一次 `backup.sh` → 新包生成、`tar tzf` 内容齐全、手工 dump 文件未被误删 -2. `/etc/cron.d/csctalon-backup` 落盘且语法正确(`sudo run-parts --test /etc/cron.d` 或等次日日志) -3. `docs/审查行动项登记.md` C-P1-7 已更新为已完成并含同类排查结论 -4. 提交(携带 `#161`/`#160` 关联)+ 推送 -5. 转 #162 开始治理框架 A(两表 + D1 探测器) - -## 关键文件 - -- [deploy/backup.sh](file:///Users/tanzk/dev/csctalon/deploy/backup.sh) —— 既有备份脚本(复用,不改逻辑) -- [deploy/README.md](file:///Users/tanzk/dev/csctalon/deploy/README.md) —— §5.1 定时备份章节(补 `/etc/cron.d/` 方式 + 现状) -- [docs/审查行动项登记.md](file:///Users/tanzk/dev/csctalon/docs/审查行动项登记.md) —— C-P1-7 状态与备注 -- [docs/design/AI数据治理框架-20260919.md](file:///Users/tanzk/dev/csctalon/docs/design/AI数据治理框架-20260919.md) —— D1 探测器实现依据(沉淀扫描脚本) -- `/tmp/scan_dup_v2.py` + `/tmp/triage_dup.py` —— 本轮扫描脚本(D1 参考实现) diff --git a/.trae/specs/191-299-closeout/spec.md b/.trae/specs/191-299-closeout/spec.md deleted file mode 100644 index e5f69b7..0000000 --- a/.trae/specs/191-299-closeout/spec.md +++ /dev/null @@ -1,86 +0,0 @@ -# Spec:`#191 收口` + `#299 收口` - -**性质**:需求(REQ) · 一次性任务规格(非 META,不约束其他任务) -**日期**:2026-10-15 -**关联工单**:`#191`(E 路恒 0 行治理,open)· `#299`(device_combo 历史回补,open) - ---- - -## 一、已核实事实(2026-10-15,生产 VM400 / release `git-20261015081123`) - -| 项 | 事实 | 取证方式 | -|---|---|---| -| #191 核验 | **未通过**(EXIT=1) | 容器内 `check_ai_issue_gates.py --issue 191 --allow-real-db` | -| E 路 scheduled 记录 | 近 **20 天零条** `trigger_type=scheduled`;7 条全部 `manual` | `SELECT ... FROM bms_sync_logs WHERE data_type='refund_record_report'` | -| E 路时效 | 最新业务日 `2026-10-10`,`lag=5`,`stale=True` | `check_freshness()` | -| 运行时配置 | 正确:低频源含 `refund_record_report`;`list_low_freq_schedules → {'refund_record_report': {'hour': 15, 'window_days': 3}}` | 容器内直调服务函数 | -| 容器启动日志 | 16:17 BJT 启动时「低频档回填:今日已执行的小时档 `['2026-10-15:6']`」→ **15 档未执行** | `docker logs csctalon-backend` | - -## 二、根因(代码级,已定位到行) - -`backend/app/services/bms_scheduler.py` 低频档分支(约 L836–842): - -```python -elif should_run_low_freq( - now_cn.hour, - low_freq_hour=now_cn.hour, - done=False, - busy=sync_task_manager.is_busy(), -): -``` - -**未传 `deadline_hour`** → 取默认 `LOW_FREQ_DEADLINE_HOUR = 7`;而 `should_run_low_freq` 第 185 行 -`if now_hour >= deadline_hour: return False`,故 **任何 per-source hour ≥ 7 的低频档恒不触发** -(默认 06:00 档因 6 < 7 仍正常,因此长期未暴露)。 - -到达 `:59` 后落 `elif now_cn.minute >= 59` 分支 → 记 `abandoned` 入 gap 队列,**无同步尝试 → 不写 -`bms_sync_logs`**,与「零 scheduled 记录」现象完全吻合。 - -**影响**:E 路(退款记录报表,对账基准)自 #191 于 09-24 引入「按源可配小时」后**从未自动采集**, -数据仅靠人工手动触发维持,当前落后 5 天。这与 #191 工单注释(L804)声明的语义 -「窗口 = 该源配置小时起 1 小时内」相矛盾。 - -## 三、变更 A:`#191` 根因修复 - -- **唯一改动点**:`backend/app/services/bms_scheduler.py` L836–842,补 `deadline_hour=now_cn.hour + 1`。 -- **不改**:`should_run_low_freq` 本体、`:59` abandon 分支、默认 06:00 档行为、fetch 窗口逻辑。 -- **语义恢复**:窗口 = `[该源配置小时, 该源配置小时 + 1)`,与工单注释一致。 -- **测试**: - 1. `backend/tests/test_source_freshness.py`(或 `test_bms_scheduler_t1.py`)新增用例: - `should_run_low_freq(15, low_freq_hour=15, deadline_hour=16, done=False, busy=False) is True` - 与 `... deadline_hour=7 ...` 为 False(锁死根因)。 - 2. **漂移守卫**:断言 `bms_scheduler` 源码低频分支调用 `should_run_low_freq` 必带 - `deadline_hour=now_cn.hour + 1`(仿既有「`expected_by_hour` 必须等于采集种子 `low_freq_hour`」守卫写法,防止两处口径再次分叉)。 - 3. `pytest` + `ruff` 全绿。 - -## 四、变更 B:`#299` 顺带项 5(覆盖率异常波动告警)——**待用户拍板** - -原文:「同步覆盖缺口监控——低频档跑完对比『devices 无套餐比例』异常波动告警(成功≠完整)」。 - -- **建议实施,且落在监控模块(非采集核心)**:`backend/app/services/source_freshness_service.py` - 新增只读巡检 `check_device_combo_coverage()`: - - 指标:设备无有效套餐比例(口径须与 #298/#320/#321 一致,套餐以 `dim_device_combo` 为单一真实源); - - 触发:比例超阈值(初值 **5%**,当前基线 **2.62%**)→ `record_system_event(category="sync", - action="device_combo_coverage", level="warning")`,按日节流(复用现有 `_emit_once`)。 - - 该模块即 #191 交付的「数据源时效与空结果告警」巡检,由调度器每小时调用、**只读、零业务写入**, - 不属于采集链路。 -- **降级选项**:若不实施 → 在 #299 关闭凭证中显式写 - `结论: 以 <理由> 降级验收`,禁止静默关闭。 - -## 五、发布与验收 - -- **发布**:`git push origin main` → 节点标准入口 `curl ... upgrade.sh | sudo bash -s -- --git`(禁手拼命令)。发布后核 `VERSION` 与迁移 head。 -- **#191 关闭条件**(工单原文): - 1. 生产**连续 2 个自然日** `scheduled` 档 `total_rows > 0` **且** `check_freshness` 的 E 路 `lag_days ≤ 1`、`stale=false`(核验脚本 EXIT=0); - 2. 0 行场景能触发 `system_events` 告警(已有 `#5385` 历史证据); - 3. 巡检可复现(`check_freshness` 输出); - 4. 单测 + `ruff` + `pytest` 全绿; - 5. 若验收窗口内因上游无法达成 → 允许降级并登记待观察项。 -- **#299 关闭凭证**:A–F 逐条已有证据(探针结论、切片折半、全量回补 `min=2024-01-01`、残差 34,523 无流水 / 18,136 待 #321、台账 125 success + 64 split 无残留、17 单测)+ 顺带项 5 结论。 - -## 六、风险与约束 - -1. **变更 A 属采集调度链路** —— 触发 AGENTS.md「未经用户确认不得修改采集核心」铁律 → **须用户明示批准后再动手**。 -2. E 路上游 T+1:修复后需观察 1 个自然日确认 15:00 档能取到数(09-24 实验结论复用,不重做探针)。 -3. 变更 B 若被纳入调度触发路径则同属采集链路(亦须批准);仅作独立每小时只读巡检则属监控范围。 -4. 存在 `abandoned` 已入 gap 队列的存量记录:修复后 05:00 缺口清点会重试,属正常收口,不额外处理。 diff --git a/.trae/specs/191-299-closeout/tasks.md b/.trae/specs/191-299-closeout/tasks.md deleted file mode 100644 index d89ecaf..0000000 --- a/.trae/specs/191-299-closeout/tasks.md +++ /dev/null @@ -1,45 +0,0 @@ -# Tasks:`#191 收口` + `#299 收口` - -> 依赖 spec.md。**T1 之前需用户对「变更 A 是否放行」明示批准(铁律:未经确认不得修改采集核心)。** - -## 阶段 0 · 用户拍板(阻塞门) - -- [x] **T0** 用户确认两件事: - - (a) 放行变更 A(`bms_scheduler.py` 低频分支补 `deadline_hour=now_cn.hour + 1`); - - (b) 变更 B 选「实施」还是「降级验收」。 → **批准 spec ⇒ (a) 放行、(b) 实施** - -## 阶段 1 · #191 根因修复(变更 A) - -- [x] **T1** 在 `backend/app/services/bms_scheduler.py` 低频档分支(L836–842)补 - `deadline_hour=now_cn.hour + 1`,并在该行上方补注释说明「窗口 = 该源配置小时起 1 小时」。 -- [x] **T2** 新增/改单测(`backend/tests/test_source_freshness.py` 或 `test_bms_scheduler_t1.py`): - - 用例 1:`should_run_low_freq(15, low_freq_hour=15, deadline_hour=16, done=False, busy=False) is True` - - 用例 2:`should_run_low_freq(15, low_freq_hour=15, deadline_hour=7, done=False, busy=False) is False`(锁根因) - - 用例 3:源码漂移守卫 —— 断言调用点含 `deadline_hour=now_cn.hour + 1` -- [x] **T3** 本地回归:`backend/.venv/bin/python -m pytest tests/ -q`(全量,期望 0 失败)+ `ruff check`。 - → 全量 EXIT=0;`test_source_freshness.py` 19 passed;ruff 全绿。 -- [x] **T4** 提交(commit message 带 `#191`)→ `git push origin main` → 节点标准入口 - `curl -fsSL http://10.147.17.41:3000/tanzk/csctalon/raw/branch/main/deploy/upgrade.sh | sudo bash -s -- --git`。 - → commit `a8b68f6`;已推送 main;发布 `git-20261015081123 → git-20261015090830`。 -- [x] **T5** 发布后核验:`VERSION` == 新 release;容器 healthy;日志出现「低频档触发 …15:xx 低频源=['refund_record_report']」。 - → current=`git-20261015090830`、healthy、域名 200、部署树含修复行 L845。 - (「低频档触发」日志需等 10-16 15:00 档,见 T6) -- [ ] **T6** 观察窗口:次日起连续 2 个自然日 15:00 档 `bms_sync_logs(data_type=refund_record_report, trigger_type=scheduled)` 各 1 条且 `total_rows>0`。 -- [ ] **T7** 跑核验脚本(scp + docker cp + `--issue 191 --allow-real-db`),期望 **EXIT=0**;若非 0 且原因在上游 → 按工单第 5 条降级并在凭证登记待观察项。 -- [ ] **T8** 按工单纪律写 `## 关闭凭证核验 (YYYY-MM-DD)`(逐条对应关闭条件 1–5,给可复现命令/SQL),勾选正文 `[x]`,关闭 `#191`。 - -## 阶段 2 · #299 收口 - -- [x] **T9** (仅当 T0(b)=实施)在 `backend/app/services/source_freshness_service.py` 新增 - `check_device_combo_coverage()` 只读巡检 + 阈值告警(初值 5%),并接入现有每小时巡检入口; - 补单测;随阶段 1 一并发布。 → 提交 `a8b68f6`,2 个单测。 -- [x] **T10** 写 `## 关闭凭证核验 (YYYY-MM-DD)` for `#299`(comment 3890)。 -- [x] **T11** 勾选 `#299` 正文 `[ ]` → 关闭(closed_at 2026-10-15T17:19:29+08:00)。 - - -## 明令不做 - -- 不改 `should_run_low_freq` 本体与 `:59` abandon 分支(避免扩大采集语义变更面)。 -- 不改 E 路 fetch 窗口/tag 逻辑(09-24 实验结论已复核,不重做探针)。 -- 不动 `docs/arch/A-20260907-001-*`(LOCKED)——本修复是把实现拉回该文档已锁定的「每天 1 次、窗口 1 小时」语义。 -- 不为 #299 新增前端界面(原方案取舍:入口仅调度自动档 + admin API)。 diff --git a/.workbuddy/memory/2026-08-20.md b/.workbuddy/memory/2026-08-20.md deleted file mode 100644 index a6cab2e..0000000 --- a/.workbuddy/memory/2026-08-20.md +++ /dev/null @@ -1,18 +0,0 @@ -# 2026-08-20 工作日志 - -## 项目审计 - -对 CSCTalon 项目进行了全面审计,覆盖五大维度: -- **架构与配置**: Docker Compose 部署合理,.env 未入库,端口暴露受限 -- **后端代码质量**: 分层清晰但 analytics.py 8508 行严重超标;30+ 处裸 except 吞异常;零测试覆盖;死依赖 passlib -- **前端代码质量**: Vue 3 懒加载路由 + DOMPurify XSS 防护良好;但无 TypeScript、无 ESLint;SyncDashboard.vue 2010 行超标 -- **数据库**: 56 表完整,37 个 Alembic 迁移单 head,连接池配置合理,启动咨询锁防重复迁移 -- **安全**: bcrypt + JWT token_version 吊销 + 登录限速 + SafeStaticFiles + Fernet 加密 + CSP + 操作日志,安全实现质量高 - -审计报告已输出至 docs/audit-report-20260820.html。 - -### 关键发现 -- P0: 零测试覆盖(tests/ 目录为空) -- P1: 超大文件拆分、裸 except 清理、死依赖清理 -- P2: TypeScript 引入、CI/CD、限速迁移 Redis -- P3: 加密 key 独立化、前端组件拆分、依赖精确锁定