Refactor(desktop-client): code structure for improved readability and maintainability

This commit is contained in:
2026-04-20 21:55:54 +08:00
parent 9fa091c150
commit 749b6b99cd
7 changed files with 1598 additions and 2328 deletions
+60 -6
View File
@@ -1,18 +1,72 @@
appId: com.geo.rankly.desktop
productName: GEO Rankly Desktop
copyright: Copyright © 2026 GEO Rankly
directories:
output: release
files:
- out/**
buildResources: build
compression: maximum
removePackageScripts: true
removePackageKeywords: true
asar: true
asarUnpack:
- "**/node_modules/better-sqlite3/build/Release/**"
- "**/node_modules/better-sqlite3/lib/**"
- "**/node_modules/bindings/**"
- "**/node_modules/file-uri-to-path/**"
- "**/node_modules/playwright-core/lib/**"
- "**/node_modules/playwright-core/bin/**"
files:
- out/**/*
- package.json
- "!**/*.map"
- "!**/*.ts"
- "!**/*.tsx"
- "!**/*.md"
- "!**/*.markdown"
- "!**/{test,tests,__tests__,__mocks__,example,examples,docs,doc,demo,samples,man}/**"
- "!**/{LICENSE,LICENSE.*,license,CHANGELOG,CHANGELOG.*,README,README.*,HISTORY,HISTORY.*,AUTHORS,CONTRIBUTING}*"
- "!**/.*"
- "!**/node_modules/.bin/**"
- "!**/node_modules/*/{Makefile,Gulpfile.js,Gruntfile.js,CMakeLists.txt,*.coffee,*.flow,*.c,*.cc,*.cpp,*.h,*.hpp,*.gyp,*.gypi,*.tgz,binding.gyp}"
- "!**/node_modules/**/{*.d.ts,tsconfig*.json,jest.config*,rollup.config*,webpack.config*,vite.config*}"
- "!**/node_modules/better-sqlite3/{deps,src,docs,benchmark}/**"
- "!**/node_modules/better-sqlite3/{test_extension.c,download.sh,*.gyp,*.gypi}"
- "!**/node_modules/playwright-core/{types,ThirdPartyNotices.txt}/**"
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
mac:
category: public.app-category.productivity
identity: null
notarize: false
hardenedRuntime: false
gatekeeperAssess: false
electronLanguages:
- en
- zh_CN
target:
- dmg
- zip
- target: dmg
arch: [arm64]
- target: zip
arch: [arm64]
dmg:
format: ULFO
contents:
- x: 130
y: 220
- x: 410
y: 220
type: link
path: /Applications
win:
target:
- nsis
- target: nsis
arch: [x64]
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
linux:
category: Office
target:
- AppImage
- target: AppImage
arch: [x64]
publish: null
+21 -7
View File
@@ -8,9 +8,25 @@ import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
const rootDir = fileURLToPath(new URL(".", import.meta.url));
const workspacePackages = [
"@geo/shared-types",
"@geo/http-client",
"@geo/publisher-platforms",
"@geo/ui-shared",
];
const workspaceAliases = {
"@geo/shared-types": resolve(rootDir, "../../packages/shared-types/src/index.ts"),
"@geo/http-client": resolve(rootDir, "../../packages/http-client/src/index.ts"),
"@geo/publisher-platforms": resolve(rootDir, "../../packages/publisher-platforms/src/index.ts"),
"@geo/ui-shared": resolve(rootDir, "../../packages/ui-shared/src/index.ts"),
"@geo/ui-shared/tokens": resolve(rootDir, "../../packages/ui-shared/src/tokens/index.ts"),
"@geo/ui-shared/tokens.css": resolve(rootDir, "../../packages/ui-shared/src/tokens/index.css"),
};
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
plugins: [externalizeDepsPlugin({ exclude: workspacePackages })],
build: {
rollupOptions: {
input: {
@@ -26,11 +42,12 @@ export default defineConfig({
resolve: {
alias: {
"@main": resolve(rootDir, "src/main"),
...workspaceAliases,
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: [externalizeDepsPlugin({ exclude: workspacePackages })],
build: {
rollupOptions: {
input: {
@@ -46,6 +63,7 @@ export default defineConfig({
resolve: {
alias: {
"@preload": resolve(rootDir, "src/preload"),
...workspaceAliases,
},
},
},
@@ -64,11 +82,7 @@ export default defineConfig({
resolve: {
alias: {
"@renderer": resolve(rootDir, "src/renderer"),
"@geo/shared-types": resolve(rootDir, "../../packages/shared-types/src/index.ts"),
"@geo/http-client": resolve(rootDir, "../../packages/http-client/src/index.ts"),
"@geo/ui-shared": resolve(rootDir, "../../packages/ui-shared/src/index.ts"),
"@geo/ui-shared/tokens": resolve(rootDir, "../../packages/ui-shared/src/tokens/index.ts"),
"@geo/ui-shared/tokens.css": resolve(rootDir, "../../packages/ui-shared/src/tokens/index.css"),
...workspaceAliases,
},
},
},
+90 -9
View File
@@ -2,36 +2,39 @@
"name": "@geo/desktop-client",
"version": "0.1.0",
"private": true,
"description": "GEO Rankly Desktop — account health, monitor scheduler, and publish dispatch client.",
"author": {
"name": "GEO Rankly",
"email": "support@geo-rankly.local"
},
"type": "module",
"main": "out/main/bootstrap.cjs",
"scripts": {
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
"build": "electron-vite build",
"package:mac": "electron-builder --mac --arm64 ",
"package:mac": "electron-builder --mac --arm64",
"test": "vitest run",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@geo/http-client": "workspace:*",
"@geo/publisher-platforms": "workspace:*",
"@geo/shared-types": "workspace:*",
"@geo/ui-shared": "workspace:*",
"ant-design-vue": "^4.2.6",
"better-sqlite3": "^12.8.0",
"electron-updater": "^6.0.0",
"marked": "^17.0.5",
"pino": "^9.0.0",
"playwright-core": "^1.55.0",
"vue": "^3.5.31",
"vue-router": "^4.5.1",
"ws": "^8.20.0"
},
"devDependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@geo/http-client": "workspace:*",
"@geo/publisher-platforms": "workspace:*",
"@geo/shared-types": "workspace:*",
"@geo/ui-shared": "workspace:*",
"@playwright/test": "^1.0.0",
"@types/node": "^24.0.0",
"@types/ws": "^8.18.1",
"@vitejs/plugin-vue": "^5.2.4",
"ant-design-vue": "^4.2.6",
"electron": "41.2.0",
"electron-builder": "^25.0.0",
"electron-vite": "^2.0.0",
@@ -39,6 +42,84 @@
"unplugin-vue-components": "^32.0.0",
"vite": "^5.4.19",
"vitest": "^2.0.0",
"vue": "^3.5.31",
"vue-router": "^4.5.1",
"vue-tsc": "^3.2.6"
},
"build": {
"appId": "com.geo.rankly.desktop",
"productName": "GEO Rankly Desktop",
"copyright": "Copyright © 2026 GEO Rankly",
"directories": {
"output": "release",
"buildResources": "build"
},
"compression": "maximum",
"removePackageScripts": true,
"removePackageKeywords": true,
"asar": true,
"asarUnpack": [
"**/node_modules/better-sqlite3/build/Release/**",
"**/node_modules/better-sqlite3/lib/**",
"**/node_modules/bindings/**",
"**/node_modules/file-uri-to-path/**",
"**/node_modules/playwright-core/lib/**",
"**/node_modules/playwright-core/bin/**"
],
"files": [
"out/**/*",
"package.json",
"!**/*.map",
"!**/*.ts",
"!**/*.tsx",
"!**/*.md",
"!**/*.markdown",
"!**/{test,tests,__tests__,__mocks__,example,examples,docs,doc,demo,samples,man}/**",
"!**/{LICENSE,LICENSE.*,license,CHANGELOG,CHANGELOG.*,README,README.*,HISTORY,HISTORY.*,AUTHORS,CONTRIBUTING}*",
"!**/.*",
"!**/node_modules/.bin/**",
"!**/node_modules/*/{Makefile,Gulpfile.js,Gruntfile.js,CMakeLists.txt,*.coffee,*.flow,*.c,*.cc,*.cpp,*.h,*.hpp,*.gyp,*.gypi,*.tgz,binding.gyp}",
"!**/node_modules/**/{*.d.ts,tsconfig*.json,jest.config*,rollup.config*,webpack.config*,vite.config*}",
"!**/node_modules/better-sqlite3/{deps,src,docs,benchmark}/**",
"!**/node_modules/better-sqlite3/{test_extension.c,download.sh,*.gyp,*.gypi}",
"!**/node_modules/playwright-core/{types,ThirdPartyNotices.txt}/**",
"!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
],
"mac": {
"category": "public.app-category.productivity",
"identity": null,
"electronLanguages": ["en", "zh_CN"],
"target": [
{ "target": "dmg", "arch": ["arm64"] },
{ "target": "zip", "arch": ["arm64"] }
],
"notarize": false,
"hardenedRuntime": false,
"gatekeeperAssess": false
},
"dmg": {
"format": "ULFO",
"contents": [
{ "x": 130, "y": 220 },
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
]
},
"win": {
"target": [
{ "target": "nsis", "arch": ["x64"] }
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
{ "target": "AppImage", "arch": ["x64"] }
],
"category": "Office"
},
"publish": null
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-934
View File
@@ -1,934 +0,0 @@
# AI 品牌曝光监测系统技术方案 V3
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档名称 | AI 品牌曝光监测系统技术方案 V3(容量与口径修订稿) |
| 文档版本 | V3.0 |
| 文档状态 | 待评审 |
| 创建日期 | 2026-04-05 |
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v2.md` |
| 适用范围 | 数据追踪模块全部页面 |
| 关联文档 | `docs/geo-platform-prd-v1.md` (PRD 8.4 数据追踪) |
| 关联文档 | `docs/question-driven-monitoring-design-v1.md` (V1 数据模型) |
| 容量目标 | 5 万并发 **活跃** 用户(同时打开页面、切换筛选、触发突刺流量) |
## 2. V3 修订范围
本文档不再只是 V2 的“容量补丁”。本版同时修正了 V2 与采集可行性方案之间的口径断层,尤其是:
1. `run_mode` 未入模,导致标准回答和搜索增强回答不可比。
2. 问题版本未完整进入汇总表唯一键,历史口径会被覆盖。
3. 采集时间窗与聚合时序冲突,无法同时满足 T+1 发布与一致性。
4. 在线缓存与离线任务队列共用同一 Redis 故障域,风险过高。
读者应先读完 V2 全文,再用本文档替换或补充以下章节:
| V2 章节 | V3 替换 / 补充章节 | 变更原因 |
| --- | --- | --- |
| 5. 数据库 Schema | 2.2 数据口径与 Schema Delta | V2 未建模 `run_mode`、版本键、引用事实 |
| 8. 缓存策略 | 3. 读流量模型与缓存架构 | V2 低估页面 fan-out,缺少防雪崩手段 |
| 10. 采集流水线设计 | 4. 采集流水线扩容 | V2 采集时间窗 / worker 容量不足 |
| 11. 部署拓扑 | 5. 部署拓扑与资源 | V2 连接池计算有误、单点问题 |
| 7. 后端架构 / 7.6 配置扩展 | 4.5、5.3、5.4 | V2 仍按单 provider / 单 Redis / 单 DB 结构假设 |
| (无) | 6. 降级与容灾 | V2 缺失 |
| 13. 实施计划 (Phase 21) | 7. 压测验收标准 | V2 缺失 |
V2 其他章节(页面需求、前端组件、API 响应结构、指标定义)原则上保持不变;但只要与本文中的口径分层、Schema Delta、采集时序冲突,应以 V3 为准。
### 2.1 数据口径分层与 UI 取数规则
为避免把不可比的数据混到同一张榜单中,V3 将监测数据拆成 3 种采集模式:
| `run_mode` | 含义 | 是否进入主统计 | 默认消费页面 |
| --- | --- | --- | --- |
| `api_standard` | 固定 prompt、低温、无搜索增强的标准 API 回答 | 是 | Dashboard、平台占比、竞争对手、业务主题、AI 对话问题、品牌印象 |
| `api_search_grounded` | 开启平台官方搜索 / 引文能力后的回答 | 是,但单独统计 | AI 引用排名;未来可选做“搜索增强视角”专题页 |
| `ui_sample` | 面向 PoC / 验证的前台 UI 采样结果 | 否 | 仅验证报表和 badcase,不进主 Dashboard |
核心规则:
1. **跨平台曝光、竞品、问题排行默认只看 `api_standard`。**
2. **引用相关页面默认只看 `api_search_grounded`。**
3. **不支持搜索增强或引文的平台,在引用页面中标记为“未接入 / 不支持”,不按 0 值并入分母。**
4. **`ui_sample` 只用于验证,不参与 `monitoring_*` 主汇总。**
### 2.2 Schema Delta(覆盖 V2 Schema 的必要修订)
V2 的基础表结构仍可复用,但以下修订是必需项。
#### 2.2.1 `ai_platforms`
增加平台能力字段,避免查询层硬编码:
1. `supports_standard BOOLEAN NOT NULL DEFAULT true`
2. `supports_search_grounded BOOLEAN NOT NULL DEFAULT false`
3. `supports_citation BOOLEAN NOT NULL DEFAULT false`
4. `supports_batch BOOLEAN NOT NULL DEFAULT false`
5. `default_run_mode VARCHAR(30) NOT NULL DEFAULT 'api_standard'`
#### 2.2.2 `question_monitor_runs`
新增以下字段:
1. `business_date DATE NOT NULL`:统计归属日,允许 `run_at` 发生在次日凌晨。
2. `run_mode VARCHAR(30) NOT NULL``api_standard` / `api_search_grounded` / `ui_sample`
3. `provider_model VARCHAR(100) NOT NULL`
4. `prompt_template_version VARCHAR(30) NOT NULL`
5. `search_enabled BOOLEAN NOT NULL DEFAULT false`
6. `request_id VARCHAR(100)`
7. `retry_count INT NOT NULL DEFAULT 0`
建议增加幂等索引:
```sql
CREATE UNIQUE INDEX uk_monitor_run_idempotent
ON question_monitor_runs(tenant_id, brand_id, question_version_id, ai_platform_id, run_mode, business_date);
```
#### 2.2.3 `question_monitor_parse_results`
新增以下字段:
1. `run_mode VARCHAR(30) NOT NULL`
2. `search_hit BOOLEAN NOT NULL DEFAULT false`
3. `search_info_json_key TEXT`
4. `extractor_version VARCHAR(30) NOT NULL DEFAULT 'v1'`
#### 2.2.4 新增任务与质量表
新增下列表:
1. `monitoring_collect_tasks`:调度任务、重试、死信、优先级、租约状态
2. `monitoring_citation_facts`:引用规范化事实表,保存 `normalized_url/domain/citation_hash`
3. `monitoring_parse_badcases`:低置信度样本与人工回标结果
#### 2.2.5 汇总表统一增加 `run_mode`
以下汇总表必须增加 `run_mode`,并进入唯一键:
1. `monitoring_brand_daily_overview`
2. `monitoring_platform_daily`
3. `monitoring_platform_question_daily`
4. `monitoring_competitor_daily`
5. `monitoring_competitor_platform_daily`
6. `monitoring_topic_daily`
7. `monitoring_question_daily`
8. `monitoring_citation_source_daily`
9. `monitoring_citation_page_daily`
10. `monitoring_brand_impression`
#### 2.2.6 问题版本进入汇总主键
以下表必须增加 `question_version_id` 并进入唯一键:
1. `monitoring_platform_question_daily`
2. `monitoring_question_daily`
推荐唯一键修订:
```sql
UNIQUE (tenant_id, brand_id, ai_platform_id, question_id, question_version_id, run_mode, metric_date)
UNIQUE (tenant_id, brand_id, question_id, question_version_id, run_mode, metric_date)
```
#### 2.2.7 引用汇总改为从规范化事实表聚合
`monitoring_citation_source_daily``monitoring_citation_page_daily` 不再直接从 `citation_urls JSONB` 聚合,而是从 `monitoring_citation_facts` 聚合:
1. 页面级汇总按 `normalized_url`
2. 站点级汇总按 `domain` / `site_name`
3. 原始 `page_url` 仅用于回放与调试
---
## 3. 读流量模型与缓存架构
### 3.1 页面 Fan-out 矩阵
V2 错误地按"每人 30 秒 1 个请求"建模。实际上,每个页面打开时会 **并行** 发出多个 API 请求。
| 页面 | 并行请求数 | 对应 API |
| --- | --- | --- |
| 主页 Dashboard | 6 | overview, exposure-trend, competitors, citation-sources, recent-questions, brand-impression |
| 平台占比分析 | 2 | platform-stats, platform-questions |
| 竞争对手分析 | 2 | competitor-exposure, competitor-detail |
| 业务主题分析 | 1 | topics |
| AI 对话问题 | 1 | questions (分页) |
| AI 引用排名 | 2 | citation-trend, citation-pages |
加权平均(按访问占比估算:主页 50%、其他页各 10%):
```
weighted_fan_out = 0.50 × 6 + 0.10 × 2 + 0.10 × 2 + 0.10 × 1 + 0.10 × 1 + 0.10 × 2
= 3.0 + 0.2 + 0.2 + 0.1 + 0.1 + 0.2
= 3.8 次/页面打开
```
### 3.2 QPS 模型
| 场景 | 计算 | QPS |
| --- | --- | --- |
| 稳态(50K 用户,平均 30 秒交互 1 次) | 50000 × 3.8 / 30 | **6,333** |
| 突刺(早会/开盘集中打开,10 秒内 30% 用户首屏加载) | 15000 × 6 / 10 | **9,000** |
| 筛选切换(20% 用户同时切换品牌/时间范围) | 10000 × 3.8 / 5 | **7,600** |
| 设计容量(取 P99 峰值 × 1.5 安全余量) | max(6333, 9000, 7600) × 1.5 | **~13,500** |
**结论**:系统必须在 Redis 层稳定承接 13,500 QPS。穿透到 PG 的流量目标 < 1,350 QPS10% miss rate)。
### 3.3 BFF 聚合层(减少 fan-out 的关键手段)
为降低首页 6 次并行请求的压力,引入 **BFF 聚合接口**
```
GET /api/tenant/monitoring/dashboard/composite
→ 服务端并行查询 6 个数据源,一次返回全部首页数据
→ 默认固定 `scope=standard`
→ 单次 Redis GET 命中完整 composite 缓存即可返回
→ 缓存 miss 时服务端内部 goroutine 并行查 6 个汇总表
```
前端首屏只发 **1 个请求**,极端情况下用户手动刷新某个区块再发单接口。
| 方案 | 首页 QPS (50K 用户) | Redis 压力 |
| --- | --- | --- |
| 6 个独立接口 | ~10,000 | 60K ops/s |
| composite 聚合 | ~1,667 | 1,667 ops/s |
其他页面 fan-out ≤ 2,不需要聚合。
### 3.4 缓存防雪崩机制
V2 仅使用 TTL + 聚合后全量删除,存在雪崩风险。V3 引入 4 层防护:
#### 3.4.1 Singleflight
同一 cache key 的并发 miss 请求,只放行 1 个打到 PG,其余等待结果。
```go
import "golang.org/x/sync/singleflight"
var sf singleflight.Group
func (s *MonitoringService) getWithCache(ctx context.Context, key string, ttl time.Duration, loader func() (any, error)) (any, error) {
// 1. 尝试 Redis GET
if cached, err := s.cache.Get(ctx, key); err == nil {
return cached, nil
}
// 2. Singleflight: 同 key 只执行一次 loader
result, err, _ := sf.Do(key, func() (any, error) {
data, err := loader()
if err != nil {
return nil, err
}
// 写回 RedisTTL 加随机抖动
jitteredTTL := ttl + time.Duration(rand.Int63n(int64(ttl/5)))
_ = s.cache.Set(ctx, key, data, jitteredTTL)
return data, nil
})
return result, err
}
```
#### 3.4.2 TTL 随机抖动
防止大量 key 同时过期导致集中穿透。
```
实际 TTL = 基础 TTL + random(0, 基础 TTL × 20%)
```
例如 5 分钟基础 TTL → 实际 300~360 秒。
#### 3.4.3 Stale-While-Revalidate
缓存过期后,先返回旧数据,后台异步刷新。用户不感知延迟。
```go
// Redis 存储两个 key:
// mon:{key} → 数据 (TTL = base_ttl)
// mon:{key}:stale → 数据 (TTL = base_ttl × 3,作为兜底)
//
// 读取流程:
// 1. GET mon:{key} → 命中 → 返回
// 2. miss → GET mon:{key}:stale → 命中 → 返回 stale 数据 + 异步触发刷新
// 3. 都 miss → 同步查 PG + singleflight
```
#### 3.4.4 热点保护(Hot Key
对高频访问的品牌(如首页默认品牌),使用 **进程内 L1 缓存**Go sync.Map 或 groupcache):
```
请求 → L1 进程缓存 (TTL 10s) → Redis (TTL 5min) → PG
```
L1 缓存的极短 TTL 保证数据不会太旧,但能挡住 Redis 的瞬时压力。
### 3.5 缓存 Key 修订
修复 V2 的分页缓存 key 缺少 `page_size` 的问题,同时将统计口径 `scope` 纳入缓存维度。
约定:
1. `scope=standard` 对应 `run_mode=api_standard`
2. `scope=search` 对应 `run_mode=api_search_grounded`
3. `ui_sample` 不进入线上缓存
```
# 分页接口必须包含 scope + page + page_size
mon:{tenant}:{brand}:platform:{pid}:questions:{days}:{scope}:{page}:{pageSize}
mon:{tenant}:{brand}:questions:{pid}:{days}:{scope}:{page}:{pageSize}
mon:{tenant}:{brand}:citation_pages:{days}:{scope}:{page}:{pageSize}
# composite 聚合接口
mon:{tenant}:{brand}:dashboard_composite:{days}:{scope}
# 非分页接口也加 scope
mon:{tenant}:{brand}:overview:{days}:{scope}
mon:{tenant}:{brand}:trend:{days}:{scope}
mon:{tenant}:{brand}:competitors:{days}:{scope}
...
```
### 3.6 缓存失效策略修订
V2 的"聚合后删除品牌全部缓存"策略改为 **渐进式失效**
| 时机 | 操作 | 原因 |
| --- | --- | --- |
| 聚合 Job 完成 | **不删除** 现有缓存 | 避免 5 万用户同时穿透 |
| 聚合 Job 完成 | 异步预热 Top 20 热门品牌的 composite 缓存 | 用新数据替换旧缓存 |
| 聚合 Job 完成 | 设置全局标记 `mon:agg_version:{tenant}` +1 | 通知各实例 L1 缓存过期 |
| 自然过期 | TTL 到期后正常 singleflight 回源 | 冷数据自然刷新 |
### 3.7 修订后的 TTL 矩阵
| 数据类型 | Redis TTL | Stale TTL | L1 TTL | 说明 |
| --- | --- | --- | --- | --- |
| Dashboard composite | 5 min | 15 min | 10 s | 首页入口,高频访问 |
| Overview 指标卡 | 5 min | 15 min | 10 s | 同上 |
| 趋势图 | 15 min | 45 min | 30 s | 日级数据,变化慢 |
| 竞品排名 | 15 min | 45 min | 30 s | 同上 |
| 引用来源 | 15 min | 45 min | 30 s | 同上 |
| 业务主题 | 30 min | 90 min | 60 s | 低频变更 |
| 问题列表(分页) | 10 min | 30 min | 不缓存 | 粒度太细,L1 命中率低 |
| 品牌印象 | 30 min | 90 min | 60 s | 日级聚合 |
| AI 平台列表 | 1 hour | 3 hour | 5 min | 几乎不变 |
---
## 4. 采集流水线扩容
### 4.1 V2 瓶颈分析
V2 的问题:
- 5 worker、50,000 任务 → 每任务仅 0.72 秒,不现实
- 凌晨 1 点采集、3 点聚合 → 2 小时时间窗太紧
- Collector ×1、Aggregator ×1 → 单点故障
### 4.2 任务量重新估算
```
标准模式:
100 品牌 × 100 问题/品牌 × 5 AI 平台 = 50,000 任务/天
搜索增强模式(首批仅 3 平台: qwen / doubao / hunyuan:
100 品牌 × 100 问题/品牌 × 3 AI 平台 = 30,000 任务/天
合计 = 80,000 任务/天
# 但问题 hash 去重后(V1 设计)
假设去重率 30% → 实际 56,000 唯一任务
# 每次 AI API 调用 + 解析 + 入库:
平均耗时: 8 秒 (含 AI 生成 5s + 解析 2s + 入库 1s)
P99 耗时: 20 秒 (含重试)
```
### 4.3 Worker 池设计
```
所需总处理时间 = 56,000 × 8s = 448,000 秒
可用采集时间窗 = 6 小时 = 21,600 秒 (0:00 ~ 6:00)
所需并发 worker = 448,000 / 21,600 ≈ 21
安全系数 1.4 → 30 worker
```
**架构**
```
┌─────────────────────────────────────────────────────┐
│ 采集调度器 (Scheduler) │
│ • 凌晨 0:00 启动 │
│ • 生成 business_date = yesterday 的全部任务 │
│ • 任务维度: 品牌 × 去重问题 × 平台 × run_mode │
│ • 写入 Queue Redis Stream: mon:collect:tasks │
└───────────────────────┬─────────────────────────────┘
┌─────────────┼─────────────┬─────────────┐
▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ Collector │ │ Collector │ │ Collector │ │ Collector │
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │ │ standby HPA│
│ worker ×10 │ │ worker ×10 │ │ worker ×10 │ │ 按需扩容 │
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────┐
│ Queue Redis Stream │
│ mon:collect:tasks (pending → ack) │
│ Consumer Group: collectors │
└─────────────────────────────────────────┘
```
**为什么用 Redis Stream 而不是内存队列**
- 多 Pod 竞争消费,自动负载均衡
- 消息持久化,Pod 重启不丢任务
- Consumer Group 的 XACK 机制提供 at-least-once 保证
- 但必须使用 **独立 Queue Redis 故障域**,不能和在线缓存共用实例
### 4.4 时间窗修订
```
0:00 调度器生成 business_date = yesterday 的全部采集任务 → Queue Redis Stream
0:00 ~ 6:00 Collector workers 消费任务,调用 AI API + 解析 + 写 PG/MinIO
6:00 检查未完成任务数量,告警如果 > 5%
6:10 将未完成任务转入 backfill 队列,不阻塞晨间主聚合
6:30 Aggregator 聚合 business_date = yesterday 且 status = succeeded 的数据
7:00 聚合完成 → 预热热门品牌缓存
7:30 T+1 数据上线,用户访问“昨日完整快照”
```
说明:
1. `run_at` 是实际执行时间,可能发生在次日凌晨。
2. `business_date` 是统计归属日,Dashboard 晨间看到的是 T+1 快照。
3. 晚到任务走 backfill,单独触发品牌级重聚合,不污染主发布窗口。
预留 6 小时采集窗口(vs V2 的 2 小时),即使 P99 延迟也有足够 buffer。
### 4.5 采集重试与限流
```go
// 每个 AI 平台独立的限流器
type PlatformRateLimiter struct {
limiters map[string]*rate.Limiter // provider_key → limiter
}
// 默认限流配置
var defaultLimits = map[string]rate.Limit{
"deepseek": 10, // 10 QPS
"qwen": 10,
"doubao": 10,
"hunyuan": 5,
"ernie": 5,
}
// 重试策略
const (
maxRetries = 3
retryBaseWait = 2 * time.Second // 指数退避: 2s, 4s, 8s
)
```
此外,Collector 必须基于 `run_mode` 和平台能力做任务路由:
1. 不支持 `api_search_grounded` 的平台不生成搜索增强任务。
2. `ui_sample` 任务只进入 PoC / badcase 流水线,不进入主聚合队列。
3. `api_standard``api_search_grounded` 使用不同的 prompt 模板版本。
### 4.6 采集容错
```go
// 单条任务失败不阻塞批次
func (w *CollectorWorker) processTask(ctx context.Context, task CollectTask) {
for attempt := 1; attempt <= maxRetries; attempt++ {
err := w.collect(ctx, task)
if err == nil {
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
return
}
if isRateLimited(err) {
time.Sleep(retryBaseWait * time.Duration(1<<(attempt-1)))
continue
}
// 非限流错误直接标记失败
w.markFailed(ctx, task, err)
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
return
}
w.markFailed(ctx, task, fmt.Errorf("max retries exceeded"))
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
}
```
### 4.7 聚合 Job 扩容
```
┌───────────────────────────────────────────────────────┐
│ Aggregator Job │
│ • 6:30 启动(聚合 business_date = yesterday
│ • 按租户 × 品牌分片,每片独立事务 │
│ • 并发聚合 goroutine: 10 │
│ • 聚合完一个品牌 → 异步预热该品牌缓存 │
│ • 全部完成后 → 全局 agg_version +1 │
└───────────────────────────────────────────────────────┘
```
聚合 Job 部署 **2 个实例**(主备),使用 **PG advisory lock** 选主,只有一个实例实际运行,另一个待命。
---
## 5. 部署拓扑与资源
### 5.1 修正:连接池是 per-process 的
V2 错误地把 `max_open_conns: 50` 当作"3 个实例共享"。实际上 `bootstrap.go``pgxpool.Pool` 是每个进程独立初始化的。
**修正后的连接池规划**
```yaml
# 每个 API 实例
database:
max_open_conns: 20 # 每实例 20
max_idle_conns: 5
# 3 个 API 实例 → PG 总连接: 60
# 3 个 Collector 实例 → PG 总连接: 30 (各 10)
# 1 个 Aggregator 实例 → PG 总连接: 20
# 合计: 110 连接
# PostgreSQL 需配置:
# max_connections = 160 (留 50 余量给运维和监控)
```
### 5.1.1 配置模型 Delta
V3 不能继续沿用当前项目的单 `database`、单 `redis`、单 `llm` 配置模型,必须扩展为以下结构:
```yaml
database:
primary:
host: pg-primary
...
replica:
host: pg-replica
...
redis:
cache:
sentinel:
master_name: geo-cache-master
addrs: ["cache-sentinel-1:26379", "cache-sentinel-2:26379", "cache-sentinel-3:26379"]
db: 0
queue:
sentinel:
master_name: geo-queue-master
addrs: ["queue-sentinel-1:26379", "queue-sentinel-2:26379", "queue-sentinel-3:26379"]
db: 0
monitoring:
providers:
deepseek:
enabled: true
supports_standard: true
supports_search_grounded: false
qwen:
enabled: true
supports_standard: true
supports_search_grounded: true
doubao:
enabled: true
supports_standard: true
supports_search_grounded: true
hunyuan:
enabled: true
supports_standard: true
supports_search_grounded: true
ernie:
enabled: false # PoC 通过后再开启
```
这意味着:
1. `server/internal/shared/config/config.go` 必须扩展结构体,不是“小改动”。
2. `bootstrap.go` 必须初始化双 DB pool、双 Redis client、provider registry。
3. 监测模块必须摆脱全局单 provider `llm.Client` 的限制。
### 5.2 部署架构
```
┌─────────────────────────────┐
│ CDN (静态资源) │
│ admin-web 打包后的 JS/CSS │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Nginx / 云 LB (SLB/ALB) │
│ API 路由 + 健康检查 │
│ 限流: 每 IP 200 req/s │
└──────────┬───────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ tenant-api │ │ tenant-api │ │ tenant-api │
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │
│ API 20conn │ │ API 20conn │ │ API 20conn │
│ L1 cache │ │ L1 cache │ │ L1 cache │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────┴─────────────────────┴─────────────────────┴──────┐
│ Cache Redis 6+ (主从 Sentinel) │
│ 用途: API 缓存 + JWT 会话 │
└────────────────────────┬─────────────────────────────────┘
┌────────────────────────┼────────────────────────┬────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PG Primary │────────▶│ PG Replica │ │ MinIO │ │ Queue Redis │
│ (读写) │ 流复制 │ (只读) │ │ (原始回答) │ │ Streams │
│ 160 conns │ │ 50 conns │ │ S3 兼容 │ │ 独立故障域 │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
▲ ▲ │
│ │ │
│ ┌────────────────────┘ Aggregator 的重查询走 Replica │
│ │ │
┌──────┴───┴──────────────────────────────────────────────┐
│ 独立进程(不接入 LB) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ Collector Pod #1 │ │ Collector Pod #2 │ │ Collector Pod #3 │ ×3 Pod
│ │ worker ×10 │ │ worker ×10 │ │ worker ×10 │ (竞争消费)
│ │ PG conn: 10 │ │ PG conn: 10 │ │ PG conn: 10 │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │ │
│ └────────────────────┴────────────────────┘
│ │
│ ▼
│ Queue Redis Stream
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Aggregator #1 │ │ Aggregator #2 │ 主备(锁选主)│
│ │ (active) │ │ (standby) │ │
│ │ PG conn: 20 │ │ PG conn: 0 │ │
│ │ 读走 Replica │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
╚══════════════════════════════════════════════════════════╝
```
### 5.3 Redis 高可用与故障域隔离
V2 使用 Redis 单点,且在线缓存与离线任务共用同一实例。V3 修正为 **双 Redis 故障域**
1. `redis.cache`:面向 API 读流量,承接缓存与会话
2. `redis.queue`:面向离线采集,承接 Stream 队列
推荐配置:
```yaml
redis:
cache:
sentinel:
master_name: "geo-cache-master"
addrs:
- "cache-sentinel-1:26379"
- "cache-sentinel-2:26379"
- "cache-sentinel-3:26379"
db: 0
queue:
sentinel:
master_name: "geo-queue-master"
addrs:
- "queue-sentinel-1:26379"
- "queue-sentinel-2:26379"
- "queue-sentinel-3:26379"
db: 0
```
Go 客户端(go-redis v9)原生支持 Sentinel,但这里需要初始化 **两个** client
```go
cacheRDB := newRedisClient(cfg.Redis.Cache)
queueRDB := newRedisClient(cfg.Redis.Queue)
```
### 5.4 PostgreSQL 读写分离
API 服务的监测查询走 **PG Replica**。通过在 `MonitoringService` 中注入两个 pool
```go
type MonitoringService struct {
writePool *pgxpool.Pool // Primary,仅供 aggregator 写入
readPool *pgxpool.Pool // ReplicaDashboard 查询
cache cache.Cache
sf singleflight.Group
}
```
`bootstrap.go` 中初始化时,如果配置了 `database.replica`,则创建第二个连接池。
### 5.5 资源规格修订
| 组件 | 实例数 | 规格 | PG 连接 | 说明 |
| --- | --- | --- | --- | --- |
| **tenant-api** | 3 | 4C8G | 20/实例 | API 含 L1 缓存、singleflight |
| **Cache Redis Sentinel** | 1主1从3哨兵 | 8G 内存 | - | API 缓存 + JWT 会话 |
| **Queue Redis Sentinel** | 1主1从3哨兵 | 4G 内存 | - | Stream 队列,独立故障域 |
| **PG Primary** | 1 | 8C32G | max 160 | SSD,汇总表 + 原始采集表 |
| **PG Replica** | 1 | 4C16G | max 50 | SSDDashboard 只读查询 |
| **MinIO** | 1 | 4C8G, 100G SSD | - | 原始回答对象存储 |
| **Collector** | 3 Pod | 2C4G | 10/Pod | Queue Redis Stream 竞争消费 |
| **Aggregator** | 2 Pod (主备) | 4C8G | 20 (active only) | 分布式锁选主 |
| **CDN** | - | - | - | 静态资源 + Nginx 限流 |
---
## 6. 降级与容灾
### 6.1 降级策略
| 故障场景 | 降级措施 | 用户感知 |
| --- | --- | --- |
| **Cache Redis 主节点故障** | Sentinel 自动切换从节点为主(< 30s) | 短暂超时后恢复 |
| **Cache Redis 完全不可用** | API 直接查 PG,跳过缓存;Nginx 层限流降至 50 req/s/IP | 响应变慢但可用 |
| **Queue Redis 故障** | 采集调度与 Collector 暂停;线上查询不受影响 | 当天数据延迟,但 Dashboard 继续服务旧数据 |
| **PG Primary 故障** | Dashboard 查询走 Replica 不受影响;采集/聚合暂停 | Dashboard 可用,数据冻结在最后聚合时间 |
| **PG Replica 故障** | Dashboard 查询回退到 Primary | 无感知,Primary 负载上升 |
| **单个 AI 平台 API 超时** | 该平台任务标记 failed,其他平台正常采集 | 该平台在对应模式下标记“数据缺失”,不强制展示为 0 |
| **全部 AI 平台不可用** | 当天采集任务全部失败,Dashboard 展示前一天数据 | 显示"数据更新时间:昨日" |
| **Collector Pod 全挂** | Queue Redis 消息堆积,等待 Pod 恢复后继续消费 | 当天数据延迟 |
| **Aggregator 异常** | 另一个 Aggregator Pod 抢锁接管 | 无感知 |
### 6.2 熔断器
对外部 AI 平台 API 使用熔断器(circuit breaker):
```go
// 每个 AI 平台独立的熔断器
type PlatformBreaker struct {
breakers map[string]*circuitbreaker.CircuitBreaker
}
// 配置
// 连续 5 次失败 → 打开熔断 → 30 秒后半开 → 1 次成功 → 关闭
```
### 6.3 监控告警
| 指标 | 告警阈值 | 通知方式 |
| --- | --- | --- |
| API P99 延迟 | > 2s 持续 5 分钟 | 钉钉/飞书 |
| Cache Redis 命中率 | < 80% 持续 10 分钟 | 钉钉/飞书 |
| Queue Redis backlog | > 10,000 持续 10 分钟 | 钉钉/飞书 |
| PG 连接池使用率 | > 80% | 钉钉/飞书 |
| 采集任务失败率 | > 10% 单平台 | 钉钉/飞书 |
| 聚合 Job 未在 8:00 前完成 | 超时 | 电话 + 钉钉 |
| Cache/Queue Redis Sentinel 主从切换 | 任何切换事件 | 电话 + 钉钉 |
### 6.4 数据一致性保障
| 场景 | 措施 |
| --- | --- |
| 聚合 Job 被中断 | 每个品牌的聚合是独立事务,失败后可重跑该品牌 |
| 采集与聚合时间窗重叠 | 聚合只处理 `business_date = yesterday AND status = succeeded` 的数据 |
| 缓存与 PG 不一致 | stale-while-revalidate 保证最终一致,不保证强一致 |
| 原始回答写 MinIO 失败 | 标记该 run 为 failed,解析结果不写入 |
| 晚到补采任务 | 进入 backfill 队列,只重聚合受影响品牌,不改写已发布快照的全局口径 |
---
## 7. 压测验收标准
### 7.1 压测场景模型
| 场景编号 | 场景名称 | 虚拟用户数 | 行为 | 持续时间 |
| --- | --- | --- | --- | --- |
| S1 | 稳态浏览 | 50,000 | 每 30 秒打开 1 个页面(加权 fan-out 3.8 | 30 min |
| S2 | 早高峰突刺 | 50,000 | 0~60 秒内全部打开首页(composite 接口) | 1 min ramp-up |
| S3 | 筛选风暴 | 20,000 | 每 5 秒切换品牌/时间范围 | 10 min |
| S4 | 缓存失效 | 50,000 | 手动清空 Cache Redis 后观察恢复 | 5 min |
| S5 | Cache Redis 故障 | 50,000 | kill Cache Redis master,观察 Sentinel 切换 | 2 min |
| S6 | Queue Redis 故障 | 0 | kill Queue Redis master,观察采集暂停与恢复 | 30 min |
### 7.2 性能目标(SLA
| 指标 | 目标值 | 说明 |
| --- | --- | --- |
| API P50 延迟 | < 50 ms | 稳态,Redis 命中 |
| API P95 延迟 | < 200 ms | 稳态 |
| API P99 延迟 | < 500 ms | 含缓存 miss + PG 查询 |
| API P99 延迟(突刺) | < 2 s | S2 场景 |
| 错误率 | < 0.1% | 5xx 错误 |
| Cache Redis 命中率 | > 90% | 稳态 |
| PG QPS(穿透) | < 1,500 | 10% miss rate |
| 采集完成率 | > 95% | 日采集任务成功率 |
| 聚合 SLA | 8:00 前完成 | 用户上班前数据可用 |
| Cache Redis 故障恢复 | < 30 s | Sentinel 自动切换 |
| Queue Redis 恢复后 backlog 清空 | < 60 min | 采集可恢复性 |
### 7.3 压测工具与脚本
```
# 推荐工具: k6 (grafana/k6)
# 脚本位置: tests/load/
tests/load/
├── k6.config.js # 共享配置
├── scenarios/
│ ├── s1_steady_browse.js # 稳态浏览
│ ├── s2_peak_burst.js # 早高峰突刺
│ ├── s3_filter_storm.js # 筛选风暴
│ ├── s4_cache_flush.js # 缓存失效
│ └── s5_cache_redis_failover.js # Cache Redis 故障
├── helpers/
│ ├── auth.js # 登录获取 token
│ └── brands.js # 随机品牌选择
└── README.md # 运行说明(S6 Queue Redis 故障为运维演练,无 k6 脚本)
```
### 7.4 压测前置条件
1. **数据准备**:100 品牌 × 100 问题 × 5 平台 × 30 天汇总数据已填充
2. **用户池**:50,000 个预创建的测试用户 token
3. **环境**:与生产同规格的独立压测环境
4. **监控**Prometheus + Grafana 大盘实时观察
### 7.5 验收流程
```
Phase 1: 基线测试
→ 5,000 用户稳态,确认 P99 < 100ms
→ 记录基线指标
Phase 2: 容量爬坡
→ 逐步提升: 10K → 20K → 30K → 40K → 50K
→ 每级停留 5 分钟,观察指标是否劣化
→ 记录每级的 P50/P95/P99、错误率、Redis/PG 指标
Phase 3: 极限测试
→ 50K 稳态 30 分钟
→ 中间插入 S2 突刺、S3 筛选风暴
→ 确认所有 SLA 指标达标
Phase 4: 容灾演练
→ S4 缓存失效:确认 singleflight 生效,PG 不被打爆
→ S5 Cache Redis 故障:确认 Sentinel 切换 < 30sAPI 降级可用
→ S6 Queue Redis 故障:确认在线 API 不受影响,采集恢复后 backlog 可消化
Phase 5: 报告
→ 输出压测报告,包含:
- 每个场景的延迟分布图
- 资源使用率(CPU/内存/连接池/磁盘IO)
- 瓶颈分析与调优建议
- 通过/未通过结论
```
---
## 8. 实施计划修订
在 V2 原有 Phase 15~21 的基础上,调整以下 Phase:
### Phase 15 修订:DB 迁移 + 数据口径
新增:
- `run_mode``business_date`、平台能力字段 migration
- `question_version_id` 进入问题级汇总唯一键
- 新增 `monitoring_collect_tasks` / `monitoring_citation_facts` / `monitoring_parse_badcases`
- 引用聚合改为从规范化事实表出发
### Phase 16 修订:后端查询服务
新增:
- `getWithCache()` 通用缓存方法 + singleflight + stale-while-revalidate
- Dashboard composite 聚合接口
- `MonitoringService` 注入 readPool/writePool
- L1 进程内缓存层
- 所有查询默认显式带 `run_mode` / `scope`
### Phase 17 修订:采集流水线
新增:
- Queue Redis Stream 任务队列替代内存队列
- `MonitoringProvider` 平台适配器注册表
- `PlatformRateLimiter` 每平台独立限流
- `PlatformBreaker` 熔断器
- 多 Pod 竞争消费架构
- 重试 + 容错逻辑
- `business_date` + backfill 流程
- `api_standard` / `api_search_grounded` 分流调度
### Phase 18 修订:聚合 Job
新增:
- 分布式锁选主
- 聚合后预热热门品牌缓存(替代全量删除)
- 并发 goroutine 聚合(10 并发)
- 聚合读走 PG Replica
- 晚到任务品牌级重聚合
- 引用事实表 → 日汇总表的聚合逻辑
### 新增 Phase 22: 基础设施
- Cache Redis Sentinel 部署配置
- Queue Redis Sentinel 部署配置
- PG 流复制 + Replica 配置
- Nginx 限流配置
- 监控告警配置(Prometheus + Grafana
- 估时:3 天
### 新增 Phase 23: 压测与验收
- 编写 k6 压测脚本(5 个压测场景)+ 1 个运维演练场景
- 数据准备脚本(100 品牌 × 30 天种子数据)
- 执行 Phase 1~4 验收流程
- 输出压测报告
- 估时:3 天
### 新增 Phase 24: 平台 PoC 与口径验收
- 3 品牌 × 30 问题 × 3 天的平台 PoC
- 验证搜索增强引用质量与波动
- 决定百度千帆是否进入首批上线白名单
- 估时:4 天
### 修订后总估时
| Phase | 内容 | V2 估时 | V3 估时 | 增量 |
| --- | --- | --- | --- | --- |
| 15 | DB 迁移 + 数据口径 | 1 天 | 3 天 | +2run_mode、business_date、任务/引用事实表) |
| 16 | 查询服务 + 缓存层 | 3 天 | 6 天 | +3singleflight、composite、L1、读写分离、scope 过滤) |
| 17 | 采集流水线 | 3 天 | 7 天 | +4Provider Registry、双 run_mode、Queue Redis、多 Pod、限流熔断) |
| 18 | 聚合 Job | 2 天 | 4 天 | +2(分布式锁、预热、并发聚合、backfill) |
| 19 | 种子数据 | 1 天 | 1 天 | 0 |
| 20 | 前端 6 页面 + ECharts | 4 天 | 4 天 | 0 |
| 21 | 前端验证 + 集成测试 | 1 天 | 1 天 | 0 |
| 22 | 基础设施(新增) | - | 3 天 | +3 |
| 23 | 压测与验收(新增) | - | 3 天 | +3 |
| 24 | 平台 PoC 与口径验收(新增) | - | 4 天 | +4 |
| **合计** | | **15 天** | **32 天** | **+17 天** |
---
## 9. V2 → V3 变更汇总
| 问题编号 | V2 问题 | V3 解决方案 |
| --- | --- | --- |
| **H0** | `run_mode` 未入模,标准回答与搜索增强回答混算 | 主统计按 `api_standard` / `api_search_grounded` 分层,默认页面显式取数规则 |
| **H1** | 读流量按 1 req/30s 估算,实际首页 6 个并行请求 | 重新建模 fan-out,引入 BFF composite 接口将首页降为 1 请求 |
| **H2** | TTL + 聚合后全删缓存,无防雪崩 | 4 层防护:singleflight + TTL 抖动 + stale-while-revalidate + L1 热点缓存 |
| **H3** | 5 worker、2 小时时间窗,采集跑不完 | 30 worker、6 小时窗口、Queue Redis 多 Pod 竞争消费 |
| **H4** | 问题版本未进入全部汇总唯一键 | `question_version_id` 进入问题级汇总唯一键,避免历史口径覆盖 |
| **H5** | 日采集与日聚合时序矛盾 | 使用 `business_date = yesterday` 的 T+1 发布模型 + backfill |
| **M1** | max_open_conns 50 被当作共享,实际 per-process | 显式规划每进程连接数,PG max_connections=160 |
| **M2** | 缺少压测验收 | 6 个场景模型 + SLA 指标 + k6 脚本 + 4 阶段验收流程 |
| **M3** | 分页缓存 key 缺 page_size | 所有分页 key 加入 `scope + page_size` 维度 |
| **M4** | 引用按原始 URL 聚合,易重复计数 | 引入 `monitoring_citation_facts`,按 `normalized_url/domain` 聚合 |
| **M5** | 在线缓存与离线队列共用 Redis 故障域 | Cache Redis / Queue Redis 拆分,分别高可用 |
| **新增** | 无降级容灾策略 | 双 Redis Sentinel + PG 读写分离 + 熔断器 + 降级措施矩阵 |
| **新增** | 无监控告警 | 7 项核心告警指标 + 阈值 |
+24 -24
View File
@@ -126,24 +126,6 @@ importers:
apps/desktop-client:
dependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.31(typescript@5.9.3))
'@geo/http-client':
specifier: workspace:*
version: link:../../packages/http-client
'@geo/publisher-platforms':
specifier: workspace:*
version: link:../../packages/publisher-platforms
'@geo/shared-types':
specifier: workspace:*
version: link:../../packages/shared-types
'@geo/ui-shared':
specifier: workspace:*
version: link:../../packages/ui-shared
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.31(typescript@5.9.3))
better-sqlite3:
specifier: ^12.8.0
version: 12.9.0
@@ -159,16 +141,25 @@ importers:
playwright-core:
specifier: ^1.55.0
version: 1.59.1
vue:
specifier: ^3.5.31
version: 3.5.31(typescript@5.9.3)
vue-router:
specifier: ^4.5.1
version: 4.6.4(vue@3.5.31(typescript@5.9.3))
ws:
specifier: ^8.20.0
version: 8.20.0
devDependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.31(typescript@5.9.3))
'@geo/http-client':
specifier: workspace:*
version: link:../../packages/http-client
'@geo/publisher-platforms':
specifier: workspace:*
version: link:../../packages/publisher-platforms
'@geo/shared-types':
specifier: workspace:*
version: link:../../packages/shared-types
'@geo/ui-shared':
specifier: workspace:*
version: link:../../packages/ui-shared
'@playwright/test':
specifier: ^1.0.0
version: 1.59.1
@@ -181,6 +172,9 @@ importers:
'@vitejs/plugin-vue':
specifier: ^5.2.4
version: 5.2.4(vite@5.4.21(@types/node@24.12.0)(lightningcss@1.32.0))(vue@3.5.31(typescript@5.9.3))
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.31(typescript@5.9.3))
electron:
specifier: 41.2.0
version: 41.2.0
@@ -202,6 +196,12 @@ importers:
vitest:
specifier: ^2.0.0
version: 2.1.9(@types/node@24.12.0)(lightningcss@1.32.0)
vue:
specifier: ^3.5.31
version: 3.5.31(typescript@5.9.3)
vue-router:
specifier: ^4.5.1
version: 4.6.4(vue@3.5.31(typescript@5.9.3))
vue-tsc:
specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3)