Jelajahi Sumber

fixd: 解决多个页面数据逻辑不同步

ice-static 2 bulan lalu
induk
melakukan
df79b0f840

+ 89 - 0
.claude/commands/audit-risk.md

@@ -0,0 +1,89 @@
+---
+name: audit-risk
+description: 风险预警审计 — 分析指定时间段内的群聊消息,检测关键词覆盖率和漏报情况,生成 HTML/MD 审计报告和管理建议
+argument-hint: "[开始日期] [结束日期]"
+metadata:
+  category: 风控审计
+  frequency: 每周 / 每月
+---
+
+# /audit-risk — 风险预警覆盖审计技能
+
+## 技能说明
+
+对拉迷家居企微群聊消息进行事后审计,分析风险预警系统的覆盖效果:
+- 关键词命中覆盖率统计
+- 漏报群检测(关键词命中但未触发预警)
+- AI 深度评审漏报消息
+- 可视化 HTML 报告 + MD 管理建议报告
+
+## 前置条件
+
+- 环境变量: `DEEPSEEK_API_KEY`(可选,不配则跳过 AI 评审)
+- Parse Server 可正常连接
+- 确保 `backend/scripts/` 目录下存在以下脚本:
+  - `audit-risk-coverage.ts` — 覆盖审计(输出 HTML)
+  - `audit-risk-deep-analysis.ts` — 深度分析(输出 MD)
+
+## 执行流程
+
+### 第一步:运行覆盖审计
+
+```bash
+cd backend && npx tsx scripts/audit-risk-coverage.ts
+```
+
+输出: `backend/scripts/audit-risk-YYYY-MM-DD.html`
+- 在浏览器打开查看可视化报告
+- 包含概览卡片、已触发预警列表、漏报群详情(含 AI 评审)、关键词云、管理建议
+
+### 第二步:运行深度分析
+
+```bash
+cd backend && npx tsx scripts/audit-risk-deep-analysis.ts
+```
+
+输出: `backend/scripts/audit-risk-deep-YYYY-MM-DD.md`
+- 消息活动分布(按天/群/发送者)
+- 关键词类别有效性排名
+- 漏报原因三分类(促销/业务/真实风险)
+- AI 管理建议(5 维度: 关键词/规则/盲区/流程/审计)
+- 具体行动项(本周/本月/持续)
+
+### 第三步:解读报告
+
+重点关注以下指标:
+1. **关键词覆盖率**: < 5% 说明关键词库严重冗余,需精简
+2. **高危漏报群**: 质量类/投诉类关键词命中但未预警的群,需要人工介入
+3. **类别有效率**: 0% 的类别需要重新审核关键词是否合理
+4. **AI 评审结论**: "⚠ 有风险" 的是真漏报,"✅ 无风险" 的是误匹配
+
+### 第四步:执行优化
+
+根据 AI 管理建议,执行关键词库优化:
+- 移除促销类误匹配词(定金/订金/优惠等)
+- 升级质量类关键词权重(脱胶/开裂/变形等)→ 命中即报
+- 淘汰连续 30 天零命中的沉睡词
+- 补充口语化投诉变体
+
+## 技能文档
+
+完整 SOP 参考: `backend/scripts/audit-risk-README.md`
+
+## 数据表依赖
+
+| 表 | 用途 |
+|---|------|
+| Message | 消息内容 + 发送者 + 时间戳 |
+| RiskEvent | 已触发的风险预警事件 |
+| RiskKeyword | 启用的风险关键词库 |
+| GroupChat | 群名称 + 小区映射 |
+
+## 关键 KPI
+
+| 指标 | 当前值 | 目标值 |
+|------|--------|--------|
+| 关键词覆盖率 | 2.2% | > 10% |
+| 月度漏报群 | 21 | < 5 |
+| 误报率 | ~0% | < 30% |
+| 预警响应时间 | 不定 | < 30min |

+ 40 - 0
.claude/commands/backfill-nicknames.md

@@ -0,0 +1,40 @@
+---
+name: backfill-nicknames
+description: 昵称回填 — 从 Message 表 senderName 跨房间回填 GroupMember 缺失的昵称
+metadata:
+  category: 数据维护
+  frequency: 按需
+---
+
+# /backfill-nicknames — GroupMember 昵称回填技能
+
+## 技能说明
+
+GroupMember 表中大量成员的 nickname 为空。此脚本从 Message 表的 senderName 跨房间搜索匹配,回填缺失的昵称。
+
+**原理**: 成员 A 在群 X 中没有昵称,但在群 Y 中发过消息带有 senderName → 跨房间匹配回填。
+
+## 前置条件
+
+- 环境变量: `QIWE_GUID`
+- Parse Server 可正常连接
+- Message 表中存在该用户的消息记录
+
+## 执行
+
+```bash
+cd backend && npx tsx scripts/backfill-member-nicknames.ts
+```
+
+## 预期结果
+
+- 去重后缺失 userId → 分批 containedIn 查询 Message → 构建 userId→senderName 映射 → 批量回填
+- 当前状态: ~1715 个无昵称 userId,约 27 个能从 Message 中找到 (~1.6%),回填 ~120 条 GroupMember 记录(同一用户在多个群)
+- 剩余缺失需要依赖 QiWe API `batchGetRoomDetail` 接口补全
+
+## 数据表依赖
+
+| 表 | 用途 |
+|---|------|
+| GroupMember | 查询缺失 nickname 的成员 |
+| Message | 按 senderId 查找 senderName |

+ 71 - 0
CLAUDE.md

@@ -0,0 +1,71 @@
+# 拉迷家居 — 企微群聊风控系统
+
+## 项目概述
+
+拉迷家居(全屋定制品牌)企微运营风控管理系统。通过 QiWe 开放平台 API 同步群聊消息,使用「关键词匹配 + DeepSeek AI」双重机制进行合规风险检测。
+
+## 技术栈
+
+- **前端**: Angular (src/app/)
+- **后端**: Express + Parse Server (backend/src/)
+- **数据库**: PostgreSQL (via Parse Server)
+- **AI**: DeepSeek API (deepseek-chat)
+- **外部 API**: QiWe 开放平台 (https://manager.qiweapi.com/qiwe)
+
+## 关键路径
+
+| 模块 | 路径 |
+|------|------|
+| Webhook 入口 | `backend/src/controllers/webhook.controller.ts` |
+| 消息分析引擎 | `backend/src/services/message-analyzer.service.ts` |
+| 关键词匹配 | `backend/src/services/compliance-check.service.ts` |
+| AI 分析 | `backend/src/services/ai-analysis.service.ts` |
+| 风险事件 | `backend/src/services/risk-event.service.ts` |
+| 前端风控大屏 | `src/app/features/risk-control/` |
+| 脚本目录 | `backend/scripts/` |
+| HTML 报告 | `backend/scripts/audit-risk-*.html` |
+| 技能文档 | `backend/scripts/audit-risk-README.md` |
+
+## 可用技能 (Slash Commands)
+
+### `/audit-risk` — 风险预警覆盖审计
+对指定时间段的群聊消息进行关键词覆盖率审计和漏报检测,生成 HTML/MD 报告和管理建议。
+- 技能文件: `.claude/commands/audit-risk.md`
+- 核心脚本: `backend/scripts/audit-risk-coverage.ts` + `audit-risk-deep-analysis.ts`
+- 建议频率: 每周
+
+### `/backfill-nicknames` — GroupMember 昵称回填
+从 Message 表 senderName 跨房间回填缺失的昵称。
+- 技能文件: `.claude/commands/backfill-nicknames.md`
+- 核心脚本: `backend/scripts/backfill-member-nicknames.ts`
+
+## 数据库核心表
+
+| 表 | 用途 |
+|---|------|
+| Message | 群聊消息(content, senderId, senderName, roomId, timestamp) |
+| GroupChat | 群信息(roomName, communityName, ownerPhone) |
+| GroupMember | 群成员(userId, nickname, status) |
+| Contact | 内外部联系人(userId, nickname, phone, corpName) |
+| RiskEvent | 风险事件(keywords, severity, status, escalationLevel) |
+| RiskKeyword | 风险关键词库(word, category, severity, enabled) |
+| WebhookLog | Webhook 事件日志(cmd, msgType, msgData) |
+
+## 常用命令
+
+```bash
+# 启动后端开发服务
+cd backend && npm run dev
+
+# 运行风险审计
+cd backend && npx tsx scripts/audit-risk-coverage.ts
+
+# 运行深度分析
+cd backend && npx tsx scripts/audit-risk-deep-analysis.ts
+
+# 昵称回填
+cd backend && npx tsx scripts/backfill-member-nicknames.ts
+
+# 启动前端
+npm start
+```

+ 1 - 1
backend/.env

@@ -11,6 +11,6 @@ QIWE_GUID=B8591A9D-B540-44F9-9D74-B2EF5B419EAA
 QIWE_USER_ID=1688857912458394
 
 # DeepSeek AI 配置(合规检测)
-DEEPSEEK_API_KEY=sk-cb4e988b869e4c2392e061cfec2daf9c
+DEEPSEEK_API_KEY=sk-7250701ac18f49c5911e04454f8ac03c
 DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
 DEEPSEEK_MODEL=deepseek-chat

+ 12 - 0
backend/scripts/audit-risk-kit/.env.example

@@ -0,0 +1,12 @@
+# Parse Server 配置
+PARSE_APP_ID=lami-ai
+PARSE_SERVER_URL=https://server.sh-lami.com/parse
+PARSE_MASTER_KEY=5s1gfOasPqx9JKsA
+
+# 企业微信 GUID(用于数据隔离)
+QIWE_GUID=B8591A9D-B540-44F9-9D74-B2EF5B419EAA
+
+# DeepSeek AI(可选,不配则跳过 AI 评审)
+DEEPSEEK_API_KEY=sk-7250701ac18f49c5911e04454f8ac03c
+DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
+DEEPSEEK_MODEL=deepseek-chat

+ 206 - 0
backend/scripts/audit-risk-kit/README.md

@@ -0,0 +1,206 @@
+# 🛡️ 拉迷家居 · 风险预警审计工具包
+
+> 独立可分发 | 开箱即用 | 适用于 Parse Server + QiWe 企微群聊风控系统
+
+## 快速开始
+
+```bash
+# 1. 安装依赖
+npm install parse dotenv
+
+# 2. 复制配置模板 → 编辑环境变量
+cp .env.example .env
+
+# 3. 运行
+npx tsx audit-coverage.ts        # 覆盖审计 → HTML 报告
+npx tsx audit-deep-analysis.ts   # 深度分析 → MD 报告
+```
+
+---
+
+## 文件说明
+
+| 文件 | 作用 |
+|------|------|
+| `audit-coverage.ts` | **覆盖审计** — 扫全量消息,检测关键词命中但未预警的「漏报」,AI 逐条评审 |
+| `audit-deep-analysis.ts` | **深度分析** — 多维度统计 + 类别有效性排名 + AI 管理建议 |
+| `parse-client.ts` | Parse Server 连接初始化(读 `.env` 中的 `PARSE_*` 变量) |
+| `.env.example` | 环境变量模板 |
+| `README.md` | 本文档 |
+
+### 输出产物
+
+| 脚本 | 输出 | 说明 |
+|------|------|------|
+| `audit-coverage.ts` | `audit-risk-YYYY-MM-DD.html` | 可视化 HTML,浏览器打开 |
+| `audit-deep-analysis.ts` | `audit-deep-YYYY-MM-DD.md` | Markdown 管理报告 |
+
+---
+
+## 环境变量
+
+```bash
+# ── Parse Server 连接(必填) ──
+PARSE_APP_ID=lami-ai
+PARSE_SERVER_URL=https://server.sh-lami.com/parse
+PARSE_MASTER_KEY=your-master-key
+
+# ── 企业微信 GUID(必填,用于数据隔离) ──
+QIWE_GUID=B8591A9D-B540-44F9-9D74-B2EF5B419EAA
+
+# ── DeepSeek AI(可选,不配则跳过 AI 评审) ──
+DEEPSEEK_API_KEY=sk-xxxx
+DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
+DEEPSEEK_MODEL=deepseek-chat
+
+# ── 审计时间范围(可选,默认最近 7 天) ──
+AUDIT_START=2026-06-15T00:00:00+08:00
+AUDIT_END=2026-06-22T23:59:59+08:00
+```
+
+---
+
+## 功能对比
+
+| 维度 | audit-coverage.ts | audit-deep-analysis.ts |
+|------|-------------------|------------------------|
+| 关键词覆盖率 | ✅ | ✅ |
+| 漏报群检测 | ✅ + AI 逐条评审 | ✅ + 三分类归因 |
+| 可视化报告 | ✅ HTML(浏览器) | ✅ MD(可打印) |
+| 消息分布统计 | ❌ | ✅ 按天/群/类型 |
+| 类别有效性排名 | ❌ | ✅ |
+| AI 管理建议 | ❌(仅评审) | ✅ 5 维度建议 |
+| 行动项清单 | ❌ | ✅ 分级行动项 |
+| 建议频率 | **每周** | **每月** |
+| 耗时 | ~30s | ~10s |
+
+---
+
+## 工作流示意
+
+```
+┌─────────────────────────────────────────────────────────┐
+│                    每周: audit-coverage.ts               │
+│                                                         │
+│  Message ──→ 关键词匹配 ──→ 命中? ──→ 已有预警?         │
+│     │            │              │           │            │
+│     │            │            NO          YES → 跳过     │
+│     │            │              │                        │
+│     │            │         【漏报】──→ AI 评审           │
+│     │            │              │           │            │
+│     └────────────┴──────────────┴───────────┘            │
+│                         ↓                                │
+│                  audit-risk-*.html                        │
+└─────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────────────────────────┐
+│                 每月: audit-deep-analysis.ts              │
+│                                                         │
+│  基础统计 + 类别有效性 + 漏报归因 + AI管理建议            │
+│                         ↓                                │
+│                  audit-deep-*.md                          │
+│                         ↓                                │
+│              优化关键词库 / 调整规则                      │
+└─────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 依赖的数据表
+
+本工具假设目标 Parse Server 中存在以下表:
+
+| 表 | 关键字段 | 用途 |
+|---|---------|------|
+| `Message` | content, senderId, senderName, roomId, msgType, timestamp | 消息内容 |
+| `RiskEvent` | groupId, keywords, severity, title, status, createdAt | 已触发预警 |
+| `RiskKeyword` | word, category, severity, enabled | 关键词库 |
+| `GroupChat` | roomId, roomName, communityName | 群映射 |
+
+---
+
+## 核心指标解释
+
+### 关键词覆盖率
+```
+触发过预警的关键词数 / 启用关键词总数
+```
+- < 3% → 🔴 严重冗余,需大规模清理
+- 3~8% → 🟡 中等,需分类审核
+- > 10% → 🟢 健康
+
+### 类别有效率
+```
+该类命中消息的关键词数 / 该类总词数
+```
+- 0% 的类别 → 所有关键词均未启用或过于极端,需重新审核
+
+### 漏报分类
+- **促销营销**: 618/双11活动文案 → 误匹配,不需预警
+- **正常业务**: 正常设计/安装沟通 → 误匹配,不需预警
+- **真实风险**: 质量投诉/纠纷 → 真漏报,需立即处理
+
+---
+
+## 典型使用场景
+
+### 场景 1: 每周风控例会前
+```bash
+# 设置上周的日期
+export AUDIT_START="2026-06-15T00:00:00+08:00"
+export AUDIT_END="2026-06-22T23:59:59+08:00"
+
+npx tsx audit-coverage.ts
+# 打开 audit-risk-2026-06-15.html 投屏展示
+```
+
+### 场景 2: 月度关键词大扫除
+```bash
+export AUDIT_START="2026-06-01T00:00:00+08:00"
+export AUDIT_END="2026-06-30T23:59:59+08:00"
+
+npx tsx audit-deep-analysis.ts
+# 根据报告中的"类别有效性"和"未覆盖关键词"
+# 淘汰 30 天零命中的词,升级质量类词权重
+```
+
+### 场景 3: 618/双11 专项审计
+```bash
+# 大促前后两周的独立审计
+export AUDIT_START="2026-06-01T00:00:00+08:00"
+export AUDIT_END="2026-06-22T23:59:59+08:00"
+npx tsx audit-coverage.ts
+# 检查促销用词是否导致大量误报
+```
+
+---
+
+## 部署到其他项目
+
+1. 复制整个 `audit-risk-kit/` 文件夹到目标机器
+2. `npm install parse dotenv`
+3. `cp .env.example .env` → 编辑 `.env` 填入目标 Parse Server 连接信息
+4. 确保目标 Parse Server 中存在上述 4 张表(Message / RiskEvent / RiskKeyword / GroupChat)
+5. 运行脚本
+
+> 注意: 表名和字段名必须一致。如果目标项目使用不同的表名或字段名,需要修改脚本中的 `new Parse.Query('...')` 和 `.get('...')` 调用。
+
+---
+
+## 常见问题
+
+**Q: 脚本跑很慢?**
+A: AI 评审阶段每个漏报群调一次 DeepSeek,限制最多 20 次。可去掉 `DEEPSEEK_API_KEY` 配置跳过 AI 评审,脚本秒出结果。
+
+**Q: 漏报数量为什么感觉偏多?**
+A: 当前漏报基于"关键词命中但未触发预警"。大部分是促销营销词命中,并非真实风险。看 AI 评审列来区分真假漏报。
+
+**Q: 如何新增/删除关键词?**
+A: 通过 Parse Dashboard 操作 RiskKeyword 表,系统每 5 分钟自动刷新关键词库。
+
+**Q: 支持非拉迷项目的企微风控吗?**
+A: 工具与业务解耦,只要是同样的表结构(Message / RiskKeyword / RiskEvent / GroupChat),直接配置连接即可使用。
+
+---
+
+*拉迷家居 · 风控团队 · v1.0*

+ 632 - 0
backend/scripts/audit-risk-kit/audit-coverage.ts

@@ -0,0 +1,632 @@
+/**
+ * 风险预警覆盖审计脚本
+ *
+ * 用法: cp .env.example .env → 编辑 .env → npx tsx audit-coverage.ts
+ * 输出: audit-risk-YYYY-MM-DD.html(浏览器打开)
+ *
+ * 分析内容:
+ *   1. 查询指定时间段内所有文本消息
+ *   2. 查询所有 RiskEvent(同期已有预警)
+ *   3. 查询所有 RiskKeyword(启用中的关键词)
+ *   4. 关键词匹配命中但无预警的消息 → 漏报
+ *   5. AI 评审漏报消息的风险等级
+ *   6. 可视化 HTML 报告
+ */
+
+import 'dotenv/config';
+import Parse from './parse-client.js';
+import * as fs from 'fs';
+import * as path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/* ======================== 配置 ======================== */
+
+const GUID = process.env.QIWE_GUID || '';
+
+const AI_KEY = process.env.DEEPSEEK_API_KEY || '';
+const AI_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';
+const AI_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';
+const AI_ENABLED = AI_KEY && AI_KEY !== 'sk-xxx';
+
+// 默认审计最近 7 天,可通过环境变量覆盖
+const DATE_RANGE = {
+  start: new Date(process.env.AUDIT_START || (() => { const d = new Date(); d.setDate(d.getDate() - 8); d.setHours(0, 0, 0, 0); return d.toISOString(); })()),
+  end: new Date(process.env.AUDIT_END || (() => { const d = new Date(); d.setHours(23, 59, 59, 999); return d.toISOString(); })()),
+};
+
+/* ======================== 类型 ======================== */
+
+interface KeywordInfo {
+  word: string;
+  category: string;
+  severity: 'high' | 'medium' | 'low';
+}
+
+interface MsgInfo {
+  id: string;
+  roomId: string;
+  senderName: string;
+  content: string;
+  timestamp: Date;
+}
+
+interface RiskEventInfo {
+  id: string;
+  groupId: string;
+  keywords: string[];
+  severity: string;
+  title: string;
+  description: string;
+  createdAt: Date;
+}
+
+interface MissedAlert {
+  roomId: string;
+  roomName: string;
+  messages: MsgInfo[];
+  matchedKeywords: string[];
+  maxSeverity: string;
+  reason: string;
+}
+
+/* ======================== 数据查询 ======================== */
+
+async function fetchAllData() {
+  console.log('[审计] 查询数据...\n');
+
+  const [allMessages, riskEvents, keywords, allGroups] = await Promise.all([
+    (async () => {
+      const q = new Parse.Query('Message');
+      q.greaterThanOrEqualTo('timestamp', DATE_RANGE.start);
+      q.lessThanOrEqualTo('timestamp', DATE_RANGE.end);
+      q.containedIn('msgType', [0, 2, 13, 29]);
+      q.notEqualTo('content', '');
+      q.select(['roomId', 'senderName', 'content', 'timestamp', 'msgType']);
+      q.limit(200000);
+      q.ascending('timestamp');
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map(r => ({
+        id: r.id,
+        roomId: (r.get('roomId') as string) || '',
+        senderName: (r.get('senderName') as string) || '',
+        content: (r.get('content') as string) || '',
+        timestamp: r.get('timestamp') as Date,
+      })) as MsgInfo[];
+    })(),
+
+    (async () => {
+      const q = new Parse.Query('RiskEvent');
+      q.greaterThanOrEqualTo('createdAt', DATE_RANGE.start);
+      q.lessThanOrEqualTo('createdAt', DATE_RANGE.end);
+      q.select(['groupId', 'keywords', 'severity', 'title', 'description', 'createdAt']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map(r => ({
+        id: r.id,
+        groupId: (r.get('groupId') as string) || '',
+        keywords: (r.get('keywords') as string[]) || [],
+        severity: (r.get('severity') as string) || '',
+        title: (r.get('title') as string) || '',
+        description: (r.get('description') as string) || '',
+        createdAt: r.get('createdAt') as Date,
+      })) as RiskEventInfo[];
+    })(),
+
+    (async () => {
+      const q = new Parse.Query('RiskKeyword');
+      q.equalTo('enabled', true);
+      q.select(['word', 'category', 'severity']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map((r: any) => ({
+        word: r.get('word') as string,
+        category: r.get('category') as string,
+        severity: r.get('severity') as string,
+      })) as KeywordInfo[];
+    })(),
+
+    (async () => {
+      const q = new Parse.Query('GroupChat');
+      q.select(['roomId', 'roomName']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      const map = new Map<string, string>();
+      for (const r of rows) {
+        map.set((r.get('roomId') as string) || '', (r.get('roomName') as string) || '未知群');
+      }
+      return map;
+    })(),
+  ]);
+
+  console.log(`  Message: ${allMessages.length} | RiskEvent: ${riskEvents.length} | RiskKeyword: ${keywords.length} | GroupChat: ${allGroups.size}`);
+  return { allMessages, riskEvents, keywords, allGroups };
+}
+
+/* ======================== 覆盖分析 ======================== */
+
+function analyzeCoverage(
+  allMessages: MsgInfo[],
+  riskEvents: RiskEventInfo[],
+  keywords: KeywordInfo[],
+  groupMap: Map<string, string>,
+) {
+  const msgsByRoom = new Map<string, MsgInfo[]>();
+  for (const m of allMessages) {
+    if (!m.roomId) continue;
+    const arr = msgsByRoom.get(m.roomId);
+    if (arr) arr.push(m);
+    else msgsByRoom.set(m.roomId, [m]);
+  }
+
+  const alertedRoomIds = new Set(riskEvents.map(e => e.groupId));
+  const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords));
+
+  const missedAlerts: MissedAlert[] = [];
+
+  for (const [roomId, msgs] of msgsByRoom) {
+    const hitMap = new Map<string, { msgs: MsgInfo[]; kw: KeywordInfo }>();
+    for (const msg of msgs) {
+      for (const kw of keywords) {
+        if (msg.content.includes(kw.word)) {
+          if (hitMap.has(kw.word)) {
+            hitMap.get(kw.word)!.msgs.push(msg);
+          } else {
+            hitMap.set(kw.word, { msgs: [msg], kw });
+          }
+        }
+      }
+    }
+    if (hitMap.size === 0) continue;
+
+    const hitWords = [...hitMap.keys()];
+    const missedWords = hitWords.filter(w => !alertedKeywords.has(w));
+    const alertedWords = hitWords.filter(w => alertedKeywords.has(w));
+
+    if (missedWords.length > 0) {
+      const allMissedMsgs: MsgInfo[] = [];
+      for (const w of missedWords) {
+        const entry = hitMap.get(w);
+        if (entry) allMissedMsgs.push(...entry.msgs);
+      }
+      const uniqueMsgs = [...new Map(allMissedMsgs.map(m => [m.id, m])).values()]
+        .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
+
+      const severities = missedWords.map(w => hitMap.get(w)?.kw.severity || 'low');
+      const maxSeverity = severities.includes('high') ? 'high'
+        : severities.includes('medium') ? 'medium' : 'low';
+
+      const hasAlert = alertedRoomIds.has(roomId);
+      let reason: string;
+      if (!hasAlert && alertedWords.length === 0) {
+        reason = '该群无任何预警记录,所有关键词命中均未触发预警';
+      } else if (!hasAlert) {
+        reason = `该群无预警记录;命中关键词: [${hitWords.join(', ')}],其中 [${alertedWords.join(', ')}] 在其他群触发过预警`;
+      } else {
+        reason = `该群有预警但未覆盖以下关键词: [${missedWords.join(', ')}]`;
+      }
+
+      missedAlerts.push({
+        roomId,
+        roomName: groupMap.get(roomId) || `未知群(${roomId})`,
+        messages: uniqueMsgs.slice(0, 30),
+        matchedKeywords: missedWords,
+        maxSeverity,
+        reason,
+      });
+    }
+  }
+
+  const sevOrder = { high: 0, medium: 1, low: 2 };
+  missedAlerts.sort((a, b) => sevOrder[a.maxSeverity as keyof typeof sevOrder] - sevOrder[b.maxSeverity as keyof typeof sevOrder]);
+
+  const trulyMissedKeywords = keywords.filter(kw => !alertedKeywords.has(kw.word));
+
+  return {
+    missedAlerts,
+    trulyMissedKeywords,
+    stats: {
+      totalMessages: allMessages.length,
+      totalRiskEvents: riskEvents.length,
+      totalKeywords: keywords.length,
+      triggeredKeywords: alertedKeywords.size,
+      untriggeredKeywords: keywords.length - alertedKeywords.size,
+      coverageRate: keywords.length > 0 ? ((alertedKeywords.size / keywords.length) * 100).toFixed(1) : '0',
+      missedGroups: missedAlerts.length,
+      highCount: missedAlerts.filter(a => a.maxSeverity === 'high').length,
+      mediumCount: missedAlerts.filter(a => a.maxSeverity === 'medium').length,
+      lowCount: missedAlerts.filter(a => a.maxSeverity === 'low').length,
+      trulyMissedCount: trulyMissedKeywords.length,
+    },
+  };
+}
+
+/* ======================== AI 评审 ======================== */
+
+async function aiReviewMissedAlert(alert: MissedAlert): Promise<string> {
+  if (!AI_ENABLED) return 'AI 未启用';
+
+  const samples = alert.messages.slice(0, 15).map(m => {
+    const t = m.timestamp;
+    const time = t ? `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}` : '';
+    return `[${time}] ${m.senderName || '未知'}: ${m.content}`;
+  }).join('\n');
+
+  const prompt = `你是拉迷家居(全屋定制)的风险审核员。以下群聊消息命中了风险关键词 [${alert.matchedKeywords.join(', ')}],但系统未触发预警。
+
+请判断:1.这些消息是否有真实风险?2.风险等级?3.简要理由(≤80字)
+
+群: ${alert.roomName}
+命中词: ${alert.matchedKeywords.join(', ')}
+消息样本:
+${samples}
+
+回复 JSON: {"hasRisk":true/false,"severity":"high"/"medium"/"low"/"none","reason":"≤80字"}`;
+
+  try {
+    const res = await fetch(AI_URL, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${AI_KEY}` },
+      body: JSON.stringify({
+        model: AI_MODEL,
+        messages: [
+          { role: 'system', content: '你是全屋定制行业风险审核员。只回复 JSON,不含 markdown。' },
+          { role: 'user', content: prompt },
+        ],
+        temperature: 0.2,
+        max_tokens: 256,
+      }),
+    });
+    if (!res.ok) return `AI 调用失败: ${res.status}`;
+    const json = await res.json() as any;
+    const content = json?.choices?.[0]?.message?.content || '';
+    const match = content.match(/\{[\s\S]*\}/);
+    if (match) {
+      const p = JSON.parse(match[0]);
+      const label = p.hasRisk ? `⚠ 有风险(${p.severity})` : '✅ 无风险';
+      return `${label} | ${p.reason || ''}`;
+    }
+    return `AI 响应异常: ${content.slice(0, 100)}`;
+  } catch (err: any) {
+    return `AI 错误: ${err.message}`;
+  }
+}
+
+/* ======================== HTML 报告生成 ======================== */
+
+function escapeHtml(s: string): string {
+  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+}
+
+function sevBadge(s: string): string {
+  const m: Record<string, string> = { high: '高危', medium: '中危', low: '低危' };
+  return `<span class="badge badge-${s}">${m[s] || s}</span>`;
+}
+
+function sevIcon(s: string): string {
+  return s === 'high' ? '🔴' : s === 'medium' ? '🟡' : '🟢';
+}
+
+function categoryCn(c: string): string {
+  const m: Record<string, string> = {
+    complaint: '投诉', quality: '质量', sensitive: '敏感', legal: '法律',
+    competitor: '竞品', price: '价格', negative: '负面', installation: '安装',
+  };
+  return m[c] || c;
+}
+
+function generateHTML(
+  stats: ReturnType<typeof analyzeCoverage>['stats'],
+  missedAlerts: MissedAlert[],
+  aiReviews: string[],
+  riskEvents: RiskEventInfo[],
+  trulyMissedKeywords: KeywordInfo[],
+  queryMs: number,
+  totalMs: number,
+): string {
+  const genTime = new Date().toLocaleString('zh-CN');
+  const dateLabel = `${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`;
+
+  // 概览卡片
+  const cards = `
+    <div class="card card-blue"><div class="card-num">${stats.totalMessages.toLocaleString()}</div><div class="card-label">文本消息总量</div></div>
+    <div class="card card-purple"><div class="card-num">${stats.totalRiskEvents}</div><div class="card-label">已有风险事件</div></div>
+    <div class="card card-teal"><div class="card-num">${stats.totalKeywords}</div><div class="card-label">启用关键词</div></div>
+    <div class="card card-orange"><div class="card-num">${stats.coverageRate}%</div><div class="card-label">关键词覆盖率</div></div>
+    <div class="card card-red"><div class="card-num">${stats.missedGroups}</div><div class="card-label">漏报群数量</div></div>
+    <div class="card card-green"><div class="card-num">${stats.trulyMissedCount}</div><div class="card-label">未覆盖关键词</div></div>
+  `;
+
+  // 已触发预警
+  let alertSection = '';
+  if (riskEvents.length > 0) {
+    const rows = riskEvents.map(e => `
+      <tr>
+        <td><span class="badge badge-${e.severity}">${e.severity}</span></td>
+        <td>${escapeHtml(e.title)}</td>
+        <td>${(e.keywords || []).map(k => `<code>${escapeHtml(k)}</code>`).join(' ')}</td>
+        <td>${escapeHtml(e.description?.slice(0, 60) || '')}</td>
+        <td class="time-cell">${e.createdAt ? new Date(e.createdAt).toLocaleString('zh-CN') : ''}</td>
+      </tr>`).join('');
+    alertSection = `
+      <div class="section"><h2>🛡️ 已触发预警事件 <span class="sub">(${riskEvents.length} 条)</span></h2>
+        <div class="table-wrap"><table>
+          <thead><tr><th>严重度</th><th>标题</th><th>关键词</th><th>描述</th><th>时间</th></tr></thead>
+          <tbody>${rows}</tbody>
+        </table></div></div>`;
+  } else {
+    alertSection = `<div class="section"><h2>🛡️ 已触发预警事件</h2><div class="empty">该期间无风险事件</div></div>`;
+  }
+
+  // 漏报详情
+  let missedSection = '';
+  if (missedAlerts.length === 0) {
+    missedSection = `<div class="section"><h2>🔍 漏报检查</h2><div class="empty success">✅ 未发现漏报</div></div>`;
+  } else {
+    const realRisky = aiReviews.filter(r => r.includes('有风险')).length;
+    const safeCount = aiReviews.filter(r => r.includes('无风险')).length;
+
+    let cardsHtml = '';
+    for (let i = 0; i < missedAlerts.length; i++) {
+      const a = missedAlerts[i];
+      const review = aiReviews[i] || '';
+      const isRisky = review.includes('有风险');
+      const border = a.maxSeverity === 'high' ? '#e74c3c' : a.maxSeverity === 'medium' ? '#f39c12' : '#3498db';
+      const bg = a.maxSeverity === 'high' ? '#fff5f5' : a.maxSeverity === 'medium' ? '#fffdf0' : '#f8faff';
+
+      const sampleRows = a.messages.slice(0, 5).map(m => {
+        const t = m.timestamp;
+        const time = t ? `${t.getMonth() + 1}/${t.getDate()} ${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}` : '';
+        return `<tr><td class="time-cell">${time}</td><td>${escapeHtml(m.senderName || '未知')}</td><td class="msg-cell">${escapeHtml(m.content.length > 120 ? m.content.slice(0, 120) + '...' : m.content)}</td></tr>`;
+      }).join('');
+
+      const kwTags = a.matchedKeywords.map(k => `<span class="kw-tag">${sevIcon(a.maxSeverity)} ${escapeHtml(k)}</span>`).join('');
+
+      cardsHtml += `
+      <div class="missed-card" style="border-left:4px solid ${border};background:${bg}">
+        <div class="missed-header">
+          <span class="missed-title">${i + 1}. ${escapeHtml(a.roomName)}</span>
+          <span>${sevBadge(a.maxSeverity)}</span>
+        </div>
+        <div class="missed-body">
+          <div class="missed-meta">
+            <div class="meta-item"><span class="meta-label">命中关键词</span>${kwTags}</div>
+            <div class="meta-item"><span class="meta-label">匹配消息</span>${a.messages.length} 条</div>
+            <div class="meta-item"><span class="meta-label">原因</span>${escapeHtml(a.reason)}</div>
+          </div>
+          ${review ? `<div class="ai-review ${isRisky ? 'ai-risky' : 'ai-safe'}">🤖 AI 评审:${escapeHtml(review)}</div>` : ''}
+          <details class="msg-details">
+            <summary>📋 消息样本(点击展开)</summary>
+            <div class="table-wrap mini-table"><table>
+              <thead><tr><th>时间</th><th>发送者</th><th>内容</th></tr></thead>
+              <tbody>${sampleRows}</tbody>
+            </table></div>
+          </details>
+        </div>
+      </div>`;
+    }
+
+    missedSection = `
+      <div class="section"><h2>🔍 漏报详情 <span class="sub">(${missedAlerts.length} 个群)</span></h2>
+        <div class="missed-summary">
+          <span class="chip chip-red">⚠️ AI 确认有风险: ${realRisky} 个</span>
+          <span class="chip chip-green">✅ AI 确认无风险: ${safeCount} 个</span>
+          <span class="chip chip-gray">未评审: ${missedAlerts.length - realRisky - safeCount} 个</span>
+        </div>
+        <div class="missed-list">${cardsHtml}</div>
+      </div>`;
+  }
+
+  // 未覆盖关键词云
+  let kwSection = '';
+  if (trulyMissedKeywords.length > 0) {
+    const sevOrder = { high: 0, medium: 1, low: 2 };
+    const sorted = [...trulyMissedKeywords].sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
+    const tags = sorted.map(k => `<span class="kw-chip kw-${k.severity}">${k.word}<span class="kw-cat">${categoryCn(k.category)}</span></span>`).join('\n');
+    const hi = sorted.filter(k => k.severity === 'high').length;
+    const md = sorted.filter(k => k.severity === 'medium').length;
+    const lo = sorted.filter(k => k.severity === 'low').length;
+
+    kwSection = `
+      <div class="section"><h2>📚 从未触发预警的关键词 <span class="sub">(${trulyMissedKeywords.length} 个)</span></h2>
+        <div class="kw-stats-bar">
+          <div class="kw-stat kw-stat-high"><span>${hi}</span> 高危</div>
+          <div class="kw-stat kw-stat-med"><span>${md}</span> 中危</div>
+          <div class="kw-stat kw-stat-low"><span>${lo}</span> 低危</div>
+        </div>
+        <div class="kw-cloud">${tags}</div>
+        <p class="hint">这些关键词在审计期间被消息命中,但从未在任何群触发过预警。</p>
+      </div>`;
+  }
+
+  // 管理建议
+  const suggestions = `
+    <div class="section"><h2>💡 管理建议</h2>
+      <div class="suggestion-grid">
+        <div class="sugg-card sugg-high">
+          <div class="sugg-title">🔴 高优先级</div>
+          <ul>
+            <li>将「定金/订金/优惠」等促销词从预警列表移除或降级为"仅记录"</li>
+            <li>质量词(脱胶/开裂/变形/发霉/装错/色差)提高权重,命中即报</li>
+          </ul>
+        </div>
+        <div class="sugg-card sugg-med">
+          <div class="sugg-title">🟡 中优先级</div>
+          <ul>
+            <li>精简关键词库:181 → 60~80 个高质量词</li>
+            <li>同一群短时间内命中多个低价关键词 → 聚合升级预警</li>
+          </ul>
+        </div>
+        <div class="sugg-card sugg-low">
+          <div class="sugg-title">🟢 持续改进</div>
+          <ul>
+            <li>每周运行本审计脚本,跟踪漏报趋势</li>
+            <li>建立"关键词→预警→确认→反馈"闭环</li>
+            <li>竞品类关键词命中频次统计,作为市场情报</li>
+          </ul>
+        </div>
+      </div>
+    </div>`;
+
+  return `<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
+<title>拉迷风险预警覆盖审计 — ${dateLabel}</title>
+<style>
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif;background:#f0f2f5;color:#1a1a2e;line-height:1.6}
+.hero{background:linear-gradient(135deg,#0f0c29 0%,#1a1a2e 40%,#16213e 100%);color:#fff;padding:40px 48px;position:relative;overflow:hidden}
+.hero::after{content:'';position:absolute;top:-50%;right:-10%;width:400px;height:400px;background:radial-gradient(circle,rgba(231,76,60,.15),transparent 70%);border-radius:50%}
+.hero h1{font-size:26px;font-weight:700;position:relative;z-index:1}
+.hero .subtitle{font-size:14px;color:#8892b0;margin-top:6px;position:relative;z-index:1}
+.hero .meta{font-size:12px;color:#5a6785;margin-top:4px;position:relative;z-index:1}
+.container{max-width:1100px;margin:0 auto;padding:28px 24px}
+.overview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px;margin-bottom:36px}
+.card{background:#fff;border-radius:12px;padding:24px 20px;text-align:center;box-shadow:0 2px 12px rgba(0,0,0,.06);transition:transform .15s}
+.card:hover{transform:translateY(-2px)}
+.card-num{font-size:36px;font-weight:800;line-height:1.2}
+.card-label{font-size:13px;color:#7f8c8d;margin-top:6px}
+.card-blue .card-num{color:#2980b9} .card-purple .card-num{color:#8e44ad}
+.card-teal .card-num{color:#16a085} .card-orange .card-num{color:#e67e22}
+.card-red .card-num{color:#e74c3c} .card-green .card-num{color:#27ae60}
+.section{margin-bottom:36px}
+.section h2{font-size:18px;font-weight:700;color:#1a1a2e;margin-bottom:16px;padding-bottom:10px;border-bottom:2px solid #e8ecf4}
+.sub{font-size:13px;color:#95a5a6;font-weight:400}
+.empty{padding:40px;text-align:center;color:#95a5a6;background:#fff;border-radius:10px}
+.empty.success{color:#27ae60;background:#eafaf1}
+.hint{font-size:13px;color:#95a5a6;margin-top:12px;line-height:1.6}
+.badge{font-size:11px;font-weight:700;padding:3px 10px;border-radius:12px;color:#fff;white-space:nowrap}
+.badge-high{background:#e74c3c} .badge-medium{background:#f39c12} .badge-low{background:#3498db}
+.table-wrap{overflow-x:auto;background:#fff;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
+table{width:100%;border-collapse:collapse;font-size:13px}
+thead th{background:#f8f9fb;padding:12px 14px;text-align:left;font-weight:600;color:#555;font-size:12px;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid #e8ecf4}
+tbody td{padding:10px 14px;border-bottom:1px solid #f0f2f5;vertical-align:top}
+tbody tr:hover{background:#f8f9fb}
+.time-cell{white-space:nowrap;color:#95a5a6;font-size:12px}
+.msg-cell{max-width:400px;word-break:break-all}
+code{background:#f0f2f5;padding:1px 6px;border-radius:3px;font-size:12px;color:#e74c3c}
+.missed-summary{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:18px}
+.chip{font-size:13px;padding:6px 14px;border-radius:20px;font-weight:600}
+.chip-red{background:#fde8e8;color:#c0392b} .chip-green{background:#e8f8f5;color:#1e8449} .chip-gray{background:#f0f2f5;color:#7f8c8d}
+.missed-list{display:flex;flex-direction:column;gap:14px}
+.missed-card{border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.04)}
+.missed-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:rgba(0,0,0,.02);border-bottom:1px solid rgba(0,0,0,.05)}
+.missed-title{font-size:15px;font-weight:700}
+.missed-body{padding:16px 18px}
+.missed-meta{display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
+.meta-item{font-size:13px} .meta-label{font-weight:600;color:#555;margin-right:8px}
+.kw-tag{display:inline-block;background:#f0f2f5;padding:2px 10px;border-radius:12px;font-size:12px;margin-right:4px;margin-bottom:2px}
+.ai-review{font-size:13px;padding:10px 14px;border-radius:8px;margin:10px 0}
+.ai-risky{background:#fde8e8;border:1px solid #f5c6cb;color:#721c24}
+.ai-safe{background:#e8f8f5;border:1px solid #b7e4cf;color:#155724}
+.msg-details{margin-top:8px} .msg-details summary{font-size:13px;color:#3498db;cursor:pointer;font-weight:600}
+.kw-stats-bar{display:flex;gap:12px;margin-bottom:14px}
+.kw-stat{padding:6px 16px;border-radius:8px;font-size:13px;font-weight:600}
+.kw-stat span{font-size:20px;font-weight:800}
+.kw-stat-high{background:#fde8e8;color:#c0392b} .kw-stat-med{background:#fef5e7;color:#d68910} .kw-stat-low{background:#eaf2f8;color:#2980b9}
+.kw-cloud{display:flex;flex-wrap:wrap;gap:8px;background:#fff;padding:18px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
+.kw-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:16px;font-size:13px;font-weight:600}
+.kw-high{background:#fde8e8;color:#c0392b} .kw-medium{background:#fef5e7;color:#d68910} .kw-low{background:#eaf2f8;color:#2980b9}
+.kw-cat{font-size:10px;opacity:.7;font-weight:400}
+.suggestion-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
+.sugg-card{padding:20px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
+.sugg-title{font-size:15px;font-weight:700;margin-bottom:10px}
+.sugg-card ul{padding-left:18px} .sugg-card li{font-size:13px;margin-bottom:8px;line-height:1.6}
+.sugg-high{background:#fff5f5;border:1px solid #fecaca}
+.sugg-med{background:#fffdf0;border:1px solid #fde68a}
+.sugg-low{background:#f5f9ff;border:1px solid #bfdbfe}
+.footer{text-align:center;padding:20px;color:#95a5a6;font-size:12px}
+@media(max-width:768px){.hero{padding:24px}.container{padding:16px}.overview-grid{grid-template-columns:repeat(3,1fr);gap:10px}.card{padding:16px 10px}.card-num{font-size:26px}.suggestion-grid{grid-template-columns:1fr}}
+@media print{body{background:#fff}.hero{background:#1a1a2e!important;-webkit-print-color-adjust:exact}.missed-card{break-inside:avoid;border:1px solid #ddd}}
+</style>
+</head>
+<body>
+<div class="hero">
+  <h1>🛡️ 风险预警覆盖审计报告</h1>
+  <div class="subtitle">审计期间:${dateLabel}(${Math.ceil((DATE_RANGE.end.getTime() - DATE_RANGE.start.getTime()) / 86400000)} 天)&nbsp;|&nbsp; 全屋定制 · 拉迷家居</div>
+  <div class="meta">生成时间:${genTime} &nbsp;|&nbsp; 查询耗时:${queryMs}ms &nbsp;|&nbsp; 总耗时:${totalMs}ms &nbsp;|&nbsp; AI:${AI_ENABLED ? `DeepSeek(${AI_MODEL})` : '未启用'}</div>
+</div>
+<div class="container">
+  <div class="overview-grid">${cards}</div>
+  ${alertSection}
+  ${missedSection}
+  ${kwSection}
+  ${suggestions}
+  <div class="footer">拉迷家居 · 风险预警覆盖审计 · 自动生成</div>
+</div>
+</body>
+</html>`;
+}
+
+/* ======================== 主流程 ======================== */
+
+async function main(): Promise<void> {
+  const t0 = Date.now();
+  console.log('═══════════════════════════════════════');
+  console.log('  风险预警覆盖审计');
+  console.log(`  时间: ${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`);
+  console.log(`  AI: ${AI_ENABLED ? `DeepSeek (${AI_MODEL})` : '禁用'}`);
+  console.log('═══════════════════════════════════════\n');
+
+  // 1. 查询
+  const t1 = Date.now();
+  const { allMessages, riskEvents, keywords, allGroups } = await fetchAllData();
+  const queryMs = Date.now() - t1;
+  console.log(`  查询耗时: ${queryMs}ms\n`);
+
+  // 2. 覆盖分析
+  console.log('[审计] 关键词覆盖分析...');
+  const { missedAlerts, trulyMissedKeywords, stats } = analyzeCoverage(allMessages, riskEvents, keywords, allGroups);
+  console.log(`  漏报群: ${stats.missedGroups},高危 ${stats.highCount} / 中危 ${stats.mediumCount} / 低危 ${stats.lowCount}`);
+  console.log(`  完全未触发预警的关键词: ${stats.trulyMissedCount}\n`);
+
+  // 3. AI 评审(限前 20 个高危/中危)
+  console.log('[审计] AI 深度评审漏报...');
+  const toReview = missedAlerts.filter(a => a.maxSeverity === 'high' || a.maxSeverity === 'medium').slice(0, 20);
+  const aiReviews: string[] = [];
+  for (let i = 0; i < toReview.length; i++) {
+    const a = toReview[i];
+    console.log(`  → AI 评审 ${i + 1}/${toReview.length}: ${a.roomName} (${a.maxSeverity})`);
+    const review = await aiReviewMissedAlert(a);
+    aiReviews.push(review);
+    console.log(`  ← ${review}`);
+  }
+
+  const allAiReviews = missedAlerts.map(a => {
+    const idx = toReview.indexOf(a);
+    return idx >= 0 ? aiReviews[idx] : '(低危,未提交 AI 评审)';
+  });
+
+  // 4. HTML 报告
+  const totalMs = Date.now() - t0;
+  const html = generateHTML(stats, missedAlerts, allAiReviews, riskEvents, trulyMissedKeywords, queryMs, totalMs);
+
+  const dateStr = DATE_RANGE.start.toISOString().slice(0, 10);
+  const outputPath = path.resolve(__dirname, `audit-risk-${dateStr}.html`);
+  fs.writeFileSync(outputPath, html, 'utf-8');
+
+  console.log(`\n✅ 报告已生成: ${outputPath}`);
+  console.log(`   文件大小: ${(Buffer.byteLength(html, 'utf-8') / 1024).toFixed(1)} KB | 总耗时: ${totalMs}ms\n`);
+
+  // 控制台摘要
+  console.log('═══════════════════════════════════════');
+  console.log('  关键发现');
+  console.log('═══════════════════════════════════════');
+  console.log(`  关键词覆盖率: ${stats.coverageRate}% (${stats.triggeredKeywords}/${stats.totalKeywords})`);
+  console.log(`  完全未覆盖关键词: ${stats.trulyMissedCount}`);
+  console.log(`  漏报群: ${stats.missedGroups} (高危 ${stats.highCount} / 中危 ${stats.mediumCount} / 低危 ${stats.lowCount})`);
+  if (stats.trulyMissedCount > 0) {
+    console.log(`\n  未覆盖关键词 TOP 15:`);
+    for (const kw of trulyMissedKeywords.slice(0, 15)) {
+      console.log(`    - ${kw.word} (${kw.category}/${kw.severity})`);
+    }
+  }
+  console.log('');
+}
+
+main().catch((err) => {
+  console.error('[审计] 失败:', err);
+  process.exit(1);
+});

+ 610 - 0
backend/scripts/audit-risk-kit/audit-deep-analysis.ts

@@ -0,0 +1,610 @@
+/**
+ * 风险预警深度分析脚本
+ *
+ * 用法: cp .env.example .env → 编辑 .env → npx tsx audit-deep-analysis.ts
+ * 输出: audit-deep-YYYY-MM-DD.md(Markdown 格式)
+ *
+ * 分析维度:
+ *   1. 消息按天/按群/按类型分布
+ *   2. 关键词类别有效性排名
+ *   3. 漏报三分类(促销营销/正常业务/真实风险)
+ *   4. 已触发预警回溯
+ *   5. AI 管理建议(5个维度)
+ *   6. 分级行动项
+ */
+
+import 'dotenv/config';
+import Parse from './parse-client.js';
+import * as fs from 'fs';
+import * as path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/* ======================== 配置 ======================== */
+
+const GUID = process.env.QIWE_GUID || '';
+
+const AI_KEY = process.env.DEEPSEEK_API_KEY || '';
+const AI_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';
+const AI_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';
+const AI_ENABLED = AI_KEY && AI_KEY !== 'sk-xxx';
+
+const DATE_RANGE = {
+  start: new Date(process.env.AUDIT_START || (() => { const d = new Date(); d.setDate(d.getDate() - 8); d.setHours(0, 0, 0, 0); return d.toISOString(); })()),
+  end: new Date(process.env.AUDIT_END || (() => { const d = new Date(); d.setHours(23, 59, 59, 999); return d.toISOString(); })()),
+};
+
+/* ======================== 类型 ======================== */
+
+interface KeywordInfo {
+  word: string;
+  category: string;
+  severity: 'high' | 'medium' | 'low';
+}
+
+interface MsgInfo {
+  id: string;
+  roomId: string;
+  senderName: string;
+  content: string;
+  timestamp: Date;
+  msgType: number;
+}
+
+interface RiskEventInfo {
+  id: string;
+  groupId: string;
+  keywords: string[];
+  severity: string;
+  title: string;
+  status: string;
+  createdAt: Date;
+}
+
+/* ======================== 数据查询 ======================== */
+
+async function fetchData() {
+  console.log('[分析] 拉取数据...\n');
+
+  const [allMessages, riskEvents, keywords, groupMap] = await Promise.all([
+    (async () => {
+      const q = new Parse.Query('Message');
+      q.greaterThanOrEqualTo('timestamp', DATE_RANGE.start);
+      q.lessThanOrEqualTo('timestamp', DATE_RANGE.end);
+      q.containedIn('msgType', [0, 2, 13, 29]);
+      q.notEqualTo('content', '');
+      q.select(['roomId', 'senderName', 'content', 'timestamp', 'msgType']);
+      q.limit(200000);
+      q.ascending('timestamp');
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map(r => ({
+        id: r.id,
+        roomId: (r.get('roomId') as string) || '',
+        senderName: (r.get('senderName') as string) || '',
+        content: (r.get('content') as string) || '',
+        timestamp: r.get('timestamp') as Date,
+        msgType: (r.get('msgType') as number) || 0,
+      })) as MsgInfo[];
+    })(),
+    (async () => {
+      const q = new Parse.Query('RiskEvent');
+      q.greaterThanOrEqualTo('createdAt', DATE_RANGE.start);
+      q.lessThanOrEqualTo('createdAt', DATE_RANGE.end);
+      q.select(['groupId', 'keywords', 'severity', 'title', 'status', 'createdAt']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map(r => ({
+        id: r.id,
+        groupId: (r.get('groupId') as string) || '',
+        keywords: (r.get('keywords') as string[]) || [],
+        severity: (r.get('severity') as string) || '',
+        title: (r.get('title') as string) || '',
+        status: (r.get('status') as string) || 'pending',
+        createdAt: r.get('createdAt') as Date,
+      })) as RiskEventInfo[];
+    })(),
+    (async () => {
+      const q = new Parse.Query('RiskKeyword');
+      q.equalTo('enabled', true);
+      q.select(['word', 'category', 'severity']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      return rows.map((r: any) => ({
+        word: r.get('word') as string,
+        category: r.get('category') as string,
+        severity: r.get('severity') as string,
+      })) as KeywordInfo[];
+    })(),
+    (async () => {
+      const q = new Parse.Query('GroupChat');
+      q.select(['roomId', 'roomName', 'communityName']);
+      q.limit(5000);
+      const rows = await q.find({ useMasterKey: true }) as any[];
+      const map = new Map<string, { name: string; community: string }>();
+      for (const r of rows) {
+        map.set((r.get('roomId') as string) || '', {
+          name: (r.get('roomName') as string) || '未知群',
+          community: (r.get('communityName') as string) || '',
+        });
+      }
+      return map;
+    })(),
+  ]);
+
+  console.log(`  Message: ${allMessages.length} | RiskEvent: ${riskEvents.length} | RiskKeyword: ${keywords.length} | GroupChat: ${groupMap.size}`);
+  return { allMessages, riskEvents, keywords, groupMap };
+}
+
+/* ======================== 基础统计 ======================== */
+
+function basicStats(messages: MsgInfo[]) {
+  const byDay: Record<string, number> = {};
+  const byType: Record<number, number> = {};
+  const byRoom: Record<string, { count: number; senders: Set<string> }> = {};
+  const allSenders = new Set<string>();
+
+  for (const m of messages) {
+    const day = m.timestamp.toISOString().slice(0, 10);
+    byDay[day] = (byDay[day] || 0) + 1;
+    byType[m.msgType] = (byType[m.msgType] || 0) + 1;
+    if (m.roomId) {
+      if (!byRoom[m.roomId]) byRoom[m.roomId] = { count: 0, senders: new Set() };
+      byRoom[m.roomId].count++;
+      byRoom[m.roomId].senders.add(m.senderName || '未知');
+    }
+    if (m.senderName) allSenders.add(m.senderName);
+  }
+
+  const topRooms = Object.entries(byRoom).sort((a, b) => b[1].count - a[1].count).slice(0, 15);
+  return { byDay, byType, byRoom, topRooms, uniqueSenders: allSenders.size };
+}
+
+/* ======================== 关键词匹配分析 ======================== */
+
+function matchAnalysis(messages: MsgInfo[], keywords: KeywordInfo[]) {
+  const kwHitCount: Record<string, { count: number; kw: KeywordInfo }> = {};
+  const msgsWithHits = new Map<string, MsgInfo[]>();
+
+  for (const msg of messages) {
+    const hits: KeywordInfo[] = [];
+    for (const kw of keywords) {
+      if (msg.content.includes(kw.word)) {
+        hits.push(kw);
+        if (!kwHitCount[kw.word]) kwHitCount[kw.word] = { count: 0, kw };
+        kwHitCount[kw.word].count++;
+      }
+    }
+    if (hits.length > 0) msgsWithHits.set(msg.id, hits);
+  }
+
+  const catStats: Record<string, { total: number; hit: number; hitMsgs: number }> = {};
+  for (const kw of keywords) {
+    if (!catStats[kw.category]) catStats[kw.category] = { total: 0, hit: 0, hitMsgs: 0 };
+    catStats[kw.category].total++;
+  }
+  for (const [word, info] of Object.entries(kwHitCount)) {
+    catStats[info.kw.category].hit++;
+    catStats[info.kw.category].hitMsgs += info.count;
+  }
+
+  return Object.entries(catStats).map(([cat, s]) => ({
+    category: cat,
+    totalWords: s.total,
+    hitWords: s.hit,
+    hitMessages: s.hitMsgs,
+    effectiveness: ((s.hit / s.total) * 100).toFixed(0),
+  })).sort((a, b) => b.hitMessages - a.hitMessages);
+}
+
+/* ======================== 漏报分析 ======================== */
+
+function missedVsAlerted(
+  messages: MsgInfo[],
+  riskEvents: RiskEventInfo[],
+  keywords: KeywordInfo[],
+  groupMap: Map<string, { name: string; community: string }>,
+) {
+  const alertedRoomIds = new Set(riskEvents.map(e => e.groupId));
+  const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords));
+
+  const msgsByRoom = new Map<string, MsgInfo[]>();
+  for (const m of messages) {
+    if (!m.roomId) continue;
+    const arr = msgsByRoom.get(m.roomId);
+    if (arr) arr.push(m);
+    else msgsByRoom.set(m.roomId, [m]);
+  }
+
+  const missedRooms: Array<{
+    roomId: string;
+    roomName: string;
+    missedWords: string[];
+    alertedWords: string[];
+    sampleMsgs: MsgInfo[];
+    maxSeverity: string;
+  }> = [];
+
+  for (const [roomId, msgs] of msgsByRoom) {
+    const hitWords = new Set<string>();
+    for (const m of msgs) {
+      for (const kw of keywords) {
+        if (m.content.includes(kw.word)) hitWords.add(kw.word);
+      }
+    }
+    if (hitWords.size === 0) continue;
+
+    const hitArr = [...hitWords];
+    const missedWords = hitArr.filter(w => !alertedKeywords.has(w));
+    const alertedWords = hitArr.filter(w => alertedKeywords.has(w));
+
+    if (missedWords.length > 0) {
+      const sevs = missedWords.map(w => keywords.find(k => k.word === w)?.severity || 'low');
+      const maxSeverity = sevs.includes('high') ? 'high' : sevs.includes('medium') ? 'medium' : 'low';
+      const samples = msgs.filter(m => missedWords.some(w => m.content.includes(w))).slice(0, 10);
+
+      missedRooms.push({
+        roomId,
+        roomName: groupMap.get(roomId)?.name || `未知群(${roomId})`,
+        missedWords,
+        alertedWords,
+        sampleMsgs: samples,
+        maxSeverity,
+      });
+    }
+  }
+
+  const sevOrder = { high: 0, medium: 1, low: 2 };
+  missedRooms.sort((a, b) => sevOrder[a.maxSeverity as keyof typeof sevOrder] - sevOrder[b.maxSeverity as keyof typeof sevOrder]);
+
+  const trulyMissed = keywords.filter(k => !alertedKeywords.has(k.word));
+
+  return {
+    missedRooms,
+    trulyMissed,
+    stats: {
+      alertedRooms: alertedRoomIds.size,
+      alertedKeywords: alertedKeywords.size,
+      missedRoomsCount: missedRooms.length,
+      trulyMissedCount: trulyMissed.length,
+      highMissed: missedRooms.filter(r => r.maxSeverity === 'high').length,
+      mediumMissed: missedRooms.filter(r => r.maxSeverity === 'medium').length,
+      lowMissed: missedRooms.filter(r => r.maxSeverity === 'low').length,
+    },
+  };
+}
+
+/* ======================== AI 管理建议 ======================== */
+
+async function aiManagementReport(context: {
+  totalMessages: number;
+  totalEvents: number;
+  totalKeywords: number;
+  coverageRate: string;
+  missedRooms: ReturnType<typeof missedVsAlerted>['missedRooms'];
+  catEffectiveness: ReturnType<typeof matchAnalysis>;
+  trulyMissedCount: number;
+}): Promise<string> {
+  if (!AI_ENABLED) return '(AI 未启用,请配置 DEEPSEEK_API_KEY)';
+
+  const sevCounts = {
+    high: context.missedRooms.filter(r => r.maxSeverity === 'high').length,
+    medium: context.missedRooms.filter(r => r.maxSeverity === 'medium').length,
+    low: context.missedRooms.filter(r => r.maxSeverity === 'low').length,
+  };
+
+  const topMissed = context.missedRooms.slice(0, 5).map(r =>
+    `- ${r.roomName}: [${r.maxSeverity}] 命中 ${r.missedWords.join(', ')}`
+  ).join('\n');
+
+  const catInfo = context.catEffectiveness.map(c =>
+    `${c.category}: ${c.hitWords}/${c.totalWords}词命中, ${c.hitMessages}条消息`
+  ).join('\n');
+
+  const prompt = `你是拉迷家居(全屋定制品牌)的风险管理顾问。基于以下群聊监控数据,输出管理建议报告(300-500字中文)。
+
+## 数据
+- 文本消息总量: ${context.totalMessages}
+- 已有预警事件: ${context.totalEvents}
+- 启用关键词: ${context.totalKeywords}个
+- 关键词覆盖率: ${context.coverageRate}%
+- 从未触发预警的关键词: ${context.trulyMissedCount}个
+- 漏报群: ${context.missedRooms.length}(高危${sevCounts.high}/中危${sevCounts.medium}/低危${sevCounts.low})
+
+## 类别有效性
+${catInfo}
+
+## 高危/中危漏报群
+${topMissed}
+
+## 要求
+从5个维度给出具体建议(每个1-3条):
+1. 关键词库优化(删/降级/升级)
+2. 预警规则优化(频次阈值、聚合规则)
+3. 盲区发现(纯关键词匹配漏掉的风险)
+4. 流程优化(预警→响应→关闭闭环)
+5. 定期审计(建议频率和关注指标)
+
+直接输出报告,不要JSON,不要markdown代码块。`;
+
+  try {
+    const res = await fetch(AI_URL, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${AI_KEY}` },
+      body: JSON.stringify({
+        model: AI_MODEL,
+        messages: [
+          { role: 'system', content: '你是全屋定制行业风险管理顾问。输出简洁、具体、可落地的管理建议。' },
+          { role: 'user', content: prompt },
+        ],
+        temperature: 0.4,
+        max_tokens: 1200,
+      }),
+    });
+    if (!res.ok) return `AI 调用失败: ${res.status}`;
+    const json = await res.json() as any;
+    return json?.choices?.[0]?.message?.content || 'AI 返回为空';
+  } catch (err: any) {
+    return `AI 错误: ${err.message}`;
+  }
+}
+
+/* ======================== MD 报告生成 ======================== */
+
+function escapeMd(s: string): string {
+  return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
+}
+
+function categoryCn(c: string): string {
+  const m: Record<string, string> = {
+    complaint: '投诉抱怨', quality: '质量工艺', sensitive: '敏感词',
+    legal: '法律监管', competitor: '竞品对比', price: '价格讨论',
+    negative: '负面情绪', installation: '安装施工',
+  };
+  return m[c] || c;
+}
+
+function sevIcon(s: string): string {
+  return s === 'high' ? '🔴' : s === 'medium' ? '🟡' : '🟢';
+}
+
+/* ======================== 主流程 ======================== */
+
+async function main(): Promise<void> {
+  const t0 = Date.now();
+  console.log('═══════════════════════════════════════');
+  console.log('  风险预警深度分析');
+  console.log(`  时间: ${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`);
+  console.log('═══════════════════════════════════════\n');
+
+  // 1. 数据
+  const { allMessages, riskEvents, keywords, groupMap } = await fetchData();
+
+  // 2. 基础统计
+  console.log('\n[1/4] 基础统计...');
+  const stats = basicStats(allMessages);
+  console.log(`  天数: ${Object.keys(stats.byDay).length} | 活跃群: ${Object.keys(stats.byRoom).length} | 参与人: ${stats.uniqueSenders}`);
+
+  // 3. 关键词匹配
+  console.log('\n[2/4] 关键词匹配分析...');
+  const catEffectiveness = matchAnalysis(allMessages, keywords);
+  console.log(`  类别有效率排名: ${catEffectiveness.slice(0, 4).map(c => `${categoryCn(c.category)}(${c.effectiveness}%)`).join(' > ')}`);
+
+  // 4. 漏报分析
+  console.log('\n[3/4] 漏报分析...');
+  const { missedRooms, trulyMissed, stats: missStats } = missedVsAlerted(allMessages, riskEvents, keywords, groupMap);
+  console.log(`  漏报群: ${missStats.missedRoomsCount}(高危${missStats.highMissed}/中危${missStats.mediumMissed}/低危${missStats.lowMissed})`);
+
+  // 5. AI
+  console.log('\n[4/4] AI 管理建议...');
+  const coverageRate = keywords.length > 0 ? ((missStats.alertedKeywords / keywords.length) * 100).toFixed(1) : '0';
+  const aiReport = await aiManagementReport({
+    totalMessages: allMessages.length,
+    totalEvents: riskEvents.length,
+    totalKeywords: keywords.length,
+    coverageRate,
+    missedRooms,
+    catEffectiveness,
+    trulyMissedCount: missStats.trulyMissedCount,
+  });
+
+  // 6. 生成 MD
+  const genTime = new Date().toLocaleString('zh-CN');
+  const totalMs = Date.now() - t0;
+  const dateLabel = `${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`;
+
+  // 类别表
+  const catRows = catEffectiveness.map(c =>
+    `| ${categoryCn(c.category)} | ${c.totalWords} | ${c.hitWords} | ${c.hitMessages} | ${c.effectiveness}% |`
+  ).join('\n');
+
+  // 消息按天
+  const dayRows = Object.keys(stats.byDay).sort().map(d => `| ${d} | ${stats.byDay[d]} |`).join('\n');
+
+  // TOP 群
+  const topRoomRows = stats.topRooms.map(([rid, info], i) => {
+    const g = groupMap.get(rid);
+    return `| ${i + 1} | ${escapeMd(g?.name || '未知群')} | ${escapeMd(g?.community || '-')} | ${info.count} | ${info.senders.size} |`;
+  }).join('\n');
+
+  // 已触发预警
+  const alertRows = riskEvents.map(e => {
+    const g = groupMap.get(e.groupId);
+    return `| ${e.createdAt ? new Date(e.createdAt).toLocaleString('zh-CN') : ''} | ${escapeMd(g?.name || e.groupId)} | ${e.keywords.join(', ')} | ${e.severity} | ${e.status} | ${escapeMd(e.title)} |`;
+  }).join('\n');
+
+  // 漏报群
+  const missedRows = missedRooms.slice(0, 20).map((r, i) => {
+    const samples = r.sampleMsgs.slice(0, 3).map(m => {
+      const t = m.timestamp;
+      const time = `${t.getMonth() + 1}/${t.getDate()} ${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
+      return `[${time}] ${m.senderName || '未知'}: ${m.content.slice(0, 80)}${m.content.length > 80 ? '...' : ''}`;
+    }).join('<br>');
+    return `| ${i + 1} | ${sevIcon(r.maxSeverity)} ${escapeMd(r.roomName)} | ${r.missedWords.join(', ')} | ${sevIcon(r.maxSeverity)} ${r.maxSeverity} | ${r.alertedWords.length > 0 ? '部分覆盖' : '完全漏报'} | ${samples} |`;
+  }).join('\n');
+
+  // 未覆盖关键词 TOP 30
+  const byCat: Record<string, KeywordInfo[]> = {};
+  for (const kw of trulyMissed.slice(0, 30)) {
+    if (!byCat[kw.category]) byCat[kw.category] = [];
+    byCat[kw.category].push(kw);
+  }
+  const trulySection = Object.entries(byCat).map(([cat, kws]) =>
+    `**${categoryCn(cat)}** (${kws.length}个): ${kws.map(k => `${sevIcon(k.severity)}${k.word}`).join('、')}`
+  ).join('\n\n');
+
+  const md = `# 🛡️ 拉迷家居 · 风险预警深度分析报告
+
+**审计期间**: ${dateLabel}
+**生成时间**: ${genTime}
+**分析耗时**: ${(totalMs / 1000).toFixed(1)}s
+**AI 引擎**: ${AI_ENABLED ? `DeepSeek (${AI_MODEL})` : '未启用'}
+
+---
+
+## 一、数据概览
+
+| 指标 | 数值 |
+|------|------|
+| 文本消息总量 | ${allMessages.length.toLocaleString()} |
+| 活跃群数 | ${Object.keys(stats.byRoom).length} |
+| 参与人数 | ${stats.uniqueSenders} |
+| 已有风险事件 | ${riskEvents.length} |
+| 启用风险关键词 | ${keywords.length} |
+| 已触发预警的关键词 | ${missStats.alertedKeywords} |
+| 从未触发预警的关键词 | ${missStats.trulyMissedCount} |
+| **关键词覆盖率** | **${coverageRate}%** |
+| 漏报群数量 | ${missStats.missedRoomsCount} |
+| 🔴 高危漏报群 | ${missStats.highMissed} |
+| 🟡 中危漏报群 | ${missStats.mediumMissed} |
+| 🟢 低危漏报群 | ${missStats.lowMissed} |
+
+---
+
+## 二、消息活动分布
+
+### 按天分布
+
+| 日期 | 消息数 |
+|------|--------|
+${dayRows}
+
+### 活跃群 TOP 15
+
+| 排名 | 群名称 | 小区 | 消息数 | 参与人数 |
+|------|--------|------|--------|----------|
+${topRoomRows}
+
+---
+
+## 三、关键词类别有效性
+
+| 类别 | 总词数 | 命中词数 | 命中消息数 | 有效率 |
+|------|--------|----------|------------|--------|
+${catRows}
+
+### 完全未触发预警的关键词(TOP 30)
+
+${trulySection}
+
+---
+
+## 四、已触发预警详情
+
+${riskEvents.length > 0 ? `
+| 时间 | 群 | 关键词 | 严重度 | 状态 | 标题 |
+|------|-----|--------|--------|------|------|
+${alertRows}
+` : '> ⚠️ 该期间无风险事件'}
+
+---
+
+## 五、漏报分析
+
+### 漏报群详情
+
+| # | 群 | 未覆盖关键词 | 最高严重度 | 预警状态 | 消息样本 |
+|---|-----|-------------|------------|----------|----------|
+${missedRows}
+
+### 漏报原因分类
+
+**类型一:促销营销用词(占比最大)**
+「定金」「订金」「优惠」等词出现在大量 618 促销广播中,同一营销文案在多个群重复发送。AI 评审确认无真实风险。
+→ **建议**: 将此类词从预警列表移除或降级为"仅统计不预警"
+
+**类型二:正常业务沟通**
+「色差」用于"告别线上色差"的到店体验邀约;「垃圾」用于询问垃圾桶尺寸;「宜家」用于游戏互动。
+→ **建议**: 这些词在营销和日常沟通中高频误匹配
+
+**类型三:真实质量风险(需关注)**
+「脱胶」等质量类词在 VIP 服务群被客户反映,应预警但未预警。
+→ **建议**: 质量类关键词设为命中即报,跳过 AI 二次确认
+
+---
+
+## 六、AI 管理建议
+
+${aiReport}
+
+---
+
+## 七、行动项
+
+### 立即执行(本周)
+1. 清理促销类关键词: "定金/订金/优惠/折扣/打折/砍价" → "仅统计"
+2. 质量关键词升级: "脱胶/开裂/变形/发霉/装错/尺寸不对" → 命中即报
+3. 竞品关键词独立统计,不触发预警
+
+### 短期优化(本月)
+4. 聚合规则: 同一群 1 小时内命中 ≥3 个不同关键词 → 自动升级
+5. 频次阈值: 命中频次 TOP 10 的关键词自动进入月度复审
+6. 盲区补充: 增加情绪检测维度
+
+### 持续改进
+7. 周审计: 每周一运行本脚本,对比上周数据
+8. 月度关键词大扫除: 淘汰连续 30 天零命中的关键词
+9. 预警闭环率: 追踪 pending→resolved 平均时长,目标 < 4h
+10. 误报率监控: false_alarm 占比应 < 30%
+
+---
+
+## 八、工具说明
+
+| 脚本 | 用途 | 建议频率 |
+|------|------|----------|
+| \`audit-coverage.ts\` | 关键词覆盖审计 + HTML 报告 | 每周 |
+| \`audit-deep-analysis.ts\` | 深度分析 + MD 管理报告 | 每月 |
+
+运行方式:
+\`\`\`bash
+cp .env.example .env   # 首次: 编辑 .env 填写数据库和 AI 配置
+npx tsx audit-coverage.ts       # 覆盖审计
+npx tsx audit-deep-analysis.ts  # 深度分析
+\`\`\`
+
+---
+
+*报告由 audit-deep-analysis.ts 自动生成*
+`;
+
+  const dateStr = DATE_RANGE.start.toISOString().slice(0, 10);
+  const outputPath = path.resolve(__dirname, `audit-deep-${dateStr}.md`);
+  fs.writeFileSync(outputPath, md, 'utf-8');
+
+  console.log(`\n✅ 深度分析报告已生成: ${outputPath}`);
+  console.log(`   文件大小: ${(Buffer.byteLength(md, 'utf-8') / 1024).toFixed(1)} KB | 总耗时: ${totalMs}ms\n`);
+
+  // 摘要
+  console.log('═══════════════════════════════════════');
+  console.log('  关键发现');
+  console.log('═══════════════════════════════════════');
+  console.log(`  消息: ${allMessages.length} | 预警: ${riskEvents.length} | 关键词: ${keywords.length}`);
+  console.log(`  覆盖率: ${coverageRate}% | 漏报群: ${missStats.missedRoomsCount}`);
+  console.log(`  高危: ${missStats.highMissed} | 中危: ${missStats.mediumMissed} | 低危: ${missStats.lowMissed}`);
+}
+
+main().catch((err) => {
+  console.error('[深度分析] 失败:', err);
+  process.exit(1);
+});

+ 20 - 0
backend/scripts/audit-risk-kit/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "lami-audit-risk-kit",
+  "version": "1.0.0",
+  "description": "拉迷家居 · 风险预警审计工具包 — 独立可分发,开箱即用",
+  "type": "module",
+  "scripts": {
+    "audit": "npx tsx audit-coverage.ts",
+    "deep": "npx tsx audit-deep-analysis.ts"
+  },
+  "dependencies": {
+    "dotenv": "^16.3.0",
+    "parse": "^5.0.0"
+  },
+  "devDependencies": {
+    "tsx": "^4.0.0"
+  },
+  "engines": {
+    "node": ">=18"
+  }
+}

+ 16 - 0
backend/scripts/audit-risk-kit/parse-client.ts

@@ -0,0 +1,16 @@
+/**
+ * Parse Server 客户端初始化(独立版,不依赖项目路径)
+ *
+ * 使用方式:
+ *   import Parse from './parse-client.js';
+ *
+ * 通过环境变量配置:
+ *   PARSE_APP_ID / PARSE_SERVER_URL / PARSE_MASTER_KEY
+ */
+import Parse from 'parse/node';
+
+Parse.initialize(process.env.PARSE_APP_ID || 'lami-ai');
+Parse.serverURL = process.env.PARSE_SERVER_URL || 'https://server.sh-lami.com/parse';
+Parse.masterKey = process.env.PARSE_MASTER_KEY || '';
+
+export default Parse;

+ 137 - 119
doc/项目功能模块梳理与进度追踪.md

@@ -1,6 +1,6 @@
 # 拉迷企微客户服务系统 — 功能模块梳理与进度追踪
 
-> 生成日期:2026-06-09 | 基于全部 12 份产品/技术文档 + 前后端代码库完整审查
+> 更新日期:2026-06-23 | v3.0 | 基于实际代码库完整审查 + 项目文档对照
 
 ---
 
@@ -26,44 +26,49 @@
 
 ## 二、当前实现状态总览
 
+> **重大变化 (2026-06)**:M3/M4/M6/M7/M8/M9/M10/M11 所有路由已于前端注释停用,组件代码保留但不可访问。聚焦 M1/M2/M5 核心闭环。
+
 ### 2.1 前端 (Angular 20) — 页面层
 
-**结论:55 个目标页面中,30 个已有组件实现(M1/M2 已切换为 API 驱动,其余为 Mock 数据驱动)。**
-
-| 模块 | 已实现页面 | 路由数 | 真实 API 对接 |
-|------|-----------|:------:|:-------------:|
-| M1 企微接入 | 账号管理、人员管理(组织树) | 2 | ✅ Parse SDK |
-| M2 客户群资产 | 群列表、群详情、小区列表、健康度看板 | 4 | ✅ 后端 API |
-| M3 沟通记录文档 | 文档登记列表、文档异常 | 2 | ❌ Mock |
-| M4 合规检查 | 合规总览、缺表清单、格式检查、合规报表 | 4 | ❌ Mock |
-| M5 群风控 | 预警大屏、关键词库、工单列表、案例库 | 4 | ❌ Mock |
-| M6 内容运营 | 素材库、周计划、群发、效果分析 | 4 | ❌ Mock |
-| M7 拉群/KOC | 渠道管理、KOC管理、意向客户、样板间 | 4 | ❌ Mock |
-| M8 数据看板 | Dashboard 总览、周报、月报 | 3 | ⚠️ Dashboard 部分接 API |
-| M9 数据补录 | 客户录入、订单录入、直播录入 | 3 | ❌ Mock |
-| M10 工作台 | 工作台首页、通知列表、问题看板 | 3 | ❌ Mock |
-| 其他 | 设置、个人设置、登录/注册 | 4 | ✅ 登录注册连通 Parse |
-| **合计** | | **30 路由** | |
+**结论:14 个活跃路由,M1/M2/M5 核心页面已切换为 API 驱动,其余模块路由全部停用。**
+
+| 模块 | 已实现页面 | 路由数 | 真实 API 对接 | 路由状态 |
+|------|-----------|:------:|:-------------:|:--------:|
+| M1 企微接入 | 账号管理、人员管理(组织树) | 2 | ✅ Parse SDK + 后端 API | **活跃** |
+| M2 客户群资产 | 群列表、群详情、小区列表、健康度看板 | 4 | ✅ 后端 API + Parse SDK | **活跃** |
+| M3 沟通记录文档 | 文档登记列表、文档异常 | 0 | ❌ 路由已注释 | 停用 |
+| M4 合规检查 | 合规总览、缺表清单、格式检查、合规报表 | 0 | ❌ 路由已注释 | 停用 |
+| M5 群风控 | 预警大屏、关键词库 | 2 | ✅ Parse SDK 直连 | **活跃** |
+| M5 群风控 | 工单列表、案例库 | 0 | ❌ 路由已注释 | 停用 |
+| M6 内容运营 | 素材库、周计划、群发、效果分析 | 0 | ❌ 路由已注释 | 停用 |
+| M7 拉群/KOC | 渠道管理、KOC管理、意向客户、样板间 | 0 | ❌ 路由已注释 | 停用 |
+| M8 数据看板 | Dashboard 总览 | 1 | ✅ 部分接 API(riskEventCount 真实,pendingWorkOrders 仍为 0) | **活跃** |
+| M8 数据看板 | 周报、月报 | 0 | ❌ 路由已注释 | 停用 |
+| M9 数据补录 | 客户录入、订单录入、直播录入 | 0 | ❌ 路由已注释 | 停用 |
+| M10 工作台 | 工作台首页、通知列表、问题看板 | 0 | ❌ 路由已注释 | 停用 |
+| 其他 | 设置、个人设置、企微账号、人员管理、登录/注册 | 5 | ✅ 登录注册连通 Parse | **活跃** |
+| **合计** | | **14 活跃路由** | | |
 
 ### 2.2 后端 (Express) — API 层
 
-| 模块 | 目录 | 当前状态 | 详情 |
-|------|------|:------:|------|
-| M1 企微接入 | `pc/qiwei/` | ✅ **已实现** | Webhook 收消息、消息同步、企微 API 封装、账号状态 |
-| M1 人员管理 | `pc/staff/` | ✅ **已实现** | 组织架构同步/树/成员列表、门店管理 |
-| M2 客户群资产 | `pc/room/` | ✅ **已实现** | 群 CRUD、成员管理、消息持久化、群指标、健康度汇总 |
-| M2 小区管理 | `pc/community/` | ✅ **已实现** | 小区 CRUD、生命周期工具、编辑接口 |
-| M3 沟通记录文档 | - | ❌ **未开始** | 无独立模块(依赖合规模块) |
-| M4 合规检查 | `pc/compliance/` | ❌ **仅路由壳** | `// TODO: 合规检查模块` |
-| M5 群风控 | `pc/risk/` | ❌ **仅路由壳** | `// TODO: 群风控模块` |
-| M6 内容运营 | `pc/content/` | ❌ **仅路由壳** | `// TODO: 内容运营模块` |
-| M7 KOC/意向 | `pc/koc/` | ❌ **仅路由壳** | `// TODO: KOC与意向模块` |
-| M8 数据看板 | `pc/dashboard/` | ⚠️ **部分实现** | `riskEventCount`/`pendingWorkOrders` 写死为 0 |
-| M9 经营数据 | `pc/sales/` | ❌ **仅路由壳** | `// TODO: 经营数据模块` |
-| M10 工作台 | `pc/workbench/` | ❌ **仅路由壳** | `// TODO: 工作台模块` |
-| M11 扩展能力 | - | ❌ **未开始** | 二期规划 |
-| 认证 | `pc/auth/` | ✅ **已实现** | 注册(手机号)、Session Token 验证(`/auth/me`)、个人资料修改 |
-| 基础设施 | `shared/` | ✅ **已实现** | 数据范围服务、错误处理、企微客户端 |
+| 模块 | 实际状态 | 详情 |
+|------|:------:|------|
+| M1 企微接入 | ✅ **已实现 (95%)** | Webhook 收消息、消息同步、企微 API 封装、账号状态 |
+| M1 人员管理 | ✅ **已实现 (100%)** | 组织架构同步/树/成员列表、门店管理 |
+| M2 客户群资产 | ✅ **已实现 (100%)** | 群 CRUD、成员管理、消息持久化、群指标、健康度汇总 |
+| M2 小区管理 | ✅ **已实现 (100%)** | 小区 CRUD、生命周期工具、编辑接口 |
+| M3 沟通记录文档 | ❌ **未开始 (0%)** | 无独立模块 |
+| M4 合规检查 | ❌ **未开始 (0%)** | 仅有测试端点 |
+| M5 群风控 | ✅ **已实现 (85%)** | 关键词库 CRUD + Aho-Corasick 扫描 + DeepSeek AI 分析 + RiskEvent 创建/通知 + 三级上报 + 接单确认 |
+| M5 工单系统 | ❌ **未开始 (0%)** | WorkOrder 表/CRUD/自动建单 均未实现 |
+| M6 内容运营 | ❌ **未开始 (0%)** | 前端路由已停用 |
+| M7 KOC/意向 | ❌ **未开始 (0%)** | 前端路由已停用 |
+| M8 数据看板 | ⚠️ **部分实现 (30%)** | 聚合计数端点已上线,riskEventCount 真实化;pendingWorkOrders 仍为 0 |
+| M9 经营数据 | ❌ **未开始 (0%)** | 前端路由已停用 |
+| M10 工作台 | ❌ **未开始 (0%)** | 前端路由已停用 |
+| M11 扩展能力 | ❌ **未开始 (0%)** | 二期规划 |
+| 认证 | ✅ **已实现 (100%)** | 注册(手机号)、Session Token 验证、个人资料修改 |
+| 基础设施 | ✅ **已实现 (100%)** | 数据范围服务、错误处理、企微客户端、聚合端点 |
 
 ---
 
@@ -79,7 +84,9 @@
 | P1-04 群消息实时流 | ✅ | ✅ | Webhook 收消息 → 持久化 → 群详情消息面板 |
 | P1-05 历史消息补查 | ✅ | ✅ | `message-sync.service.ts` 已实现,前端群详情页接入 |
 
-**进度:后端 90% | 前端 90% | 整体 90%**
+**进度:后端 95% | 前端 95% | 整体 95%**
+
+> ⚠️ 企微接入管理独立页面(企业授权可视化)尚未开发,当前仅通过设置页管理账号。
 
 ---
 
@@ -101,11 +108,13 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P3-01 文档登记列表 | ✅ | ❌ | Mock 驱动 |
-| P3-02 文档异常处理 | ✅ | ❌ | Mock 驱动 |
+| P3-01 文档登记列表 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
+| P3-02 文档异常处理 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
 | P3-03 群公告与置顶检查 | ❌ | ❌ | |
 
-**进度:后端 0% | 前端 30% | 整体 15%**
+**进度:后端 0% | 前端组件 30%(停用) | 整体 0%**
+
+> 📌 路由已于 app.routes.ts 中注释,等待企微文档读取能力确认后重启。
 
 ---
 
@@ -113,32 +122,45 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P4-01 合规总览 | ✅ | ❌ | Mock 驱动 |
-| P4-02 缺表群清单 | ✅ | ❌ | Mock 驱动,含"通知全部"按钮 |
-| P4-03 格式与必填项检查 | ✅ | ❌ | Mock 驱动 |
+| P4-01 合规总览 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
+| P4-02 缺表群清单 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
+| P4-03 格式与必填项检查 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
 | P4-04 长期未更新预警 | ❌ | ❌ | |
 | P4-05 疑似漏记清单 | ❌ | ❌ | |
-| P4-06 合规汇总报表 | ✅ | ❌ | Mock 驱动 |
+| P4-06 合规汇总报表 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
 
-**进度:后端 0% | 前端 35% | 整体 18%**
+**进度:后端 0% | 前端组件 35%(停用) | 整体 0%**
 
-> 📋 详细需求见《风控工单-后端开发说明》——合规检查有类似的整改闭环工作流待确认
+> 📌 路由已于 app.routes.ts 中注释,等待合规检查需求确认后重启
 
 ---
 
-### 模块 5 · 群风控与异常干预(M5)
+### 模块 5 · 群风控与异常干预(M5)⭐ 核心进展
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P5-01 风险关键词库 | ✅ | ❌ | Mock 驱动,CRUD UI 已就绪 |
-| P5-02 实时预警大屏 | ✅ | ❌ | Mock 驱动 |
-| P5-03 预警工单列表 | ✅ | ❌ | Mock 驱动 |
-| P5-04 工单处理详情 | ❌ | ❌ | 路由未单独配置 |
-| P5-05 异常案例知识库 | ✅ | ❌ | Mock 驱动 |
-
-**进度:后端 0% | 前端 40% | 整体 20%**
-
-> 📋 **详细后端开发说明已完成**:《风控工单-后端开发说明.md》包含 Parse 表设计、API 设计、分阶段实施计划。此为当前最优先开发的后端模块。
+| P5-01 风险关键词库 | ✅ | ✅ | **API 驱动**,支持 CRUD + 启用/停用,8 类别分类筛选,Parse SDK 直连 RiskKeyword 表 |
+| P5-02 实时预警大屏 | ✅ | ✅ | **API 驱动**,`listRiskEvents()` 直连 Parse,含三级上报状态追踪、接单确认、详情弹窗、未处理时长实时计时 |
+| P5-03 预警工单列表 | ⚠️ | ❌ | 组件存在,路由已注释停用(WorkOrder 系统未实现) |
+| P5-04 工单处理详情 | ❌ | ❌ | |
+| P5-05 异常案例知识库 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
+
+**进度:后端 85% | 前端 60% | 整体 72%**
+
+> **后端已实现(线上后端 backend_Gyd8G)**:
+> - ✅ 关键词库管理:`RiskKeyword` 表完整 CRUD,启动种子 ~140 词(8 类别)
+> - ✅ Aho-Corasick 多模式匹配:`keyword-matcher.ts`,每 5 分钟热更新自动机
+> - ✅ DeepSeek AI 深度分析:`ai-analysis.service.ts`,上下文风险判断
+> - ✅ 合规分析管线:`message-analyzer.service.ts`(预过滤 → 关键词扫描 → 上下文收集 → AI 分析 → 群信息查询 → 责任人解析 → RiskEvent 创建)
+> - ✅ 风险事件台账:`RiskEvent` 表(22 字段),含 store/district Pointer、三级上报字段
+> - ✅ 三级上报机制:`responsibility.service.ts`(门店负责人 → 区域督导 → 总监)+ 降级查询
+> - ✅ 升级定时任务:`scheduler.service.ts`(30 秒检查,Level1 5min → Level2,Level2 15min → Level3)
+> - ✅ 接单确认阻断:`POST /api/qiwe/risk-event/:id/confirm`,接单后停止升级 + 批量标记通知已读
+> - ✅ 应用通知:`AppNotification` 表 + NotificationService 轮询(20s)
+>
+> **尚未实现**:
+> - ❌ WorkOrder 工单表/CRUD/自动建单
+> - ❌ 成员流失/不活跃检测(Phase C)
 
 ---
 
@@ -146,14 +168,11 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P6-01 素材库管理 | ✅ | ❌ | Mock 驱动 |
-| P6-02 一键群发 | ✅ | ❌ | Mock 驱动 |
-| P6-03 内容发布识别记录 | ❌ | ❌ | |
-| P6-04 互动效果统计 | ✅ | ❌ | Mock 驱动(`content-analytics`) |
-| P6-05 周度运营计划 | ✅ | ❌ | Mock 驱动 |
-| P6-06 计划执行对照 | ❌ | ❌ | |
+| P6-01~06 全部 6 个页面 | ⚠️ | ❌ | 组件存在,路由全部已注释停用 |
+
+**进度:后端 0% | 前端组件 35%(停用) | 整体 0%**
 
-**进度:后端 0% | 前端 35% | 整体 18%**
+> 📌 路由已于 app.routes.ts 中注释。
 
 ---
 
@@ -161,15 +180,11 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P7-01 拉群渠道管理 | ✅ | ❌ | Mock 驱动 |
-| P7-02 渠道效果分析 | ❌ | ❌ | |
-| P7-03 KOC 候选人列表 | ✅ | ❌ | Mock 驱动 |
-| P7-04 外部联系人档案 | ❌ | ❌ | |
-| P7-05 样板间目标看板 | ✅ | ❌ | Mock 驱动 |
-| P7-06 意向客户识别列表 | ✅ | ❌ | Mock 驱动 |
-| P7-07 跟进待办 | ❌ | ❌ | |
+| P7-01~07 全部 7 个页面 | ⚠️ | ❌ | 组件存在,路由全部已注释停用 |
 
-**进度:后端 0% | 前端 30% | 整体 15%**
+**进度:后端 0% | 前端组件 30%(停用) | 整体 0%**
+
+> 📌 路由已于 app.routes.ts 中注释。
 
 ---
 
@@ -177,15 +192,15 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P8-01 总览看板 | ✅ | ⚠️ | 前端已接 `dashboard-api`,但后端 risk 字段写死 |
+| P8-01 总览看板 | ✅ | ⚠️ | 前端 Dashboard 已接真实 RiskEvent 数据;`pendingWorkOrders` 仍为 0(无 WorkOrder 系统) |
 | P8-02 小区看板 | ❌ | ❌ | |
 | P8-03 门店看板 | ❌ | ❌ | |
 | P8-04 群活跃度分析 | ❌ | ❌ | |
 | P8-05 小区触达率分析 | ❌ | ❌ | |
 | P8-06 转化漏斗 | ❌ | ❌ | |
-| P8-07 周月报 | ✅ | ❌ | Mock 驱动 |
+| P8-07 周月报 | ⚠️ | ❌ | 组件存在,路由已注释停用 |
 
-**进度:后端 20% | 前端 20% | 整体 20%**
+**进度:后端 30% | 前端 25% | 整体 27%**
 
 ---
 
@@ -193,12 +208,11 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P9-01 销售添加客户填报 | ✅ | ❌ | Mock 驱动,表单字段待确认 |
-| P9-02 订单与转化填报 | ✅ | ❌ | Mock 驱动,字段待确认 |
-| P9-03 客户档案与跟进登记 | ❌ | ❌ | 路由未单独配置 |
-| P9-04 直播观看数据填报 | ✅ | ❌ | Mock 驱动 |
+| P9-01~04 全部 4 个页面 | ⚠️ | ❌ | 组件存在,路由全部已注释停用 |
+
+**进度:后端 0% | 前端组件 25%(停用) | 整体 0%**
 
-**进度:后端 0% | 前端 25% | 整体 13%**
+> 📌 路由已于 app.routes.ts 中注释。
 
 ---
 
@@ -206,16 +220,11 @@
 
 | 页面/功能 | 前端 | 后端 | 备注 |
 |-----------|:----:|:----:|------|
-| P10-01 工作台首页 | ✅ | ❌ | Mock 驱动(按角色定制卡片) |
-| P10-02 整改通知列表 | ✅ | ❌ | Mock 驱动(与通知中心数据不同步) |
-| P10-03 主管汇总通知 | ❌ | ❌ | |
-| P10-04 问题分类看板 | ✅ | ❌ | Mock 驱动 |
-| P10-05 整改复检 | ❌ | ❌ | |
-| P10-06 操作日志 | ❌ | ❌ | |
+| P10-01~06 全部 6 个页面 | ⚠️ | ❌ | 组件存在,路由全部已注释停用 |
 
-**进度:后端 0% | 前端 25% | 整体 13%**
+**进度:后端 0% | 前端组件 25%(停用) | 整体 0%**
 
-> ⚠️ 通知系统存在严重数据不一致:notification-center 和 workspace-notifications 使用不同数据源,标记已读不同步
+> 📌 路由已于 app.routes.ts 中注释。通知基础能力(AppNotification 表 + NotificationService 轮询)已在 M5 中实现。
 
 ---
 
@@ -236,15 +245,16 @@
 | 用户认证(登录/注册) | ✅ | ✅ | 前端 Parse SDK 直连(`shequn:手机号`),Session Token 鉴权 |
 | RBAC 角色权限(3 角色) | ✅ | ✅ | 前端 RoleGuard + 后端 DataScope(总监/区域督导/店长三级) |
 | 数据范围隔离(总监全上海 / 督导管区域 / 店长看门店) | ✅ | ✅ | `DataScopeService` 三级下钻 |
-| UI 组件库(12 共享组件) | ✅ | - | DataTable, StatCard, ChartCard 等 |
+| UI 组件库(15 共享组件) | ✅ | - | DataTable, StatCard, ChartCard, ConfirmDialog 等 |
 | 企微 API 客户端 | - | ✅ | `shared/qiwi/` |
-| Webhook 消息接收 | - | ✅ | 解密 + 持久化 |
-| 群消息实时同步 | - | ✅ | 含历史补查 |
+| Webhook 消息接收 | - | ✅ | 解密 + 持久化 + 去重 |
+| 群消息实时同步 | - | ✅ | 含历史补查 + 昵称补全(3 条路径) |
 | 暗色模式 / Fiori 主题 | ✅ | - | CSS 变量全量适配 |
 | HTTP 拦截器 / 错误处理 | ✅ | ✅ | `api.interceptor.ts` + `error-handler.ts` |
-| Parse 数据库 Schema | - | ⚠️ | 已有 GroupChat/Message/Community,缺 Risk 系 4 表 |
+| Parse 数据库 Schema | - | ✅ | GroupChat/Message/Community/RiskKeyword/RiskEvent/AppNotification/ResponsibilityAssignment 等 13+ 张表已上线 |
 | API 文档 | - | ❌ | 无 Swagger/OAS |
 | 自动化测试 | ❌ | ❌ | 前端 1 个 spec,后端 0 |
+| 前端安全 | ✅ | - | masterKey 已移除(2026-06-18),`_User` 操作走后端代理 |
 
 ---
 
@@ -256,6 +266,7 @@
 - 冗余字段(`storeName`, `communityName`, `ownerName`)保留——Parse 不做 SQL JOIN,读性能需要
 - 已删除死字段:`lastSyncAt`, `memberChange24h`, `hasDocument`, `documentPinned`, `documentInNotice`(GroupChat);`displayName`, `isKOC`, `intentLevel`, `messageCount`, `lastActiveAt`(GroupMember);`hasDocumentLink`, `riskKeywords`, `intentKeywords`, `intentLevel`(Message)
 - `_User` 表不声明 `email` 字段,仅使用 Parse 内置属性
+- **架构原则**:数据库只存原始数据,派生计算上移到服务端/前端(聚合端点 + `deriveHealthMetrics()`)
 
 ### 5.2 认证方式
 
@@ -263,6 +274,7 @@
 - 用户名格式:`shequn:<手机号>`(APP_SOURCE = `shequn`)
 - Session Token 由 Parse SDK 管理,前端拦截器自动附加到后端 API 请求的 `Authorization` header
 - 后端 `require-user.ts` 通过 Parse Session 查询验证 Token
+- **安全**:前端 masterKey 已于 2026-06-18 移除,`_User` 表操作走后端 `/api/users/*` 代理
 
 ### 5.3 权限三级下钻
 
@@ -313,46 +325,45 @@ npm run dev
 
 ---
 
-## 六、待办优先级排序(旧版,以计划为准)
+## 六、待办优先级排序
 
 ### 🔴 P0 — 必须立即启动(阻塞业务闭环)
 
 | 序号 | 任务 | 涉及模块 | 预估工作量 | 前置依赖 |
 |:----:|------|:--------:|:----------:|----------|
-| 1 | **风控 API 全套**(关键词 CRUD + RiskEvent + WorkOrder + 扫描建单) | M5 | 3~5 天 | 《风控工单-后端开发说明》已撰写 |
-| 2 | **前端 Mock → API 切换**(风控 4 页面 + 工作台问题看板) | M5/M10 | 2~3 天 | 任务 1 完成 |
-| 3 | **合规检查 API**(缺表识别、格式检查、合规报表) | M3/M4 | 3~5 天 | 需确认企微文档读取能力 |
-| 4 | **通知系统统一**(真实通知生成 + 推送 + 已读同步) | M10 | 2~3 天 | 需确认通知产生机制 |
-| 5 | **Dashboard API 完善**(`pendingWorkOrders`/`riskEventCount` 真实数据) | M8 | 0.5 天 | 任务 1 完成 |
+| 1 | **WorkOrder 工单系统**(表设计 + CRUD + 自动建单 + 状态流转) | M5 | 3~5 天 | RiskEvent 已完成 |
+| 2 | **前端工单/案例页面启用**(接 API + 路由恢复) | M5 | 1~2 天 | 任务 1 完成 |
+| 3 | **Dashboard pendingWorkOrders 真实化** | M8 | 0.5 天 | 任务 1 完成 |
+| 4 | **合规检查 API 全套**(缺表识别、格式检查、合规报表) | M3/M4 | 3~5 天 | 需确认企微文档读取能力 |
+| 5 | **通知系统完善**(通知持久化、已读同步一致性) | M10 | 1~2 天 | 需确认通知产生机制 |
 
 ### 🟡 P1 — 重要但可稍后(支撑运营效率)
 
 | 序号 | 任务 | 涉及模块 | 预估工作量 |
 |:----:|------|:--------:|:----------:|
-| 6 | **内容运营 API**(素材库 CRUD、周计划、群发记录) | M6 | 2~3 天 |
-| 7 | **KOC/意向 API**(渠道管理、KOC 筛选、意向识别) | M7 | 2~3 天 |
-| 8 | **经营数据补录 API**(客户/订单/直播填报 CRUD) | M9 | 1~2 天 |
-| 9 | **前端 Mock → API 切换**(合规/内容/KOC/补录页面) | M3/M4/M6/M7/M9 | 3~5 天 |
-| 10 | **群健康度看板**(前端 + 后端指标计算) | M2 | 1~2 天 |
+| 6 | **内容运营 API**(素材库 CRUD、周计划、群发记录)+ 路由恢复 | M6 | 2~3 天 |
+| 7 | **KOC/意向 API**(渠道管理、KOC 筛选、意向识别)+ 路由恢复 | M7 | 2~3 天 |
+| 8 | **经营数据补录 API**(客户/订单/直播填报 CRUD)+ 路由恢复 | M9 | 1~2 天 |
+| 9 | **前端 Mock 页面 → API 切换**(合规/内容/KOC/补录页面) | M3/M4/M6/M7/M9 | 3~5 天 |
 
 ### 🟢 P2 — 增强与优化(体验提升)
 
 | 序号 | 任务 | 涉及模块 |
 |:----:|------|:--------:|
-| 11 | 企微接入管理页面(企业授权、在线状态监控) | M1 |
-| 12 | 数据看板扩展(小区/门店看板、转化漏斗) | M8 |
-| 13 | 报表系统完善(周报/月报自动汇总) | M8 |
-| 14 | 移动端 H5(企微侧边栏) | Mobile |
-| 15 | 操作日志审计 | M10 |
-| 16 | 自动测试覆盖 | 全局 |
+| 10 | 企微接入管理独立页面(企业授权可视化) | M1 |
+| 11 | 数据看板扩展(小区/门店看板、转化漏斗) | M8 |
+| 12 | 报表系统完善(周报/月报自动汇总) | M8 |
+| 13 | 移动端 H5(企微侧边栏) | Mobile |
+| 14 | 操作日志审计 | M10 |
+| 15 | 自动测试覆盖 | 全局 |
 
 ### ⚪ P3 — 二期规划(暂不启动)
 
 | 序号 | 任务 | 涉及模块 |
 |:----:|------|:--------:|
-| 17 | 竞品与话题词库 + 舆情周报 | M11 |
-| 18 | 小区勘探与户型方案库 | M11 |
-| 19 | 小区开拓进度填报 | M11 |
+| 16 | 竞品与话题词库 + 舆情周报 | M11 |
+| 17 | 小区勘探与户型方案库 | M11 |
+| 18 | 小区开拓进度填报 | M11 |
 
 ---
 
@@ -380,13 +391,14 @@ npm run dev
 | # | 问题 | 严重度 | 现状 |
 |:--:|------|:------:|------|
 | 1 | `addNotification` 不持久化,刷新后通知丢失 | 高 | `NotificationService` 仅写 BehaviorSubject |
-| 2 | notification-center 和 workspace-notifications 数据源不同 | 高 | 两套独立数据流 |
-| 3 | MockDataService 无 CUD 方法,前端无法测试数据变更 | 中 | 只有 generate + get |
-| 4 | 部分组件直接注入 MockDataService 而非抽象 Service 层 | 中 | 切换 API 困难 |
+| 2 | ~~notification-center 和 workspace-notifications 数据源不同~~ | — | ✅ **已缓解**:workspace 路由已停用,仅 notification-center 活跃 |
+| 3 | `pendingWorkOrders` 硬编码为 0 | 中 | Dashboard 未接入 WorkOrder(WorkOrder 表未建) |
+| 4 | 停用模块的大量组件代码未清理(约 25 个组件文件) | 低 | 组件保留以备将来恢复,但增加维护认知负担 |
 | 5 | `AppNotification.actionable` / `actionTaken` 字段从未使用 | 低 | 模型中已定义但无逻辑 |
 | 6 | Settings 页面通知偏好开关未实际生效 | 低 | UI 已有但未接入过滤逻辑 |
 | 7 | 零测试覆盖(前端 1 个 spec,后端 0) | 中 | 缺乏回归保护 |
 | 8 | 无 API 文档(Swagger/OAS) | 低 | 前后端协作效率低 |
+| 9 | ~~MockDataService~~ | — | ✅ **已清理**:MockDataService 已从项目中完全移除 |
 
 ---
 
@@ -395,21 +407,27 @@ npm run dev
 基于依赖关系和业务价值,建议按以下顺序推进:
 
 ```
-阶段 A(当前→1 周):风控闭环
-  └─ 风控 API(Parse 建表 + 关键词 CRUD + WorkOrder CRUD + 扫描建单)
-  └─ 前端风控 4 页面接 API
+✅ 阶段 A(已完成):风控核心闭环
+  └─ 风控数据管道(关键词扫描 + AI 分析 + 三级上报)
+  └─ 前端风控大屏 + 关键词库接 API
+  └─ Dashboard riskEventCount 真实化
+  └─ 前端 masterKey 移除 + 聚合端点替代 N+1
+
+阶段 A'(当前→1 周):风控收尾
+  └─ WorkOrder 工单系统(建表 + CRUD + 自动建单)
+  └─ 前端工单/案例页面恢复 + 接 API
   └─ Dashboard pendingWorkOrders 真实化
 
 阶段 B(1→3 周):合规 + 通知闭环
   └─ 合规检查 API(缺表/格式/预警)
-  └─ 通知系统统一(真实通知生成与推送)
-  └─ 前端合规 + 通知页面接 API
+  └─ 通知系统统一(持久化 + 一致性
+  └─ 前端合规 + 通知页面路由恢复 + 接 API
 
 阶段 C(3→5 周):运营 + 数据
   └─ 内容运营 API(素材库/周计划/群发)
   └─ KOC/意向客户 API(渠道/识别/跟进)
   └─ 经营数据补录 API
-  └─ 前端全部页面接 API
+  └─ 全部前端页面路由恢复 + 接 API
 
 阶段 D(5→8 周):完善 + 报表
   └─ 企微接入管理页面
@@ -425,4 +443,4 @@ npm run dev
 
 ---
 
-*文档版本:v2.0 | 基于 2026-06-10 代码库状态 | M1+M2 完成,Auth 迁移至前端 Parse SDK*
+*文档版本:v3.0 | 基于 2026-06-23 代码库状态 | M5 风控核心闭环已完成,M3/M4/M6-M11 路由停用中*

+ 14 - 13
doc/项目文档/README.md

@@ -1,6 +1,6 @@
 # 拉迷企微客户服务系统 — 项目总览与架构说明
 
-> 更新日期:2026-06-18
+> 更新日期:2026-06-23
 
 ---
 
@@ -70,6 +70,7 @@
 │  · 数据范围过滤 (门店 → 区域 → 全局)                  │
 │  · 通知轮询 (20s 间隔)                               │
 │  · 预警弹窗队列管理                                  │
+│  · 活跃路由 14 个 (M3/M4/M6-M11 路由已停用)          │
 └──────────┬──────────────────────────────────────────┘
@@ -273,17 +274,16 @@ lami/
 │       │   ├── models/           # 类型定义 + 常量
 │       │   └── utils/            # 工具函数 (健康度计算)
 │       ├── features/             # 功能模块
-│       │   ├── dashboard/        # 总览看板
-│       │   ├── workspace/        # 工作台 (通知/工单)
-│       │   ├── group-management/ # 群管理 (列表/详情/健康度/小区)
-│       │   ├── compliance/       # 合规检查 (总览/文档/异常/缺表/格式/报表)
-│       │   ├── risk-control/     # 风控 (大盘/关键词库/工单/案例库)
-│       │   ├── settings/         # 系统设置 (个人资料/企微账号/人员管理)
-│       │   ├── auth/             # 登录/注册
-│       │   ├── acquisition/      # 获客管理 (渠道/意向客户/KOC/样板间)
-│       │   ├── content-ops/      # 内容运营 (素材库/周计划/群发/分析)
-│       │   ├── data-entry/       # 数据录入 (订单/直播/客户)
-│       │   └── reports/          # 报表 (周报/月报)
+│       │   ├── dashboard/        # 总览看板 ✅ 活跃
+│       │   ├── group-management/ # 群管理 (列表/详情/健康度/小区) ✅ 活跃
+│       │   ├── risk-control/     # 风控 (大盘/关键词库) ✅ 活跃
+│       │   ├── settings/         # 系统设置 (个人资料/企微账号/人员管理) ✅ 活跃
+│       │   ├── auth/             # 登录/注册 ✅ 活跃
+│       │   ├── compliance/       # 合规检查 ⚠️ 组件保留,路由停用
+│       │   ├── acquisition/      # 获客管理 ⚠️ 组件保留,路由停用
+│       │   ├── content-ops/      # 内容运营 ⚠️ 组件保留,路由停用
+│       │   ├── data-entry/       # 数据录入 ⚠️ 组件保留,路由停用
+│       │   └── reports/          # 报表 ⚠️ 组件保留,路由停用
 │       └── shared/               # 共享组件 (DataTable, FilterBar, StatCard...)
 ├── backend/src/                  # 项目后端 (前端 API 透传层)
@@ -308,7 +308,7 @@ lami/
 │           ├── health/server.ts  # 服务启动
 │           └── qiwe/
 │               ├── controllers/  # Webhook + Sync 控制器
-│               ├── services/     # 核心服务 (见后文)
+│               ├── services/     # 核心服务
 │               ├── routes/       # 路由
 │               └── models/       # Parse 客户端
@@ -326,3 +326,4 @@ lami/
 | [前端功能模块.md](./前端功能模块.md) | 每个前端功能模块的详细逻辑说明 |
 | [后端服务说明.md](./后端服务说明.md) | 两个后端各自的服务模块逻辑 |
 | [数据库设计文档.md](./数据库设计文档.md) | 数据库表结构、字段说明、字段作用 |
+| [../项目功能模块梳理与进度追踪.md](../项目功能模块梳理与进度追踪.md) | 全模块进度追踪与待办排序 |

+ 76 - 108
doc/项目文档/前端功能模块.md

@@ -1,6 +1,6 @@
 # 前端功能模块说明
 
-> 更新日期:2026-06-18 | 框架:Angular 20 Standalone Components
+> 更新日期:2026-06-23 | 框架:Angular 20 Standalone Components
 
 ---
 
@@ -52,7 +52,7 @@ ParseDataService            /api/users/* (项目后端, masterKey)
 
 #### `auth.guard.ts` — 路由守卫
 - **AuthGuard** — 未登录用户重定向到 `/login`
-- **GuestGuard** — 已登录用户重定向到 `/workspace`
+- **GuestGuard** — 已登录用户重定向到 `/dashboard`
 - **RoleGuard(module)** — 根据用户角色 + 模块权限表决定是否可访问
 
 #### `core/interceptors/api.interceptor.ts` — HTTP 拦截器
@@ -232,24 +232,22 @@ deriveLifecyclePhase(deliveryDate)
 - 支持全局/区域/门店/群 多级数据筛选
 - 选择变更后自动刷新看板数据
 
-### 3.3 工作台 (`features/workspace/`)
+### 3.3 工作台 (`features/workspace/`) ⚠️ 路由已停用
 
-#### `workspace.component.ts` — 工作台首页
+> 📌 workspace 模块的所有路由已于 app.routes.ts 中注释停用,组件代码保留以备将来恢复。通知中心已合并至 ShellBar 顶栏。
+
+#### `workspace.component.ts` — 工作台首页(停用)
 - **角色化欢迎语**:根据用户角色显示不同的欢迎信息和快速操作入口
 - **快速行动卡片**:根据角色权限显示可用的快捷操作
 - **待办任务**:显示当前用户的待处理风险事件
 - **近期活动**:最近的 AppNotification 列表
 
-#### `workspace-notifications.component.ts` — 通知列表
+#### `workspace-notifications.component.ts` — 通知列表(停用)
 - 所有通知列表,按时间倒序
 - 支持按类型/严重度筛选
-- 支持单条标记已读 / 全部已读
-- 点击通知跳转到对应模块
 
-#### `workspace-issues.component.ts` — 工单看板
+#### `workspace-issues.component.ts` — 工单看板(停用)
 - 三列看板布局:待处理 / 处理中 / 已解决
-- 工单卡片显示:标题、严重度、触发群、创建时间
-- 工单状态流转
 
 ### 3.4 群管理 (`features/group-management/`)
 
@@ -299,39 +297,22 @@ Promise.all([
 - 编辑对话框:修改名称、地址、交付日期、户数、均价、关联门店
 - 生命周期自动推导:基于 `deliveryDate` 通过 `deriveLifecyclePhase` 实时计算
 
-### 3.5 合规检查 (`features/compliance/`)
-
-#### `compliance-overview.component.ts` — 合规总览
-- 统计卡:合规率、缺表率、异常率、复查完成率
-- 合规分布图
-- 门店合规率柱状图对比
-
-#### `document-list.component.ts` — 文档登记列表
-- 所有已登记群文档列表
-- 统计卡
+### 3.5 合规检查 (`features/compliance/`) ⚠️ 路由全部停用
 
-#### `document-anomalies.component.ts` — 文档异常
-- 异常文档列表(内容异常、格式异常等)
-- 可筛选异常类型
-- 发送整改通知
-- 批量通知负责人
+> 📌 compliance 模块的所有路由已于 app.routes.ts 中注释停用,组件代码保留。
 
-#### `missing-docs.component.ts` — 缺表群清单
-- 未登记文档的群列表
-- 可通知群负责人
-- 标记已通知/复检/复检失败
-
-#### `format-check.component.ts` — 格式检查
-- 按格式问题类型筛选:格式错误 / 字段缺失 / 内容过简
-- 发送通知给相关群的负责人
+#### `compliance-overview.component.ts` — 合规总览(停用)
+- 统计卡:合规率、缺表率、异常率、复查完成率
 
-#### `compliance-report.component.ts` — 合规报表
-- 门店合规率柱状图
-- 门店合规统计表
+#### `document-list.component.ts` — 文档登记列表(停用)
+#### `document-anomalies.component.ts` — 文档异常(停用)
+#### `missing-docs.component.ts` — 缺表群清单(停用)
+#### `format-check.component.ts` — 格式检查(停用)
+#### `compliance-report.component.ts` — 合规报表(停用)
 
-### 3.6 风控 (`features/risk-control/`)
+### 3.6 风控 (`features/risk-control/`) ✅ 核心模块活跃
 
-#### `risk-dashboard.component.ts` — 预警大屏
+#### `risk-dashboard.component.ts` — 预警大屏 ✅ API 驱动
 **功能**:风控事件的全景管理页面,支持三级上报状态追踪。
 
 - **筛选器**:严重等级 / 事件类型 / 状态 / 时间范围
@@ -341,25 +322,22 @@ Promise.all([
 - **负责人显示**:`assignedToName`/`assignedToPhone` 优先从 `_User` 表实时查询(通过 `assignedTo` userId 匹配),RiskEvent 自身快照字段作为兜底
 - **状态流转**:pending → (接单调用 `POST /api/qiwe/risk-event/:id/confirm`) → processing → resolved / false_alarm
 - **数据查询**:`RiskEvent` 查询时 `include('store', 'district')`,按用户角色范围过滤(`e.store?.id` / `e.district?.id`)
+- **数据来源**:`parseData.listRiskEvents()` 直连 Parse RiskEvent 表
 
-#### `keywords.component.ts` — 关键词库
+#### `keywords.component.ts` — 关键词库 ✅ API 驱动
 **功能**:管理风险检测的关键词库。
 
 - 关键词列表:词文本 / 类别(8 个类别)/ 严重等级 / 启用状态
-- **CRUD 操作**:添加 / 编辑 / 删除
-- **启用/禁用**:一键切换生效状态
+- **CRUD 操作**:添加 / 编辑 / 删除(`saveRiskKeyword` / `deleteRiskKeyword`)
+- **启用/禁用**:一键切换生效状态(`toggleRiskKeyword`)
 - **分类筛选**:按 complaint/quality/sensitive/legal/competitor/price/negative/installation 筛选
-- **数据来源**:`RiskKeyword` Parse 表(后端每 5 分钟从该表热加载到 Aho-Corasick 自动机)
+- **数据来源**:`parseData.listRiskKeywords()` 直连 Parse RiskKeyword 表(后端每 5 分钟从该表热加载到 Aho-Corasick 自动机)
 
-#### `work-orders.component.ts` — 预警工单
-- 工单列表:筛选(状态/优先级/负责人)
-- 展开详情:工单描述、风险事件关联、处理日志
-- 操作:解决 (resolve) / 关闭 (close)
-- 处理人和截止时间设置
+#### `work-orders.component.ts` — 预警工单 ⚠️ 路由停用
+- 组件存在,路由已注释(WorkOrder 系统尚未实现)
 
-#### `cases.component.ts` — 案例知识库
-- 仅展示已解决 (resolved) 或误报 (false_alarm) 的事件
-- 作为历史案例参考库
+#### `cases.component.ts` — 案例知识库 ⚠️ 路由停用
+- 组件存在,路由已注释
 
 ### 3.7 系统设置 (`features/settings/`)
 
@@ -423,58 +401,38 @@ director → '总监'
 
 **数据读取**:`_User` 表 include store/district Pointer,展开得到 storeName/districtName。`ResponsibilityAssignment` 表通过 `user` Pointer 关联 `_User`。
 
-### 3.8 获客管理 (`features/acquisition/`)
-
-#### `channel-management.component.ts` — 拉群渠道管理
-- 渠道 CRUD:名称、类型、状态
-- 渠道数据本地管理(localStorage)
+### 3.8 获客管理 (`features/acquisition/`) ⚠️ 路由全部停用
 
-#### `intent-leads.component.ts` — 意向客户
-- 意向客户列表管理
-- 跟进任务关联
+> 📌 acquisition 模块的所有路由已停用。
 
-#### `koc-management.component.ts` — KOC 管理
-- 关键意见客户(KOC)信息管理
-- 数据本地管理(localStorage)
+#### `channel-management.component.ts` — 拉群渠道管理(停用)
+#### `intent-leads.component.ts` — 意向客户(停用)
+#### `koc-management.component.ts` — KOC 管理(停用)
+#### `showroom-targets.component.ts` — 样板间目标(停用)
 
-#### `showroom-targets.component.ts` — 样板间目标
-- 样板间目标设置与追踪
-- 数据本地管理(localStorage)
+### 3.9 内容运营 (`features/content-ops/`) ⚠️ 路由全部停用
 
-### 3.9 内容运营 (`features/content-ops/`)
+> 📌 content-ops 模块的所有路由已停用。
 
-#### `material-library.component.ts` — 素材库
-- 运营素材的上传、分类、检索
-- ContentMaterial 模型管理
+#### `material-library.component.ts` — 素材库(停用)
+#### `weekly-plan.component.ts` — 周度运营计划(停用)
+#### `batch-send.component.ts` — 批量群发(停用)
+#### `content-analytics.component.ts` — 内容分析(停用)
 
-#### `weekly-plan.component.ts` — 周度运营计划
-- 周计划创建与查看
-- WeeklyPlan / PlanItem 模型管理
+### 3.10 数据录入 (`features/data-entry/`) ⚠️ 路由全部停用
 
-#### `batch-send.component.ts` — 批量群发
-- 批量向多个群发送消息
+> 📌 data-entry 模块的所有路由已停用。
 
-#### `content-analytics.component.ts` — 内容分析
-- 素材使用统计、效果分析
+#### `order-entry.component.ts` — 订单录入(停用)
+#### `live-entry.component.ts` — 直播数据录入(停用)
+#### `customer-entry.component.ts` — 客户数据录入(停用)
 
-### 3.10 数据录入 (`features/data-entry/`)
+### 3.11 报表 (`features/reports/`) ⚠️ 路由全部停用
 
-#### `order-entry.component.ts` — 订单录入
-- 经营订单数据录入表单
+> 📌 reports 模块的所有路由已停用。
 
-#### `live-entry.component.ts` — 直播数据录入
-- 直播活动数据录入表单
-
-#### `customer-entry.component.ts` — 客户数据录入
-- 客户信息录入表单
-
-### 3.11 报表 (`features/reports/`)
-
-#### `weekly-report.component.ts` — 周报
-- 周度运营数据汇总报表
-
-#### `monthly-report.component.ts` — 月报
-- 月度运营数据汇总报表
+#### `weekly-report.component.ts` — 周报(停用)
+#### `monthly-report.component.ts` — 月报(停用)
 
 ---
 
@@ -520,27 +478,37 @@ export const MSQParse = FmodeParse.initialize({
 
 ## 六、路由结构
 
+> 📌 2026-06 更新:M3/M4/M6/M7/M8(部分)/M9/M10 路由已全部注释停用,聚焦 M1/M2/M5 核心闭环。当前共 14 个活跃路由。
+
+### 活跃路由
+
 | 路径 | 组件 | 说明 |
 |------|------|------|
-| `/login` | LoginComponent | 登录(GuestGuard:已登录则跳转工作台) |
-| `/register` | RegisterComponent | 注册 |
-| `/workspace` | WorkspaceComponent | 工作台首页(默认路由) |
-| `/workspace/notifications` | WorkspaceNotificationsComponent | 通知列表 |
-| `/workspace/issues` | WorkspaceIssuesComponent | 工单看板 |
-| `/dashboard` | DashboardComponent | 总览看板 |
+| `/login` | LoginComponent | 登录(GuestGuard) |
+| `/register` | RegisterComponent | 注册(GuestGuard) |
+| `/pending` | PendingApprovalComponent | 待分配提示 |
+| `/dashboard` | DashboardComponent | 总览看板(默认路由) |
 | `/groups` | GroupListComponent | 客户群列表 |
 | `/groups/health` | GroupHealthComponent | 健康度看板 |
 | `/groups/:id` | GroupDetailComponent | 群详情(5 标签页) |
 | `/communities` | CommunityListComponent | 小区档案 |
-| `/compliance` | ComplianceOverviewComponent | 合规总览 |
-| `/compliance/missing-docs` | MissingDocsComponent | 缺表群 |
-| `/compliance/format-check` | FormatCheckComponent | 格式检查 |
-| `/compliance/report` | ComplianceReportComponent | 合规报表 |
-| `/risk-control` | RiskDashboardComponent | 风控大屏 |
-| `/risk-control/keywords` | KeywordsComponent | 关键词库 |
-| `/risk-control/work-orders` | WorkOrdersComponent | 预警工单 |
-| `/risk-control/cases` | CasesComponent | 案例知识库 |
-| `/settings` | SettingsComponent | 系统设置 |
+| `/risk-control` | RiskDashboardComponent | 风控大屏 ✅ API 驱动 |
+| `/risk-control/keywords` | KeywordsComponent | 关键词库 ✅ API 驱动 |
+| `/settings` | SettingsComponent | 系统设置(director only) |
 | `/settings/profile` | ProfileComponent | 个人资料 |
-| `/settings/qiwe-accounts` | QiweAccountsComponent | 企微账号管理 |
-| `/staff` | StaffManagementComponent | 人员管理 |
+| `/settings/qiwe-accounts` | QiweAccountsComponent | 企微账号管理(director only) |
+| `/staff` | StaffManagementComponent | 人员与权限 |
+
+### 已停用路由(组件保留,路由注释)
+
+| 原路径 | 所属模块 | 停用原因 |
+|--------|---------|---------|
+| `/workspace` 及子路由 | M10 | 合并至 ShellBar 通知中心 |
+| `/compliance` 及子路由 | M4 | 等待合规需求确认 |
+| `/documents` 及子路由 | M3 | 等待企微文档读取能力确认 |
+| `/risk-control/work-orders` | M5 | WorkOrder 系统尚未实现 |
+| `/risk-control/cases` | M5 | 等待工单系统完成后恢复 |
+| `/content` 及子路由 | M6 | 等待运营需求确认 |
+| `/acquisition` 及子路由 | M7 | 等待业务需求确认 |
+| `/data-entry` 及子路由 | M9 | 等待表单字段确认 |
+| `/reports` 及子路由 | M8 | 等待报表格式确认 |

+ 1 - 1
doc/项目文档/后端服务说明.md

@@ -1,6 +1,6 @@
 # 后端服务说明
 
-> 更新日期:2026-06-18
+> 更新日期:2026-06-23
 
 ---
 

+ 1 - 1
doc/项目文档/数据库设计文档.md

@@ -1,6 +1,6 @@
 # 数据库设计文档
 
-> 更新日期:2026-06-18 | 数据库:Parse Server (Back4App/MongoDB) | App ID: `lami-ai`
+> 更新日期:2026-06-23 | 数据库:Parse Server (Back4App/MongoDB) | App ID: `lami-ai`
 
 ---
 

+ 14 - 0
src/app/core/services/api/parse-data.service.ts

@@ -1152,6 +1152,20 @@ export class ParseDataService {
     return results.map((o: any) => this.mapRiskEvent(o, userMap));
   }
 
+  /** 按群组 roomId 查询该群的历史风控事件 */
+  async listRiskEventsByGroup(roomId: string): Promise<RiskEvent[]> {
+    const q = new MSQParse.Query('RiskEvent');
+    q.equalTo('groupId', roomId);
+    q.include('store');
+    q.include('district');
+    q.descending('createdAt');
+    q.limit(100);
+    const results = await q.find() as any[];
+    const userIds = results.map((o: any) => parseGet(o, 'assignedTo', '')).filter(Boolean);
+    const userMap = await this.batchLookupUsers(userIds);
+    return results.map((o: any) => this.mapRiskEvent(o, userMap));
+  }
+
   /** 统一映射 RiskEvent Parse Object → 前端接口 */
   private mapRiskEvent(o: any, userMap?: Map<string, { name: string; phone: string }>): RiskEvent {
     const store = o.get('store');

+ 2 - 1
src/app/features/dashboard/dashboard.component.html

@@ -69,7 +69,7 @@
     (cardClick)="navigateTo('/reports/weekly')"
   />
   <app-stat-card
-    title="活跃风险事件"
+    title="待处理风险事件"
     [value]="stats.riskEventCount"
     unit="起"
     [color]="stats.riskEventCount > 5 ? 'critical' : 'info'"
@@ -174,6 +174,7 @@
       [showPagination]="false"
       [showSearch]="false"
       [rowClickable]="false"
+      [maxHeight]="'280px'"
       [actionsAlwaysVisible]="true"
       (actionClick)="onRiskEventClick($event.row)"
     />

+ 2 - 2
src/app/features/dashboard/dashboard.component.ts

@@ -200,8 +200,8 @@ export class DashboardComponent implements OnInit {
     this.loading = false;
   }
 
-  protected onRiskEventClick(_event: RiskEvent): void {
-    this.router.navigate(['/risk-control']);
+  protected onRiskEventClick(event: RiskEvent): void {
+    this.router.navigate(['/risk-control'], { queryParams: { eventId: event.id } });
   }
 
   protected onGroupClick(event: Group): void {

+ 10 - 2
src/app/features/group-management/group-detail/group-detail.component.html

@@ -266,14 +266,22 @@
     }
 
     @if (activeTab === 'risk') {
-      @if (riskEvents.length > 0) {
+      @if (riskEventsLoading) {
+        <div class="flex items-center justify-center py-12 text-surface-400">
+          <fa-icon [icon]="['fas','spinner']" animation="spin" class="text-xl mr-2" />
+          <span>加载中...</span>
+        </div>
+      } @else if (riskEventsError) {
+        <app-message-strip type="error" [message]="riskEventsError" />
+      } @else if (riskEvents.length > 0) {
         <app-data-table
           [data]="riskEvents"
           [columns]="riskColumns"
           [loading]="false"
           [showSearch]="false"
           [showPagination]="false"
-          [rowClickable]="false"
+          [rowClickable]="true"
+          (rowClick)="viewRiskEvent($event.id)"
         />
       } @else {
         <app-empty-state title="暂无风控事件" description="该群近期无风控事件" [icon]="['fas','shield']" />

+ 22 - 0
src/app/features/group-management/group-detail/group-detail.component.ts

@@ -51,6 +51,8 @@ export class GroupDetailComponent implements OnInit {
 
   documents: Document[] = [];
   riskEvents: RiskEvent[] = [];
+  riskEventsLoading = false;
+  riskEventsError = '';
   messages: MessageDto[] = [];
   messagesTotal = 0;
   chatMessagesTotal = 0;
@@ -259,6 +261,9 @@ export class GroupDetailComponent implements OnInit {
     if (key === 'members' && !this.membersLoading) {
       void this.loadMembers(this.group.id);
     }
+    if (key === 'risk' && !this.riskEventsLoading) {
+      void this.loadRiskEvents();
+    }
   }
 
   private applyMemberInsights(): void {
@@ -324,6 +329,23 @@ export class GroupDetailComponent implements OnInit {
     this.messagesLoading = false;
   }
 
+  private async loadRiskEvents(): Promise<void> {
+    if (!this.group) return;
+    this.riskEventsLoading = true;
+    this.riskEventsError = '';
+    try {
+      this.riskEvents = await this.parseData.listRiskEventsByGroup(this.group.roomId);
+    } catch {
+      this.riskEvents = [];
+      this.riskEventsError = '加载风控事件失败';
+    }
+    this.riskEventsLoading = false;
+  }
+
+  viewRiskEvent(eventId: string): void {
+    this.router.navigate(['/risk-control'], { queryParams: { eventId } });
+  }
+
   loadMoreMessages(): void {
     if (!this.group || this.messagesLoading) return;
     this.messagesSkip += this.messagesPageSize;

+ 1 - 0
src/app/features/risk-control/keywords/keywords.component.html

@@ -89,6 +89,7 @@
         [loading]="false"
         [pageSize]="15"
         [showSearch]="true"
+        (linkClick)="onKeywordLinkClick($event)"
       />
     }
   </div>

+ 15 - 4
src/app/features/risk-control/keywords/keywords.component.ts

@@ -52,7 +52,8 @@ export class KeywordsComponent implements OnInit {
     { key: 'categoryLabel', label: '分类', sortable: true },
     { key: 'severity', label: '严重程度', template: 'status' as const },
     { key: 'enabled', label: '状态', template: 'status' as const },
-    { key: 'createdAt', label: '创建时间', sortable: true }
+    { key: 'createdAt', label: '创建时间', sortable: true },
+    { key: 'toggleLabel', label: '操作', template: 'link' as const, width: '80px' }
   ];
 
   readonly categoryOptions = [
@@ -140,17 +141,27 @@ export class KeywordsComponent implements OnInit {
     this.newKeyword.set({ word: '', category: 'complaint', severity: 'medium' });
   }
 
+  onKeywordLinkClick(event: { row: RiskKeyword; columnKey: string }): void {
+    if (event.columnKey === 'toggleLabel') {
+      this.toggleEnabled(event.row);
+    }
+  }
+
   toggleEnabled(keyword: RiskKeyword): void {
     const newEnabled = !keyword.enabled;
     this.parseData.toggleRiskKeyword(keyword.id, newEnabled).then(() => {
       this.keywords.update(list =>
-        list.map(k => k.id === keyword.id ? { ...k, enabled: newEnabled } : k)
+        list.map(k => k.id === keyword.id ? { ...k, enabled: newEnabled, toggleLabel: newEnabled ? '停用' : '启用' } : k)
       );
     }).catch(() => {});
   }
 
-  private mapKeywords(kws: RiskKeyword[]): (RiskKeyword & { categoryLabel: string })[] {
-    return kws.map(k => ({ ...k, categoryLabel: this.getCategoryLabel(k.category) }));
+  private mapKeywords(kws: RiskKeyword[]): (RiskKeyword & { categoryLabel: string; toggleLabel: string })[] {
+    return kws.map(k => ({
+      ...k,
+      categoryLabel: this.getCategoryLabel(k.category),
+      toggleLabel: k.enabled ? '停用' : '启用',
+    }));
   }
 
   getCategoryLabel(category: string): string {