# 官网编辑输入升级、关键词搜索与访问监控实现方案
## 1. 文档目标与范围
本方案面向当前仓库架构(`portal_website_edit` + `portal_website` + `ruoyi-portal` + `hw_web.sql`),输出三项能力的完整落地设计:
1. 将编辑后台模板组件文本输入快速升级为富文本或 Markdown 输入。
2. 基于当前重点表(`hw_web_menu`、`hw_web`、`hw_web1`、`hw_web_menu1`、`hw_web_document`)实现前后端关键词搜索。
3. 实现官网匿名访问监控(PV/UV/访问路径/来源/停留等)。
说明:本文件是“实现方案与代码位点设计”,不直接修改业务代码。
---
## 2. 需求清单
## 2.1 功能需求
1. 编辑器输入升级
- 支持 `纯文本`、`富文本(HTML)`、`Markdown` 三种输入模式。
- 兼容历史 `webJsonString` 数据,不影响已发布页面渲染。
- 支持逐组件/逐字段平滑迁移,不要求一次性改完全部 `editEl` 组件。
2. 关键词搜索
- 用户输入关键词后可搜索菜单、页面内容、产品详情、资料下载。
- 返回结果需标识内容类型、命中位置、跳转路由参数。
- 支持分页、排序(相关度优先)、高亮摘要。
3. 官网访问监控(非登录)
- 采集匿名访问行为:页面访问、停留时长、来源、设备、下载、搜索。
- 输出基础看板:PV、UV、入口页、跳出率、热门页面、热门关键词。
- 支持反爬/刷量基础治理。
## 2.2 非功能需求
1. 安全
- 富文本输出必须经过 XSS 清洗。
- 搜索接口必须防注入、限流、防超长关键词。
- 监控数据脱敏存储(IP 哈希、最小化隐私采集)。
2. 性能
- 搜索接口 P95 < 300ms(初期中小数据量)。
- 埋点上报不阻塞页面渲染,采用异步/批量上报。
3. 可维护性
- 输入升级采用“统一组件 + 统一协议”模式,避免 21 个模板重复改造。
- 搜索结果统一 DTO,前端只做渲染不做多形态兼容。
- 监控事件采用统一事件字典,便于扩展。
---
## 3. 对现有架构与代码的理解(基于当前仓库结构)
## 3.1 前端编辑端 `portal_website_edit`
1. 关键入口
- `portal_website_edit/src/views/editPage/index.vue`
- `portal_website_edit/src/views/productCenter/edit/index.vue`
2. 模板组件
- `portal_website_edit/src/components/editEl/` 下 `carousel.vue`、`editEl1.vue` ... `editEl16.vue`、`classicCase.vue`、`productCenter.vue`
- 当前大量使用 `contenteditable + blur + 直接写 this.$props.data`
3. 数据模型
- 保存时统一 `JSON.stringify(components)` 写入后端 `webJsonString`
- 后端持久层主要是 `hw_web`、`hw_web1`
4. 请求与鉴权
- `portal_website_edit/src/utils/request.js` 统一请求封装
- Cookie 中 `Admin-Token` 注入 Bearer
## 3.2 展示端 `portal_website`
1. 渲染模式
- 基于菜单树 + JSON 区块渲染
- `type = 1..16` 映射 `components/el/*`
2. 数据来源
- `/portal/hwWebMenu/*`
- `/portal/hwWeb/*`、`/portal/hwWeb1/*`
- `/portal/hwWebDocument/*`
## 3.3 后端与数据库
1. 后端
- Spring Boot + MyBatis,多模块 RuoYi 结构
- `ruoyi-portal` 负责门户内容接口
2. 重点表
- `hw_web_menu`:菜单树
- `hw_web`:通用页面 JSON
- `hw_web1`:产品详情 JSON
- `hw_web_menu1`:另一套菜单树(当前编辑端未重点使用)
- `hw_web_document`:下载文档与密钥
3. 当前特征
- 逻辑删除字段 `is_delete='0'` 才是有效数据
- `hw_web/hw_web1` 服务层版本化写入(旧记录逻辑删除 + 插入新记录)
- 无唯一索引保障业务键唯一(存在并发竞态风险)
---
## 4. 总体落地策略
采用“三阶段并行可拆分实施”:
1. 阶段 A(低风险快上线)
- 输入升级先实现统一组件和数据协议,逐组件接入。
- 搜索先做 SQL `LIKE + UNION ALL + 评分` 版本。
- 监控先做基础埋点与日报表。
2. 阶段 B(质量增强)
- 输入渲染加完整 XSS 策略、回填协议迁移工具。
- 搜索引入索引表(可选)与权重调优。
- 监控引入实时指标与反刷策略。
3. 阶段 C(规模优化)
- 搜索可迁移到 ES/OpenSearch(数据量变大时)。
- 监控引入漏斗分析、页面性能指标(Web Vitals)。
---
## 5. 方案一:模板文本输入升级为富文本/Markdown
## 5.1 设计目标
1. 保持现有 `webJsonString` 可读可写。
2. 新增字段协议,不破坏旧字段语义。
3. 同一字段可识别 `plain/html/md`,展示端统一渲染。
## 5.2 数据协议(推荐)
建议把“原本字符串字段”升级为内容对象:
```json
{
"format": "plain|html|md",
"raw": "原始输入",
"html": "用于展示的安全HTML"
}
```
兼容规则:
- 旧数据是字符串时:默认 `format=plain`。
- 新数据写入对象时:展示端优先用对象逻辑渲染。
## 5.3 前端编辑端代码位点与实现细节
## 5.3.1 新增公共组件
- 建议新增:`portal_website_edit/src/components/common/RichContentInput.vue`
核心逻辑:
1. `props.value` 支持字符串或对象。
2. 内部 `mode` 可切换 `plain/rich/md`。
3. rich 模式用 Quill;md 模式用 Markdown 编辑器(如 Toast UI)。
4. 每次输入输出统一对象 `{ format, raw, html }`。
示例(核心片段,Vue2 思路):
```vue
纯文本
富文本
Markdown
```
## 5.3.2 改造模板组件接入点
- 目标目录:`portal_website_edit/src/components/editEl/*.vue`
- 改造规则:
1. 把 `contenteditable` 文本块替换为 `RichContentInput`。
2. `this.$props.data.xxx = ...` 改为 v-model 数据绑定。
3. 仅替换文本字段,图片/列表结构保持原样。
优先改造组件:
1. `editEl2.vue`、`editEl3.vue`、`editEl8.vue`、`editEl11.vue`(文本密集)
2. `classicCase.vue`、`productCenter.vue`(首页影响面大)
## 5.3.3 保存与读取适配
- 位置:`portal_website_edit/src/views/editPage/index.vue`
逻辑:
1. 保存前遍历 `components`,将文本字段标准化为协议对象(或保持旧字符串)。
2. 读取后做 normalize,确保组件拿到统一结构。
3. 增加 `schemaVersion`(建议挂在页面 JSON 顶层)。
## 5.4 展示端渲染适配
- 建议新增:`portal_website/src/utils/renderContent.js`
```js
import DOMPurify from 'dompurify'
export function normalizeContent(v) {
if (typeof v === 'string') return { format: 'plain', raw: v, html: '' }
return v || { format: 'plain', raw: '', html: '' }
}
export function renderHtml(v, mdRender) {
const c = normalizeContent(v)
if (c.format === 'plain') return DOMPurify.sanitize((c.raw || '').replace(/\n/g, '
'))
if (c.format === 'md') return DOMPurify.sanitize(mdRender(c.raw || ''))
return DOMPurify.sanitize(c.html || '')
}
```
- 在 `portal_website/src/components/el/*.vue` 中统一通过 `renderHtml()` 输出。
## 5.5 安全策略
1. 前端展示前净化 HTML(DOMPurify)。
2. 后端入库前二次净化(可在 Service 层做策略兜底)。
3. 限制允许标签白名单(`p/br/strong/em/ul/li/a/img/table...`)。
## 5.6 使用场景
1. 市场运营编辑产品详情时输入图文混排说明。
2. 技术文档编辑参数章节时使用 Markdown 表格。
3. 首页案例描述保留历史纯文本,不影响旧页面。
---
## 6. 方案二:基于现有表结构的关键词搜索
## 6.1 设计目标
1. 一次搜索覆盖菜单、页面、产品详情、文档。
2. 结果可直接跳转到对应页面。
3. 保持对当前 `hw_web` / `hw_web1` 版本化数据模型兼容。
## 6.2 接口设计
## 6.2.1 门户搜索接口(展示端)
- 建议新增:`GET /portal/search`
- 参数:
- `keyword`(必填,1~50)
- `pageNum`(默认 1)
- `pageSize`(默认 20,上限 50)
返回结构:
```json
{
"code": 200,
"data": {
"total": 123,
"rows": [
{
"sourceType": "menu|web|web1|document",
"title": "命中标题",
"snippet": "命中摘要",
"score": 120,
"route": "/productCenter/detail",
"routeQuery": {"webCode":"7","typeId":"1","deviceId":"1001"}
}
]
}
}
```
## 6.2.2 编辑端搜索接口(可选)
- 建议新增:`GET /portal/search/edit`
- 结果附带 `editRoute`,支持一键跳到 `/editor` 或 `/productCenter/edit`。
## 6.3 后端代码位点建议(ruoyi-portal)
建议新增如下文件(按 RuoYi 常见分层):
1. Controller
- `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwSearchController.java`
2. Service
- `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwSearchService.java`
- `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwSearchServiceImpl.java`
3. Domain/DTO
- `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/SearchResultDTO.java`
4. Mapper
- `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwSearchMapper.java`
- `ruoyi-portal/src/main/resources/mapper/portal/HwSearchMapper.xml`
## 6.4 SQL 实现策略(V1)
使用 `UNION ALL + LIKE + 业务打分`:
```sql
SELECT 'menu' AS source_type,
m.web_menu_id AS biz_id,
m.web_menu_name AS title,
100 AS score,
NULL AS web_code,
NULL AS type_id,
NULL AS device_id
FROM hw_web_menu m
WHERE m.is_delete='0'
AND m.web_menu_name LIKE CONCAT('%', #{keyword}, '%')
UNION ALL
SELECT 'web' AS source_type,
w.web_id AS biz_id,
CONCAT('页面#', w.web_code) AS title,
60 AS score,
w.web_code,
NULL AS type_id,
NULL AS device_id
FROM hw_web w
WHERE w.is_delete='0'
AND w.web_json_string LIKE CONCAT('%', #{keyword}, '%')
UNION ALL
SELECT 'web1' AS source_type,
w1.web_id AS biz_id,
CONCAT('详情#', w1.web_code, '-', w1.typeId, '-', w1.device_id) AS title,
70 AS score,
w1.web_code,
w1.typeId AS type_id,
w1.device_id
FROM hw_web1 w1
WHERE w1.is_delete='0'
AND w1.web_json_string LIKE CONCAT('%', #{keyword}, '%')
UNION ALL
SELECT 'document' AS source_type,
d.document_id AS biz_id,
d.file_name AS title,
50 AS score,
d.web_code,
d.type AS type_id,
NULL AS device_id
FROM hw_web_document d
WHERE d.is_delete='0'
AND (
d.file_name LIKE CONCAT('%', #{keyword}, '%')
OR d.document_address LIKE CONCAT('%', #{keyword}, '%')
)
ORDER BY score DESC;
```
## 6.5 搜索结果路由映射逻辑
1. `menu` 命中
- 跳转 `portal_website` 的菜单路由(如 `/test?id={menuId}`)。
2. `web` 命中
- 若 `web_code=-1` -> 首页块位(可转首页并定位)。
- 若 `web_code=7` -> `/productCenter`。
- 其他 -> `/test?id={webCode}`(按现有约定)。
3. `web1` 命中
- `/productCenter/detail?webCode=...&typeId=...&deviceId=...`
4. `document` 命中
- 跳资料页或详情页 `el14` 区块,并传 `documentId`。
## 6.6 前端代码位点建议
展示端:
1. `portal_website/src/api/search.js`:新增搜索 API 调用。
2. `portal_website/src/views/search/index.vue`:新增搜索结果页。
3. 顶部导航组件(现有 header 组件)增加搜索框与回车触发。
编辑端(可选):
1. `portal_website_edit/src/api/search.js`
2. `/editor` 顶部增加“全站内容搜索”入口,便于运营定位。
## 6.7 性能与演进
1. V1(当前)
- 直接扫表 + `LIKE`
- 适合中小数据量和快速上线
2. V2(数据增长后)
- 增加 `hw_search_index` 索引表(预拆 JSON 文本)
- 定时任务增量更新索引
3. V3(大规模)
- 接入 Elasticsearch/OpenSearch
## 6.8 使用场景
1. 用户搜索“工业交换机”,同时命中产品详情、案例页、下载文档。
2. 用户搜索“防爆”,优先展示行业方案菜单入口。
3. 运营搜索关键词后直达编辑页修正文案。
---
## 7. 方案三:官网匿名访问监控(PV/UV/停留/来源)
## 7.1 监控目标
1. 实时看总访问趋势与来源质量。
2. 识别热门页面和高价值行为(下载、搜索、咨询)。
3. 支持营销活动效果归因(UTM)。
## 7.2 事件模型
推荐事件类型:
1. `page_view`
2. `page_leave`
3. `search_submit`
4. `download_click`
5. `contact_submit`
公共字段:
- `visitorId`(匿名长期 ID)
- `sessionId`(30 分钟无活动重置)
- `eventType`
- `path`
- `referrer`
- `utmSource/utmMedium/utmCampaign`
- `ua/device/browser/os`
- `ipHash`
- `eventTime`
## 7.3 前端采集实现位点(portal_website)
1. 新增:
- `portal_website/src/utils/analytics.js`
2. 注册:
- `portal_website/src/main.js` 中初始化 SDK
- `portal_website/src/router/index.js` 增加 `afterEach` 做 `page_view`
- 页面卸载时发送 `page_leave`(`sendBeacon` 优先)
示例:
```js
// analytics.js
export function collect(eventType, payload = {}) {
const body = {
eventType,
...payload,
visitorId: getOrCreateVisitorId(),
sessionId: getOrCreateSessionId(),
eventTime: Date.now(),
path: location.pathname + location.search,
referrer: document.referrer || ''
}
if (navigator.sendBeacon) {
navigator.sendBeacon('/prod-api/portal/analytics/collect', JSON.stringify(body))
} else {
fetch('/prod-api/portal/analytics/collect', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
keepalive: true
})
}
}
```
## 7.4 后端实现位点建议(ruoyi-portal)
1. Controller
- `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwAnalyticsController.java`
- 采集接口 `@Anonymous`:`POST /portal/analytics/collect`
2. Service/Mapper
- `IHwAnalyticsService` + `HwAnalyticsServiceImpl`
- `HwAnalyticsMapper` + `HwAnalyticsMapper.xml`
3. 定时汇总(Quartz)
- 每 5 分钟汇总实时指标到日统计表
- 每日 00:10 出日报聚合
## 7.5 数据库表设计(建议新增)
1. 明细表:`hw_web_visit_event`
- `id` bigint pk
- `event_type` varchar(32)
- `visitor_id` varchar(64)
- `session_id` varchar(64)
- `path` varchar(500)
- `referrer` varchar(500)
- `utm_source/utm_medium/utm_campaign` varchar(128)
- `ip_hash` varchar(128)
- `ua` varchar(500)
- `device/browser/os` varchar(64)
- `stay_ms` bigint null
- `created_at` datetime
- 索引:`idx_created_at`、`idx_event_type`、`idx_visitor_session`、`idx_path`
2. 日统计表:`hw_web_visit_daily`
- `stat_date` date pk
- `pv` bigint
- `uv` bigint
- `ip_uv` bigint
- `avg_stay_ms` bigint
- `bounce_rate` decimal(5,2)
- `search_count` bigint
- `download_count` bigint
- `created_at/updated_at` datetime
## 7.6 风险控制
1. 防刷
- IP + UA + path 短时间阈值限流(可复用 `@RateLimiter` 思路)
- 机器人 UA 黑名单
2. 隐私
- 不存明文 IP,只存 hash
- 避免采集姓名、手机号等 PII
3. 性能
- 上报接口异步入库(可先入 Redis Stream/队列再落库)
## 7.7 使用场景
1. 运营查看“今日 PV/UV、来源渠道、热门页面”。
2. 产品查看“搜索词和下载行为”评估内容价值。
3. 市场评估广告 UTM 转化效果。
---
## 8. 迭代计划(建议)
1. 第 1 周
- 完成输入协议、公共编辑组件、2~4 个核心模板接入。
- 完成搜索接口 V1(后端 + 展示端结果页)。
- 完成基础埋点 `page_view/page_leave/search_submit`。
2. 第 2 周
- 扩展模板接入到主要文本组件。
- 搜索结果高亮、排序优化、路由跳转完善。
- 埋点加入下载/咨询转化事件与日报接口。
3. 第 3 周
- 安全加固(XSS 白名单、限流、SQL 压测)。
- 引入搜索索引表和监控聚合任务(按数据规模决定)。
---
## 9. 验收标准
1. 输入升级
- 旧页面内容不丢失,显示一致。
- 新增富文本/Markdown可编辑、可保存、可展示。
- XSS 测试样例无法执行脚本。
2. 搜索
- 关键词可命中四类内容(菜单/页面/详情/文档)。
- 返回可直接跳转对应页面。
- 支持分页、相关度排序、摘要高亮。
3. 监控
- 能输出日维度 PV/UV/停留/跳出率。
- 可区分来源渠道并查看热门页面。
- 采集不显著影响首屏性能。
---
## 10. 附:实现时的关键注意点
1. 保持 `webJsonString` 契约稳定,避免一次性大范围改字段名。
2. `hw_web/hw_web1` 版本化写入下,搜索只查 `is_delete='0'`。
3. `hw_web_menu1` 字段命名历史问题(`value/valuel`)如启用需先统一映射。
4. 下载文档密钥逻辑不应被搜索结果泄露,搜索仅返回可公开字段。
5. 所有新接口遵循现有返回约定(`AjaxResult` / `TableDataInfo`)。
---
## 11. 搜索功能备选方案:基于 ES(优先 Easy-Es)
本章节作为“方案二(MySQL LIKE 搜索)”的升级备选,目标是在数据规模上升后获得更好的全文检索效果、相关度排序和高亮能力。
## 11.1 技术选型结论(优先级)
1. 优先:Easy-Es(Java 侧开发效率更高)
- 适用前提:团队接受引入 Easy-Es 依赖;检索需求以常规全文检索、过滤、高亮为主。
- 优点:实体注解 + Wrapper 风格,开发成本低,和现有 MyBatis 项目协作成本较小。
- 风险:框架抽象层增加,遇到复杂 DSL 场景仍需下沉原生能力。
2. 备选:原生 Elasticsearch Java Client
- 适用前提:需要复杂查询/聚合/性能调优,或希望减少第三方封装依赖。
- 优点:能力最全、控制力最高。
- 风险:开发样板代码更多。
建议:先按 Easy-Es 落地,保留“复杂查询接口走原生 Client”的扩展点。
## 11.2 索引模型设计(结合当前 5 张重点表)
统一索引:`hw_portal_content_v1`,查询别名:`hw_portal_content`
文档来源:
1. `hw_web_menu` -> `sourceType=menu`
2. `hw_web` -> `sourceType=web`
3. `hw_web1` -> `sourceType=web1`
4. `hw_web_document` -> `sourceType=document`
文档 ID 设计(避免版本化写入导致 ID 抖动):
1. 菜单:`menu:{web_menu_id}`
2. 页面:`web:{web_code}`
3. 详情:`web1:{web_code}:{typeId}:{device_id}`
4. 文档:`doc:{document_id}`
说明:`hw_web/hw_web1` 当前是“逻辑删旧 + 插入新”,用业务键作为 ES `_id`,可保证覆盖更新稳定。
## 11.3 ES Mapping 建议
```json
PUT hw_portal_content_v1
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"default_cn": {
"type": "custom",
"tokenizer": "ik_max_word"
},
"search_cn": {
"type": "custom",
"tokenizer": "ik_smart"
}
}
}
},
"mappings": {
"dynamic": "false",
"properties": {
"docId": { "type": "keyword" },
"sourceType": { "type": "keyword" },
"title": { "type": "text", "analyzer": "default_cn", "search_analyzer": "search_cn", "fields": { "kw": { "type": "keyword", "ignore_above": 256 } } },
"content": { "type": "text", "analyzer": "default_cn", "search_analyzer": "search_cn" },
"keywords": { "type": "keyword" },
"webCode": { "type": "keyword" },
"typeId": { "type": "keyword" },
"deviceId": { "type": "keyword" },
"menuId": { "type": "keyword" },
"documentId": { "type": "keyword" },
"route": { "type": "keyword" },
"routeQuery": { "type": "flattened" },
"isDelete": { "type": "keyword" },
"updatedAt": { "type": "date" }
}
}
}
```
注意:
1. 若 ES 环境未安装 IK 分词器,可临时改为 `standard`,但中文效果会下降。
2. `routeQuery` 用 `flattened` 可减少动态字段爆炸风险。
## 11.4 入索引字段映射规则(按表)
1. `hw_web_menu`
- `title = web_menu_name`
- `content = web_menu_name + ancestors`(可拼接)
- `menuId = web_menu_id`
- `route = /test`
- `routeQuery = { "id": web_menu_id }`
2. `hw_web`
- `title = 页面标识(如 web_code)` 或从 JSON 提取首个标题字段
- `content = web_json_string`(建议清洗 HTML 标签后再入索引)
- `webCode = web_code`
- `route` 根据 `web_code` 映射(`-1 -> /index`, `7 -> /productCenter`, 其他 -> `/test`)
3. `hw_web1`
- `title = 设备详情标识或 JSON 首标题`
- `content = web_json_string`(清洗后)
- `webCode/typeId/deviceId` 对应业务键
- `route = /productCenter/detail`
- `routeQuery = { webCode, typeId, deviceId }`
4. `hw_web_document`
- `title = file_name`(若表中无该列则退化为 document_id)
- `content = document_address`(仅用于检索,不返回敏感内容)
- `documentId = document_id`
- `route` 指向资料页或对应详情页
## 11.5 同步流程设计(全量 + 增量 + 补偿)
## 11.5.1 全量初始化
1. 启动任务或管理接口触发:
- 扫描四类数据表(`is_delete='0'`)
- 按 500~1000 条批量写入 ES(Bulk)
- 完成后切换 alias 到新索引(可选蓝绿)
2. 建议代码位置
- `ruoyi-portal/.../service/search/HwSearchRebuildService.java`
- `ruoyi-portal/.../controller/HwSearchAdminController.java`(内部管理接口)
## 11.5.2 增量同步(主路径)
触发点(在现有写接口后):
1. 菜单变更:`/portal/hwWebMenu/*`
2. 页面变更:`/portal/hwWeb/updateHwWeb`
3. 详情变更:`/portal/hwWeb1/updateHwWeb1`
4. 文档变更:`/portal/hwWebDocument/*`
实现建议:
1. 在 Service 成功提交后发送“同步事件”(推荐事务后回调)。
2. 同步消费者将业务对象转换为索引文档并 `upsert`。
3. 删除操作写成 `delete by id` 或 `isDelete=1` 后过滤。
## 11.5.3 补偿机制(强烈建议)
新增 outbox 表:`hw_search_sync_outbox`
- 字段:`id,biz_type,biz_key,op_type,payload,status,retry_count,next_retry_time,created_at,updated_at`
流程:
1. 业务写库与 outbox 同事务提交。
2. 定时任务扫描 `status=pending/failed` 进行重试。
3. 超过阈值告警,人工或脚本修复。
该机制可避免“数据库成功、ES 失败”造成长期不一致。
## 11.6 查询 DSL 设计
## 11.6.1 通用搜索 DSL(关键词 + 类型过滤 + 高亮)
```json
POST hw_portal_content/_search
{
"from": 0,
"size": 20,
"_source": ["docId","sourceType","title","route","routeQuery","webCode","typeId","deviceId","menuId","documentId","updatedAt"],
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "工业交换机",
"fields": ["title^5", "content^2"],
"type": "best_fields"
}
}
],
"filter": [
{ "term": { "isDelete": "0" } }
]
}
},
"highlight": {
"pre_tags": [""],
"post_tags": [""],
"fields": {
"title": {},
"content": { "fragment_size": 120, "number_of_fragments": 1 }
}
},
"sort": [
{ "_score": "desc" },
{ "updatedAt": "desc" }
]
}
```
## 11.6.2 指定类型筛选
在 `filter` 增加:
```json
{ "terms": { "sourceType": ["web1", "document"] } }
```
## 11.6.3 精确业务键跳转查询
用于编辑后台快速定位:
```json
{
"query": {
"bool": {
"filter": [
{ "term": { "sourceType": "web1" } },
{ "term": { "webCode": "7" } },
{ "term": { "typeId": "2" } },
{ "term": { "deviceId": "10001" } }
]
}
}
}
```
## 11.7 Easy-Es 落地骨架(建议)
建议包结构(`ruoyi-portal` 内):
1. `.../search/es/entity/PortalSearchDoc.java`
2. `.../search/es/mapper/PortalSearchMapper.java`
3. `.../search/service/PortalSearchEsService.java`
4. `.../search/service/impl/PortalSearchEsServiceImpl.java`
5. `.../search/convert/PortalSearchDocConverter.java`
实体示意(简化):
```java
@IndexName("hw_portal_content")
public class PortalSearchDoc {
@IndexId
private String docId;
private String sourceType;
private String title;
private String content;
private String webCode;
private String typeId;
private String deviceId;
private String menuId;
private String documentId;
private String route;
private Map routeQuery;
private String isDelete;
private Date updatedAt;
}
```
查询示意(伪代码):
```java
LambdaEsQueryWrapper qw = new LambdaEsQueryWrapper<>();
qw.match(PortalSearchDoc::getTitle, keyword).or().match(PortalSearchDoc::getContent, keyword);
qw.eq(PortalSearchDoc::getIsDelete, "0");
qw.orderByDesc(BaseEsEntity::getScore);
// 执行分页查询并组装高亮字段
```
注:高亮、复杂 `multi_match`、`function_score` 等场景若 Easy-Es 封装不够直观,可在同服务中直接使用原生 ES Client 补充。
## 11.8 与当前 MySQL 方案的切换策略
1. 接口层保持不变:继续使用 `/portal/search`。
2. 配置开关:
- `search.engine=mysql|es`(默认 mysql)
- `search.es.enabled=true/false`
3. 灰度:
- 小流量用户先走 ES,比对命中率与响应时间。
- 指标达标后全量切换。
4. 回滚:
- 保留 MySQL 搜索实现,ES 故障时秒级切回。
## 11.9 适用场景与收益
1. 中文关键词检索命中明显提升(分词 + 相关度)。
2. 多来源内容统一搜索体验(菜单/页面/详情/文档)。
3. 结果高亮与权重排序更符合官网用户行为。
## 11.10 风险与前置条件
1. 前置条件
- 可用 ES 集群(开发/测试/生产)
- 分词器插件(推荐 IK)
- 运维监控(磁盘、分片、慢查询)
2. 风险
- 数据一致性:需 outbox + 重试保障
- 运维复杂度:备份、索引管理、版本兼容
- 成本:新增基础设施资源
结论:
在你当前架构中,ES 是可行且长期更优的搜索升级路径;落地顺序建议"Easy-Es 优先 + 原生 Client 补位 + MySQL 方案保底"。
---
## 12. 深度补充分析(2026-02-28)
以下内容是对方案 1/2/3 + 11 的深度审视,标注了原方案未覆盖或不够充分的要点。
---
## 12.1 问题一补充:富文本/Markdown 输入升级的实际落地细节
### 12.1.1 字段分类:并非所有文本字段都需要富文本
通过审查全部 21 个 `editEl` 组件的 `contenteditable` 使用(grep 到约 50+ 处),字段可分为三类:
1. **短标题字段(不需要富文本)**
- `title`、`subTitle`、`itemTitle`、`leftTitle`、`bannerTitle`、`bannerValue`
- 特点:一般 ≤50 字,纯文本即可,富文本反而增加复杂度
- 涉及组件:几乎所有 `editEl1~editEl16`、`carousel`
2. **中长内容字段(适合富文本/Markdown)**
- `contentInfo`(editEl2)、`info`(editEl6)、`value`(editEl4/el9/el11/carousel)、`itemInfo`(editEl3)、`leftInfo`(editEl8)
- 特点:可能 50~500+ 字,含段落、换行、强调等需求
- 优先改造范围
3. **结构化字段(不需要改造)**
- `editEl12`(imgList/features)、`editEl13`(params 表格)、`editEl14`(fileList 文件)、`editEl15`(banner 双图文,字段短)、`editEl16`(params 多列表格)
- 这些组件的文本是表格单元格或列表项,不适合富文本
**结论:** 原方案 5.3.2 "把 `contenteditable` 文本块替换为 `RichContentInput`" 应改为 **"仅对中长内容字段替换"**,短标题字段保留纯文本(可用 `el-input` 替代 `contenteditable`,但不引入富文本)。
### 12.1.2 Quill 2.0 + Vue 2 兼容性问题(关键风险)
当前 `package.json` 已有 `quill@2.0.3`,但:
1. **`vue-quill-editor`**(社区最常用的 Vue 2 Quill 封装)仅支持 Quill 1.x,**不兼容 Quill 2.0**。
2. **`@vueup/vue-quill`** 仅支持 Vue 3。
3. Quill 2.0 的 API 与 1.x 有 breaking changes(模块注册、Delta 格式、toolbar 配置等)。
**可行方案(按优先级):**
| 方案 | 说明 | 风险 |
|---|---|---|
| A. 降级到 Quill 1.3.7 + vue-quill-editor | 社区成熟、文档多、Vue 2 直接可用 | Quill 1.x 不再维护,功能较老 |
| B. 自封装 Quill 2.0 组件 | 手动 `new Quill(el, options)` 包装为 Vue 2 组件 | 需自行处理生命周期、v-model 绑定、销毁清理 |
| C. 使用 TinyMCE / CKEditor 5(Vue 2 插件) | 功能更全、官方有 Vue 2 集成 | 体积更大、需额外学习 |
| D. 使用 wangEditor 5(国产,Vue 2 支持) | 中文生态好、文档全、体积适中 | 社区相对 Quill/CKEditor 小 |
**建议**:若团队无特别偏好,**方案 D(wangEditor 5)** 对当前项目最友好——原生中文文档、Vue 2 官方支持(`@wangeditor/editor-for-vue`@1.x)、轻量、默认中文 toolbar。若已有 Quill 使用经验则走 **方案 B**。
### 12.1.3 Markdown 编辑器选型(Vue 2 场景)
原方案提到 "Toast UI",补充对比:
| 编辑器 | Vue 2 支持 | 特点 |
|---|---|---|
| `@toast-ui/vue-editor` | ✅ 官方支持 | 功能全,但包体较大(~300KB gzip) |
| `v-md-editor` | ✅ | 国产、轻量、支持预览 |
| `mavon-editor` | ✅ | 国产、使用广泛、功能成熟 |
建议:若 Markdown 需求为"技术文档/参数描述"场景,用 **`v-md-editor`** 即可。
### 12.1.4 `contenteditable` 替换为 `el-input` 的快捷路径
在正式引入富文本之前,有一个**更快速的改进路径**:
- 将所有 `` 替换为 ``(短字段用 `type="text"`,长字段用 `type="textarea"`)。
- 好处:消除 `contenteditable` 的原地改 props 问题,获得输入校验能力,不引入额外依赖。
- 这可以作为阶段 0 快速实施,后续再对长内容字段升级为富文本。
---
## 12.2 问题二补充:匿名访问监控的额外考量
### 12.2.1 自建 vs 第三方方案对比
原方案仅考虑了自建方案。补充第三方对比:
| 方案 | 部署方式 | 数据隐私 | 开发成本 | 功能完整度 |
|---|---|---|---|---|
| 自建(方案三) | 集成到现有后端 | 完全可控 | 高(需开发采集+聚合+看板) | 按需定制 |
| **Umami(推荐评估)** | 自托管 Docker | 数据自有 | 极低(部署即用) | PV/UV/来源/设备/事件/UTM |
| Matomo | 自托管 | 数据自有 | 低 | 非常全面(含漏斗、热力图) |
| 百度统计/Google Analytics | SaaS | 数据在第三方 | 极低(嵌入 JS 即可) | 全面 |
**建议**:
- 若只需 PV/UV/来源/热门页面等基础看板,**Umami 自托管 1~2 小时即可部署完成**,无需后端开发,前端仅需嵌入一行 `