|
|
# 官网编辑输入升级、关键词搜索与访问监控实现方案
|
|
|
|
|
|
## 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
|
|
|
<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. 前端展示前净化 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": ["<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 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` 的快捷路径
|
|
|
|
|
|
在正式引入富文本之前,有一个**更快速的改进路径**:
|
|
|
|
|
|
- 将所有 `<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 中提取纯文本**。两种方案:
|
|
|
|
|
|
**方案 A:MySQL 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?
|
|
|
|
|
|
Elasticsearch(ES)是基于 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.2,3.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. **最快见效**:搜索 V1(hw_search_index + FULLTEXT ngram) + Umami 自托管 → 1~2 天可上线基础能力。
|
|
|
2. **第二优先**:editEl 组件将 `contenteditable` 改为 `el-input`(阶段 0)→ 消除最大输入风险。
|
|
|
3. **第三优先**:对 `contentInfo`/`value`/`info` 等长文本字段引入 wangEditor → 富文本能力。
|
|
|
4. **按需引入**:ES + Easy-Es(数据量增长后)、自建埋点(Umami 不满足时)。
|