文档版本:2026-05-30
适用范围:backend/+ Angular 前端风控相关页面
背景:前端「问题分类看板」「预警工单」「风控总览」等页面当前使用MockDataService假数据;后端尚无对应 API。
| 模块 | 状态 |
|---|---|
GroupChat / Message / Webhook 收消息 |
✅ 已实现 |
四级权限 + data-scope.service.ts |
✅ 已实现 |
| 总览看板(群、生命周期、健康评级) | ✅ 已接后端 |
| 登录 / 群列表 / 小区 / 组织 | ✅ 已接后端 |
| 模块 | 状态 |
|---|---|
RiskKeyword / RiskEvent / WorkOrder Parse 表 |
❌ 未建 |
/api/risk/* 路由与业务服务 |
❌ 未建 |
总览 pendingWorkOrders、riskEventCount |
❌ 写死为 0 |
| Webhook 消息 → 关键词扫描 → 自动建单 | ❌ 未做 |
| 路由 | 组件 | 当前数据来源 |
|---|---|---|
/workspace/issues |
问题分类看板 | mockData.getWorkOrders() |
/risk-control/work-orders |
预警工单列表 | mockData.getWorkOrders() |
/risk-control |
风控总览 | mockData.getRiskEvents() |
/risk-control/keywords |
关键词库 | mockData.getRiskKeywords() |
| 工作台「未处理工单」数量 | workspace | mockData.getWorkOrders() 过滤 |
结论:不是前端故意造假,而是后端接口尚未实现,前端用 Mock 撑 UI 演示。
定义位置:src/app/core/models/index.ts
interface RiskEvent {
id: string;
groupId: string; // 对应 GroupChat.roomId
groupName: string;
communityName: string;
type: 'keyword' | 'member_drop' | 'inactive' | 'multi_doc' | 'other';
severity: 'high' | 'medium' | 'low';
title: string;
description: string;
keywords?: string[];
status: 'pending' | 'processing' | 'resolved' | 'false_alarm';
assignedTo?: string;
assignedToName?: string;
createdAt: Date;
resolvedAt?: Date;
resolution?: string;
}
interface WorkOrder {
id: string;
riskEventId: string;
title: string;
description: string;
groupName: string;
communityName: string;
assignedTo: string;
assignedToName: string;
priority: 'high' | 'medium' | 'low';
status: 'open' | 'in_progress' | 'resolved' | 'closed';
createdAt: Date;
deadline: Date;
resolvedAt?: Date;
resolution?: string;
resolutionLog: string[];
}
interface RiskKeyword {
id: string;
word: string;
category: 'complaint' | 'competitor' | 'sensitive' | 'price' | 'other';
severity: 'high' | 'medium' | 'low';
enabled: boolean;
createdAt: Date;
}
在 src/shared/db/schema-setup.ts 的 ensureSchemas() 中追加。
| 字段 | 类型 | 说明 |
|---|---|---|
word |
String | 关键词,如「欧派」「投诉」 |
category |
String | complaint / competitor / sensitive / price / other |
severity |
String | high / medium / low |
enabled |
Boolean | 是否启用 |
createdBy |
String | 创建人 AppUser.id(可选) |
启动种子(与前端 Mock 一致)
| word | category | severity |
|---|---|---|
| 投诉 | complaint | high |
| 欧派 | competitor | medium |
| 索菲亚 | competitor | medium |
| 退群 | sensitive | high |
| 太贵了 | price | low |
| 字段 | 类型 | 说明 |
|---|---|---|
roomId |
String | 群 ID,关联 GroupChat.roomId |
groupName |
String | 冗余群名 |
communityId |
String | 可选 |
communityName |
String | 冗余小区名 |
storeId |
String | 权限过滤,从 GroupChat 复制 |
type |
String | keyword / member_drop / inactive / multi_doc / other |
severity |
String | high / medium / low |
title |
String | 预警标题 |
description |
String | 详情 |
keywords |
Array | 命中的词列表 |
messageId |
String | 触发消息 ID(可选) |
status |
String | pending / processing / resolved / false_alarm |
assignedTo |
String | AppUser.id |
assignedToName |
String | 冗余姓名 |
workOrderId |
String | 关联 WorkOrder objectId |
resolvedAt |
Date | 结案时间 |
resolution |
String | 处理说明 |
guid |
String | 企微实例 GUID |
| 字段 | 类型 | 说明 |
|---|---|---|
riskEventId |
String | 关联 RiskEvent objectId |
roomId |
String | 群 ID |
groupName |
String | 群名 |
communityName |
String | 小区名 |
storeId |
String | 权限过滤 |
title |
String | 工单标题 |
description |
String | 描述 |
assignedTo |
String | AppUser.id |
assignedToName |
String | 负责人姓名 |
priority |
String | high / medium / low |
status |
String | open / in_progress / resolved / closed |
deadline |
Date | 截止时间 |
resolvedAt |
Date | 完成时间 |
resolution |
String | 处理结果 |
resolutionLog |
Array | 操作日志(字符串数组) |
createdBy |
String | 系统或操作人 AppUser.id |
| 字段 | 类型 | 说明 |
|---|---|---|
key |
String | 如 member_drop_24h |
value |
Number | 如 8(24h 退群 ≥8 触发) |
enabled |
Boolean | 是否启用 |
description |
String | 说明 |
默认种子
| key | value | 说明 |
|---|---|---|
member_drop_24h |
8 | 24 小时内退群人数阈值 |
inactive_days |
5 | 连续无互动天数 |
路由挂载:app.use('/api/risk', riskRoutes)
目录建议:src/apps/pc/risk/
除 Webhook 内部调用外,以下接口均需 Authorization: Bearer <token>。
| 方法 | 路径 | 说明 | 角色 |
|---|---|---|---|
| GET | /api/risk/keywords |
列表,?category=&enabled=true |
总监/督导/店长 |
| POST | /api/risk/keywords |
新增 | 总监/督导 |
| PATCH | /api/risk/keywords/:id |
修改 severity / enabled | 总监/督导 |
| DELETE | /api/risk/keywords/:id |
删除 | 总监 |
GET 响应示例
{
"success": true,
"data": {
"keywords": [
{
"id": "xxx",
"word": "欧派",
"category": "competitor",
"severity": "medium",
"enabled": true,
"createdAt": "2026-05-30T00:00:00.000Z"
}
],
"total": 5
},
"error": null
}
POST body
{
"word": "欧派",
"category": "competitor",
"severity": "medium"
}
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/risk/events |
列表 + 筛选 |
| GET | /api/risk/events/:id |
详情 |
| PATCH | /api/risk/events/:id |
更新状态 / 指派 / 误报 |
查询参数
| 参数 | 说明 |
|---|---|
severity |
high / medium / low |
type |
keyword / member_drop / inactive / multi_doc / other |
status |
pending / processing / resolved / false_alarm |
scopeLevel |
global / region / store(与群列表一致) |
storeId |
scopeLevel=store 时 |
PATCH body 示例
{
"status": "processing",
"assignedTo": "<AppUserId>",
"resolution": "已联系群主处理"
}
GET 响应示例
{
"success": true,
"data": {
"events": [
{
"id": "re1",
"groupId": "10865515454476837",
"groupName": "xxx业主群",
"communityName": "xxx",
"type": "keyword",
"severity": "high",
"title": "群内出现竞品名称提及",
"description": "命中关键词:欧派",
"keywords": ["欧派"],
"status": "pending",
"assignedToName": "李店长",
"createdAt": "2026-05-30T09:00:00.000Z"
}
],
"total": 1,
"scope": { "level": "store", "storeId": "...", "storeName": "上海总部" }
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/risk/work-orders |
看板 / 列表 |
| GET | /api/risk/work-orders/:id |
详情 |
| POST | /api/risk/work-orders |
手工建单(可选) |
| PATCH | /api/risk/work-orders/:id |
改状态、写处理记录 |
| POST | /api/risk/work-orders/:id/resolve |
标记已解决 |
| POST | /api/risk/work-orders/:id/close |
关闭工单 |
查询参数
| 参数 | 说明 |
|---|---|
status |
open / in_progress / resolved / closed |
priority |
high / medium / low |
q |
标题 / 群名搜索 |
scopeLevel / storeId |
数据范围(服务端校验) |
GET 响应示例
{
"success": true,
"data": {
"workOrders": [
{
"id": "wo1",
"riskEventId": "re1",
"title": "群内出现竞品名称提及",
"description": "系统自动检测到异常,请相关人员进行确认处理。",
"groupName": "xxx业主群",
"communityName": "xxx",
"assignedTo": "userId",
"assignedToName": "李店长",
"priority": "high",
"status": "open",
"createdAt": "2026-05-30T09:00:00.000Z",
"deadline": "2026-06-02T09:00:00.000Z",
"resolutionLog": ["系统自动生成工单"]
}
],
"total": 3,
"counts": {
"open": 1,
"in_progress": 2,
"resolved": 4,
"closed": 0
},
"scope": { "level": "store", "storeId": "...", "storeName": "上海总部" }
}
}
POST resolve / close
无 body 或可选:
{
"resolution": "已联系当事人处理完毕"
}
响应更新 status、resolvedAt、resolution、resolutionLog。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/risk/overview |
待处理数、severity/type 分布 |
供 /risk-control 风控总览页图表使用;也可合并进 GET /api/risk/events 的聚合字段。
响应示例
{
"success": true,
"data": {
"pendingEvents": 5,
"openWorkOrders": 3,
"severityDistribution": { "high": 2, "medium": 2, "low": 1 },
"typeDistribution": { "keyword": 3, "member_drop": 1, "inactive": 1 },
"scope": { "level": "store", "storeId": "..." }
}
}
GET /api/dashboard/overview
将 dashboard.service.ts 中写死的:
riskEventCount: 0,
pendingWorkOrders: 0,
recentRiskEvents: [],
改为真实查询(在当前 scope 下):
| 字段 | 计算规则 |
|---|---|
pendingWorkOrders |
WorkOrder.status in (open, in_progress) 计数 |
riskEventCount |
RiskEvent.status in (pending, processing) 计数 |
recentRiskEvents |
最近 5 条 RiskEvent(可选,需扩展 DTO) |
backend/src/apps/pc/risk/
├── models/
│ └── risk.types.ts # DTO,对齐前端 interface
├── services/
│ ├── keyword.service.ts # 关键词 CRUD + 种子
│ ├── risk-event.service.ts # 预警 CRUD、创建、列表
│ ├── work-order.service.ts # 工单 CRUD、状态机、resolutionLog
│ ├── risk-scan.service.ts # 消息扫描、阈值检测(阶段 B)
│ └── risk-scope.util.ts # 按 storeId/region/owner 过滤
├── controllers/
│ ├── keyword.controller.ts
│ ├── risk-event.controller.ts
│ └── work-order.controller.ts
└── routes/
└── risk.routes.ts
需修改的现有文件
| 文件 | 变更 |
|---|---|
src/shared/db/schema-setup.ts |
新增 4 表字段 |
src/index.ts 或 organization-bootstrap |
关键词种子、可选演示工单种子 |
src/apps/pc/app.ts |
app.use('/api/risk', riskRoutes) |
src/apps/pc/dashboard/services/dashboard.service.ts |
真实 pendingWorkOrders / riskEventCount |
src/apps/pc/qiwe/services/webhook.service.ts |
消息入库后调用 risk-scan(阶段 B) |
参考实现(逻辑可迁移,字段需按本文对齐)
lami-base-v1/backend/src/apps/pc/risk/services/risk.service.ts
复用已有模块:
| 模块 | 路径 |
|---|---|
| 鉴权 | src/shared/auth/require-user.ts |
| 范围解析 | src/shared/auth/data-scope.service.ts |
过滤规则
RiskEvent、WorkOrder 创建时从 GroupChat 复制 storeId、groupName、communityName。global:不过滤 storeregion:storeId in scope.storeIdsstore:storeId = scope.storeIdsingle_group:在 store 基础上,可选仅 roomId 属于 ownerWecomId 对应群(与群列表一致)。| 角色 | 可见工单范围 |
|---|---|
| 总监 | 全局 / 区域 / 门店(可选) |
| 区域督导 | 区域内门店 |
| 店长 | 本店 |
| 单群 | 本店 + 本人负责群(可选) |
关键词管理:建议仅 director、regional_supervisor 可写;store_manager 只读。
Webhook 收到群消息
→ 保存 Message(已有)
→ riskScanService.scanText(content, roomId, messageId)
→ 命中 RiskKeyword(enabled=true)
→ createRiskEvent(type=keyword, severity=关键词级别)
→ createWorkOrder(status=open)
→ RiskEvent.workOrderId 回写
→ resolutionLog 追加「系统自动生成工单」
Webhook 成员退群 / 定时任务
→ 更新 GroupChat.memberChange24h
→ 超过 RiskConfig.member_drop_24h
→ createRiskEvent(type=member_drop)
→ createWorkOrder
定时任务(如每小时)
→ 群今日消息 = 0 且连续 inactive_days 天
→ createRiskEvent(type=inactive)
→ createWorkOrder
| 场景 | assignee |
|---|---|
GroupChat.ownerId 匹配某 AppUser.wecomUserId |
该单群运营 |
| 否则 | 该群 storeId 对应店长(role=store_manager) |
| 无店长 | 区域督导(regionCode 匹配 Store.region) |
deadline 建议:创建时间 + 72 小时(可配置)。
后端完成后,前端需修改:
| 前端文件 | 变更 |
|---|---|
新建 src/app/core/services/api/risk-api.service.ts |
封装上述 API |
workspace-issues.component.ts |
改调 risk-api.getWorkOrders() |
work-orders.component.ts |
列表 + resolve/close 调 API |
risk-dashboard.component.ts |
改调 risk-api.getEvents() |
keywords.component.ts |
改调 keywords CRUD |
workspace.component.ts |
待处理工单数量改调 API |
dashboard.component.ts |
已接 API 时 pendingWorkOrders 来自 overview |
无数据时:显示 EmptyState,不再回退 Mock。
GET /api/risk/work-orders、GET /api/risk/events(只读)+ 数据范围dashboard.service.ts 真实 pendingWorkOrders交付标准:看板无 Mock;没数据时空状态。
GET /api/risk/overview 统计RiskCase(二期)| # | 用例 | 预期 |
|---|---|---|
| 1 | 未登录 GET /api/risk/work-orders |
401 |
| 2 | 总监 scopeLevel=global |
返回全部可见工单 |
| 3 | 店长登录 | 仅本店;带其他 storeId 403 |
| 4 | 总览 pendingWorkOrders |
与 open+in_progress 工单数一致 |
| 5 | 看板三列计数 | 与 API counts 一致 |
| 6 | Webhook 发送含「欧派」消息 | 自动 1 条 RiskEvent + 1 条 WorkOrder |
| 7 | POST resolve | status→resolved,resolutionLog 追加 |
| 8 | 前端问题分类看板 | 不再出现「王运营」等 Mock 固定人名(除非真实指派) |
| 业务模块 | 功能清单描述 | 本文覆盖 |
|---|---|---|
| 模块 5 群风控 | 风险关键词库 | §3.1、§4.1 |
| 模块 5 | 敏感词命中预警 | §7.1 |
| 模块 5 | 人数骤降阈值异常 | §3.4、§7.2 |
| 模块 5 | 预警自动生成工单 | §7.1 |
| 模块 5 | 工单处理闭环 | §4.3 resolve/close |
| 模块 10 | 问题分类看板 | §4.3、§8 |
| 模块 8 数据看板 | 待处理工单指标 | §4.5 |
鉴权 → src/shared/auth/require-user.ts
数据范围 → src/shared/auth/data-scope.service.ts
Webhook 消息 → src/apps/pc/qiwe/services/webhook.service.ts
总览(待改) → src/apps/pc/dashboard/services/dashboard.service.ts
前端工单 Mock → src/app/core/services/mock-data.service.ts (generateWorkOrders)
前端模型 → src/app/core/models/index.ts (RiskEvent, WorkOrder, RiskKeyword)
旧项目参考 → lami-base-v1/backend/src/apps/pc/risk/
文档维护:风控模块开发完成后请同步更新验收用例与 API 示例。