You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hw-web/doc/IMPLEMENTATION_PLAN_PORTAL_...

1221 lines
42 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 官网编辑输入升级、关键词搜索与访问监控实现方案
## 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 模式用 Quillmd 模式用 Markdown 编辑器(如 Toast UI)。
4. 每次输入输出统一对象 `{ format, raw, html }`
示例(核心片段,Vue2 思路):
```vue
<template>
<div class="rich-content-input">
<el-radio-group v-model="mode" size="mini">
<el-radio-button label="plain">纯文本</el-radio-button>
<el-radio-button label="rich">富文本</el-radio-button>
<el-radio-button label="md">Markdown</el-radio-button>
</el-radio-group>
<el-input
v-if="mode==='plain'"
type="textarea"
:rows="4"
v-model="plainText"
@input="emitValue"
/>
<quill-editor
v-else-if="mode==='rich'"
v-model="richHtml"
@change="emitValue"
/>
<markdown-editor
v-else
v-model="mdText"
@change="emitValue"
/>
</div>
</template>
<script>
export default {
props: { value: { type: [String, Object], default: '' } },
data() { return { mode: 'plain', plainText: '', richHtml: '', mdText: '' } },
created() { this.initFromValue(this.value) },
methods: {
initFromValue(v) {
if (typeof v === 'string') { this.mode = 'plain'; this.plainText = v; return; }
this.mode = v.format || 'plain';
this.plainText = this.mode === 'plain' ? (v.raw || '') : '';
this.richHtml = this.mode === 'html' || this.mode === 'rich' ? (v.html || '') : '';
this.mdText = this.mode === 'md' ? (v.raw || '') : '';
},
emitValue() {
const payload = this.mode === 'plain'
? { format: 'plain', raw: this.plainText, html: '' }
: this.mode === 'rich'
? { format: 'html', raw: this.richHtml, html: this.richHtml }
: { format: 'md', raw: this.mdText, html: this.$md.render(this.mdText) };
this.$emit('input', payload);
}
}
}
</script>
```
## 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, '<br/>'))
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. 前端展示前净化 HTMLDOMPurify)。
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-EsJava 侧开发效率更高)
- 适用前提:团队接受引入 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 条批量写入 ESBulk
- 完成后切换 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": ["<em class='hit'>"],
"post_tags": ["</em>"],
"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<String, Object> routeQuery;
private String isDelete;
private Date updatedAt;
}
```
查询示意(伪代码):
```java
LambdaEsQueryWrapper<PortalSearchDoc> 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 5Vue 2 插件) | 功能更全、官方有 Vue 2 集成 | 体积更大、需额外学习 |
| D. 使用 wangEditor 5国产Vue 2 支持) | 中文生态好、文档全、体积适中 | 社区相对 Quill/CKEditor 小 |
**建议**:若团队无特别偏好,**方案 DwangEditor 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` 的快捷路径
在正式引入富文本之前,有一个**更快速的改进路径**
- 将所有 `<span contenteditable="true" @blur="edit(...)">` 替换为 `<el-input v-model="data.xxx" />`(短字段用 `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 小时即可部署完成**,无需后端开发,前端仅需嵌入一行 `<script>` 标签。
- 若需要自定义事件download_click/search_submit/contact_submit且希望与业务数据库关联则自建方案仍有价值可与 Umami **并行使用**。
- 先用 Umami 快速获得基础看板,自建方案聚焦于"业务转化事件 + 与搜索/内容编辑数据关联"。
### 12.2.2 CORS 配置
`portal_website`(展示端)与后端可能不同源。采集接口 `POST /portal/analytics/collect` 需要:
1. 后端在 `WebMvcConfigurer` 或 Spring Security 中对 `/portal/analytics/**` 放行 CORS。
2. 该接口标记 `@Anonymous`(无需登录)。
3. `sendBeacon` 发送的 `Content-Type``text/plain`(非 `application/json`),需后端兼容解析或前端用 `Blob` 包装。
### 12.2.3 visitorId 生成与持久化
建议方案:
- 首次访问生成 UUID存入 `localStorage`(非 Cookie避免被清理策略影响
-`localStorage` 被清除则重新生成UV 会略有偏差,可接受)。
- **不要**用 IP 或 fingerprint 作为唯一标识(隐私合规风险)。
### 12.2.4 数据保留与归档策略
`hw_web_visit_event` 明细表会快速增长。建议:
- 明细表保留 **90 天**,超期按月归档到 `hw_web_visit_event_archive_YYYYMM`
- 日统计表 `hw_web_visit_daily` 永久保留。
- 归档可通过 Quartz 定时任务实现(每月 1 日归档上月数据)。
---
## 12.3 问题三补充:搜索功能的深度分析
### 12.3.1 当前数据量评估(基于 hw_web.sql 实际数据)
| 表 | 总行数 | 活跃行is_delete='0' | 单行 web_json_string 大小 |
|---|---|---|---|
| `hw_web_menu` | 28 | ~20+ | N/A无 JSON |
| `hw_web` | 38 | ~15-18 | 1KB ~ 15KB含大量 URL 和 JSON 结构标记) |
| `hw_web1` | 31 | ~15-20 | 1KB ~ 10KB |
| `hw_web_document` | 11 | ~10 | N/A |
| `hw_web_menu1` | 2 | ~2 | N/A |
**结论:当前活跃数据总量 < 100 条,MySQL 方案完全够用。** 即使考虑到未来增长 10 倍(~1000 条),MySQL 仍可应对。ES 的引入应作为"数据量破万 + 搜索体验要求高"时的升级路径。
### 12.3.2 原方案 V1 SQL 的关键缺陷JSON 误匹配
原方案 6.4 SQL 直接 `LIKE` 搜索 `web_json_string`,会产生严重误匹配:
```
-- 用户搜索"png":会命中所有包含图片 URL 的记录
-- 用户搜索"type":会命中所有 JSON 结构的 type 字段
-- 用户搜索"icon":会命中所有含 icon 字段的 JSON
```
**必须在搜索前从 JSON 中提取纯文本**。两种方案:
**方案 AMySQL 8.0 JSON 函数实时提取(零成本,推荐 V1**
MySQL 8.0 支持 `JSON_EXTRACT` + `JSON_UNQUOTE`,可以在 SQL 中提取 JSON 中的文本字段。但 `web_json_string` 存储的是 `longtext`(非 JSON 列类型),且结构嵌套复杂(数组套对象套数组),用 SQL JSON 函数提取成本高。
更实际的做法:**在 Service 层做 JSON 文本提取**。
```java
// 伪代码:从 webJsonString 中提取所有可见文本
public String extractSearchableText(String webJsonString) {
// 1. 解析 JSON
// 2. 递归遍历,提取 title/subTitle/value/contentInfo/name 等文本字段
// 3. 过滤掉 icon/url/type 等非文本字段
// 4. 拼接为纯文本返回
}
```
**方案 B:预构建搜索索引表(推荐 V1.5**
新增表 `hw_search_index`
```sql
CREATE TABLE hw_search_index (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
source_type VARCHAR(16) NOT NULL COMMENT 'menu/web/web1/document',
biz_id VARCHAR(64) NOT NULL COMMENT '业务主键或业务键',
title VARCHAR(512) NULL COMMENT '标题从JSON提取',
content TEXT NULL COMMENT '纯文本内容从JSON提取去除URL/结构标记)',
web_code VARCHAR(32) NULL,
type_id VARCHAR(32) NULL,
device_id VARCHAR(32) NULL,
menu_id VARCHAR(32) NULL,
document_id VARCHAR(64) NULL,
route VARCHAR(128) NULL COMMENT '展示端跳转路由',
route_query JSON NULL COMMENT '路由参数',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_delete CHAR(1) NOT NULL DEFAULT '0',
FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram,
UNIQUE INDEX uk_source_biz (source_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官网搜索索引表';
```
关键点:
- **MySQL FULLTEXT + ngram** 分词器,原生支持中文分词,无需引入 ES
- `ngram` token_size 默认 2,对中文效果比 LIKE 好很多(可匹配"物联"、"轮胎"等双字词)。
- `hw_web`/`hw_web1` 保存成功后(Service 层事务后回调),触发索引更新。
- 搜索 SQL`SELECT ... FROM hw_search_index WHERE MATCH(title, content) AGAINST(#{keyword} IN BOOLEAN MODE) AND is_delete='0'`。
### 12.3.3 应纳入搜索范围的额外表(原方案遗漏)
5 张重点表外,Mapper 目录显示 `ruoyi-portal` 还操作以下表,这些表也包含可搜索内容:
| | 可搜索字段 | 说明 |
|---|---|---|
| `hw_portal_config_type` | `config_type_name`, `config_type_desc`, `home_config_type_name` | 产品分类名称和描述 |
| `hw_product_info` | `product_info_ctitle`, `product_info_etitle` | 产品信息标题 |
| `hw_product_info_detail` | `product_info_detail_title`, `product_info_detail_desc` | 产品明细描述 |
| `hw_product_case_info` | `case_info_title`, `case_info_desc` | 案例标题和描述 |
| `hw_about_us_info` | `about_us_info_title`, `about_us_info_desc` | 关于我们 |
搜索命中这些表时,路由映射规则:
- `hw_portal_config_type` -> `/productCenter`(按 `config_type_classification` 和层级确定 tab
- `hw_product_info/detail` -> `/productCenter/detail`(按 `config_type_id` 跳转)
- `hw_product_case_info` -> 案例详情页
- `hw_about_us_info` -> `/contactUs`
### 12.3.4 Elasticsearch 与 Easy-Es 的技术可行性深度评估
#### 什么是 Elasticsearch
ElasticsearchES是基于 Lucene 的分布式全文搜索引擎,提供:
- **倒排索引**:对文档中的每个词建立索引,搜索时 O(1) 定位,远快于 SQL LIKE 的全表扫描。
- **分词**:中文需要 IK 分词器插件,将"工业物联网解决方案"拆为"工业/物联网/解决/方案"等词项。
- **相关度评分**BM25 算法自动按匹配程度排序,比固定 score 更准确。
- **高亮**:返回结果自动标记命中词位置,展示端直接渲染。
- **近实时搜索**:数据写入后 1 秒内可被检索到。
- **代价**:需要独立部署 ES 集群(最少 1 节点JVM 内存建议 ≥1GB运维磁盘/分片/索引管理。
#### 什么是 Easy-Es
Easy-Es 是国产开源框架(`cn.easy-es`),目标是"让 ES 操作像 MyBatis-Plus 一样简单"
- **注解驱动**`@IndexName`、`@IndexId`、`@IndexField` 标注实体类,自动创建/更新索引 Mapping。
- **Wrapper 查询**`LambdaEsQueryWrapper` 链式构建查询条件,不需要手写 JSON DSL。
- **自动高亮**:配置 `highLight=true` 即可。
- **与 Spring Boot 集成**`easy-es-boot-starter` 自动配置连接。
#### 版本兼容性分析(关键)
| 依赖 | 当前版本 | Easy-Es 要求 |
|---|---|---|
| Java | 17 | ✅ 支持 |
| Spring Boot | 3.5.8 | ⚠️ Easy-Es 2.0.0 官方适配 Spring Boot 3.0~3.23.5.x 需验证 |
| Elasticsearch | 未引入 | Easy-Es 2.x 支持 ES 7.14~8.x |
**风险点**
1. Easy-Es 2.0.0 内部依赖 `elasticsearch-rest-high-level-client`ES 官方已在 8.x 标记 deprecated与 Spring Boot 3.5.8 的自动配置可能冲突。需要 **排除 Spring Data Elasticsearch 自动配置** 或锁定版本。
2. 若 ES 服务端版本为 8.x需确认 Easy-Es 2.0.0 的高级客户端兼容模式是否正常。
#### Maven 依赖参考(若引入 Easy-Es
```xml
<!-- 根 pom.xml properties -->
<easy-es.version>2.0.0</easy-es.version>
<elasticsearch.version>7.14.0</elasticsearch.version>
<!-- ruoyi-portal/pom.xml -->
<dependency>
<groupId>cn.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>${easy-es.version}</version>
</dependency>
```
`application.yml` 配置:
```yaml
easy-es:
enable: true
address: localhost:9200
schema: http
# username: elastic # 如有认证
# password: xxx
```
#### 最终建议落地路径
```
V1当前实施
└─ MySQL hw_search_index + FULLTEXT ngram
├─ 无额外基础设施
├─ 中文分词效果可用(双字粒度)
└─ 数据量 <10000 完全够用
V2数据量 >10000 或需要更好搜索体验时)
└─ 引入 ES + Easy-Es
├─ 部署 ES 7.14+ 单节点 + IK 分词器
├─ hw_search_index 同步写入 ES
├─ 搜索接口通过配置开关切换 MySQL/ES
└─ 保留 MySQL 搜索作为降级方案
V3规模化
└─ ES 集群 + 原生 Client 补位复杂查询
```
### 12.3.5 搜索结果摘要生成策略
原方案返回 `snippet` 字段但未说明如何生成。补充:
1. **MySQL 方案**:在 Service 层做关键词上下文截取(关键词前后各取 50~80 字符)。
2. **ES 方案**:直接用 `highlight` 返回的 fragment。
3. **通用规则**
- 优先返回 `title` 高亮。
-`title` 未命中,返回 `content` 中命中片段。
- 摘要长度限制 120 字符。
- 高亮标签 `<em class="search-hit">...</em>`,展示端 CSS 控制样式。
---
## 12.4 三个问题的总结评估
| 问题 | 原方案覆盖度 | 主要补充点 |
|---|---|---|
| 1. 富文本输入 | ~70% | 字段分类短标题不改、Quill 2.0 兼容性、wangEditor/v-md-editor 推荐、el-input 快捷路径 |
| 2. 访问监控 | ~75% | Umami 自托管作为快速方案、CORS/sendBeacon 兼容、visitorId 策略、数据保留策略 |
| 3. 搜索功能 | ~65% | JSON 误匹配问题、hw_search_index + FULLTEXT ngram 作为 V1.5、额外可搜索表、Easy-Es 版本兼容性风险、摘要生成 |
### 修订后的推荐实施优先级
1. **最快见效**:搜索 V1hw_search_index + FULLTEXT ngram + Umami 自托管 → 1~2 天可上线基础能力。
2. **第二优先**editEl 组件将 `contenteditable` 改为 `el-input`(阶段 0→ 消除最大输入风险。
3. **第三优先**:对 `contentInfo`/`value`/`info` 等长文本字段引入 wangEditor → 富文本能力。
4. **按需引入**ES + Easy-Es数据量增长后、自建埋点Umami 不满足时)。