406 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 云图报告自动创建 PRD
## 一、项目基本信息
- 项目路径:`/Users/wxs/projects/browser_script/scriptCat/yuntu/yuntuReportFilling`
- 项目类型ScriptCat 浏览器脚本 + Node.js 后端服务 + PostgreSQL
- 业务目标:在云图页面中复用最近一次成功创建报告的请求参数,自动创建一份同比报告,并将关键数据持久化到数据库
- 核心价值:
- 降低人工重复创建报告的成本
- 自动将时间范围回退一年,方便做同比分析
- 为后续追溯和排查保留完整 payload 与 report_id
## 二、业务背景
当前用户在云图页面手动创建报告后,还需要再次输入或复制相同配置,才能创建同比报告。该过程重复且容易出错。
本项目通过监听用户首次手动创建报告时的请求与响应,缓存最近一次成功的 payload当用户点击右侧按钮后脚本自动将顶层 `startTime``endTime` 各回退一年,并再次调用云图创建接口完成报告复制。
## 三、范围说明
### 本期范围
- 监听并识别云图“创建报告”接口
- 记录最近一次成功创建报告的完整 payload
- 在页面右侧提供“一键复制同比报告”按钮
- 自动回退顶层 `startTime` / `endTime` 一年
- 调用后端 API 持久化成功创建的报告数据
- 将手动创建报告和脚本自动创建报告统一写入 PostgreSQL
### 本期不做
- 不从数据库反向同步最新 payload 到浏览器本地
- 不支持多条历史 payload 管理
- 不支持用户手动编辑复制前的字段
- 不修改除顶层 `startTime` / `endTime` 外的其他字段
## 四、目标页面与第三方接口
### 4.1 目标页面
- 页面域名:`https://yuntu.oceanengine.com`
- 目标页面路径:`/yuntu_brand/ecom/product/segmentedMarketcreation`
- 页面示例:`https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketcreation?aadvid=1648829117232140`
### 4.2 需监听与复用的云图接口
- 请求方法:`POST`
- 接口路径:`/product_node/v2/api/segmentedMarket/createSegmentedMarket`
- 完整示例:
- `https://yuntu.oceanengine.com/product_node/v2/api/segmentedMarket/createSegmentedMarket?aadvid=1648829117232140`
- 关键识别条件:
- 请求方法必须为 `POST`
- 路径必须匹配 `createSegmentedMarket`
- 查询参数中包含 `aadvid`
### 4.3 第三方接口约束
- 浏览器脚本调用该接口时,必须复用当前用户浏览器会话中的登录态
- 不允许在脚本或后端中硬编码 Cookie、CSRF Token 等敏感信息
- 只允许浏览器脚本直接调用云图接口;后端不直接代调云图接口
- 接口成功后,从响应体 `data.reportId` 中提取业务 `report_id`
## 五、核心流程
### 5.1 手动创建报告并记录
触发时机:用户在页面中手动点击“创建报告”并且云图接口返回成功。
执行流程:
1. 监听 `createSegmentedMarket` 请求与响应。
2. 提取请求 payload 与请求 URL 中的 `aadvid`
3. 校验响应成功,且响应体存在 `data.reportId`
4. 将以下内容写入浏览器本地:
- `lastReportPayload`:最近一次成功创建报告的完整 payload
- `lastReportMeta`:最近一次成功创建报告的元信息,至少包含 `reportId``aadvid``sourceType``capturedAt`
5. 如果本地写入失败,立即报错,并保持按钮不可用状态;本次不调用后端持久化接口。
6. 如果本地写入成功,调用后端 API 将本次手动创建报告写入数据库。
### 5.2 一键复制同比报告
触发时机:用户点击页面右侧“一键复制同比报告”按钮。
执行流程:
1. 读取 `lastReportPayload``lastReportMeta`
2. 若本地不存在有效 payload则按钮保持置灰不允许点击。
3. 深拷贝 payload仅修改顶层 `startTime``endTime`
4. 使用修改后的 payload 调用云图创建接口。
5. 接口成功后,从响应体中读取新的 `data.reportId`
6. 先将新的 payload 和元信息覆盖写入本地存储。
7. 本地写入成功后,调用后端 API 持久化本次自动复制创建的报告数据。
8. 持久化成功后,给出成功提示,并显示新的 `report_id`
失败处理:
- 云图接口调用失败:提示“同比报告创建失败”,保留旧的本地 payload不调用后端 API。
- 本地覆盖写入失败:提示“本地缓存更新失败”,不调用后端 API按钮恢复可点击。
- 后端持久化失败:提示“报告已创建成功,但数据库记录失败”,保留新的本地 payload按钮恢复可点击。
## 六、时间处理规则
### 6.1 处理目标
将顶层 `startTime``endTime` 各回退一个自然年,用于生成同比报告。
### 6.2 处理原则
- 只处理 payload 顶层字段:
- `startTime`
- `endTime`
- 其他字段保持原值不变
- 日期格式统一为 `YYYY-MM-DD`
- 使用“自然年回退”规则,而不是简单减去 365 天
### 6.3 具体算法
1.`startTime``endTime` 解析为日期对象。
2. 分别将年份减 1尽量保持原始月与日不变。
3. 如果目标年份不存在该日期,则回退到目标月份最后一天。
示例:
```javascript
// 正常日期
2025-03-01 -> 2024-03-01
2026-02-28 -> 2025-02-28
// 闰年特殊情况
2024-02-29 -> 2023-02-28
```
该规则可以最大限度保持同比区间的自然日语义。
## 七、页面交互设计
### 7.1 按钮位置
- 按钮固定显示在页面右侧中部
- 文案建议:`一键复制同比报告`
### 7.2 按钮状态
- 默认状态:
- 当本地存在有效 `lastReportPayload` 时可点击
- 当本地不存在有效 `lastReportPayload` 时置灰且不可点击
- 加载状态:
- 点击后立即进入 loading
- loading 期间禁止再次点击,防止重复创建
- 完成状态:
- 成功后 toast 提示创建成功,并展示新 `report_id`
- 成功后不自动跳转页面,保持用户留在当前页面,避免打断连续操作
### 7.3 状态恢复规则
- 用户再次手动成功创建报告后,按钮状态根据本地缓存重新计算
- 任何失败场景都需要正确退出 loading 状态
## 八、浏览器本地存储设计
### 8.1 存储方式
- 使用 `GM_setValue` / `GM_getValue`
### 8.2 存储键
- `lastReportPayload`
- `lastReportMeta`
### 8.3 存储内容
`lastReportPayload`
```json
{
"endTime": "2026-02-28",
"name": "测试",
"periodType": "MONTH",
"startTime": "2025-03-01"
}
```
`lastReportMeta`
```json
{
"reportId": "123456789",
"aadvid": "1648829117232140",
"sourceType": "MANUAL_CAPTURE",
"capturedAt": "2026-03-31T15:00:00+08:00"
}
```
### 8.4 更新规则
- 每次成功创建报告后覆盖写入
- 始终只保留最近一次成功创建报告的数据
- 脚本启动时不从数据库同步数据
## 九、后端 API 设计
后端职责仅为持久化成功创建的报告数据,不直接调用云图接口。
### 9.1 保存报告记录
- 方法:`POST`
- 路径:`/api/reports`
- 用途:保存一条已创建成功的报告记录
请求体:
```json
{
"reportId": "123456789",
"sourceType": "AUTO_COPY",
"sourceReportId": "987654321",
"aadvid": "1648829117232140",
"name": "测试",
"price": ["1,100", "101,100000"],
"rules": [
{
"keywords": ["奶粉"],
"op": "INCLUDE"
}
],
"analysisDims": [
"MARKETOVERVIEW",
"PRODUCTINSIGHT",
"MARKETCOMPOSING",
"CONSUMERINSIGHT"
],
"categories": [],
"channels": ["ALL"],
"startTime": "2024-03-01",
"endTime": "2025-02-28",
"periodType": "MONTH",
"userName": "qichumiaosi@163.com",
"payload": {}
}
```
字段说明:
- `reportId`:云图响应中的 `data.reportId`
- `sourceType`:取值为 `MANUAL_CAPTURE``AUTO_COPY`
- `sourceReportId`
- `MANUAL_CAPTURE` 时可为空
- `AUTO_COPY` 时必填,表示复制来源报告 ID
- `aadvid`:从页面 URL 或请求 URL 中提取
- `payload`:完整请求体,原样保存
成功响应:
```json
{
"success": true,
"data": {
"id": 1,
"reportId": "123456789"
}
}
```
失败响应:
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "sourceReportId is required when sourceType is AUTO_COPY"
}
}
```
### 9.2 后端校验规则
- `reportId` 必填
- `sourceType` 必须为 `MANUAL_CAPTURE``AUTO_COPY`
- `AUTO_COPY` 场景下 `sourceReportId` 必填
- `payload` 必须为合法 JSON
- `startTime``endTime` 必须符合 `YYYY-MM-DD`
- 数据库以 `report_id` 做唯一约束,重复写入时返回成功并提示已存在,保证接口幂等
## 十、数据库设计
### 10.1 表设计说明
表名:`public.yuntu_report_info`
字段设计目标:
- 记录云图返回的业务 `report_id`
- 区分手动创建和自动复制
- 能追溯自动复制来源
- 保存结构化字段与完整 payload
### 10.2 建表 SQL
```sql
DROP TABLE IF EXISTS public.yuntu_report_info;
CREATE TABLE public.yuntu_report_info (
id BIGSERIAL PRIMARY KEY,
report_id VARCHAR(64) NOT NULL,
source_type VARCHAR(32) NOT NULL,
source_report_id VARCHAR(64) NULL,
aadvid VARCHAR(32) NOT NULL,
name TEXT NOT NULL,
price JSONB NOT NULL DEFAULT '[]'::jsonb,
rules JSONB NOT NULL DEFAULT '[]'::jsonb,
analysis_dims JSONB NOT NULL DEFAULT '[]'::jsonb,
categories JSONB NOT NULL DEFAULT '[]'::jsonb,
transaction_channels JSONB NOT NULL DEFAULT '[]'::jsonb,
start_time DATE NOT NULL,
end_time DATE NOT NULL,
period_type VARCHAR(32) NULL,
user_name TEXT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_yuntu_report_info_report_id UNIQUE (report_id),
CONSTRAINT chk_yuntu_report_info_source_type
CHECK (source_type IN ('MANUAL_CAPTURE', 'AUTO_COPY')),
CONSTRAINT chk_yuntu_report_info_copy_source
CHECK (
(source_type = 'MANUAL_CAPTURE' AND source_report_id IS NULL)
OR
(source_type = 'AUTO_COPY' AND source_report_id IS NOT NULL)
)
);
CREATE INDEX idx_yuntu_report_info_source_report_id
ON public.yuntu_report_info (source_report_id);
CREATE INDEX idx_yuntu_report_info_created_at
ON public.yuntu_report_info (created_at DESC);
CREATE INDEX idx_yuntu_report_info_aadvid_created_at
ON public.yuntu_report_info (aadvid, created_at DESC);
CREATE OR REPLACE FUNCTION public.set_yuntu_report_info_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_yuntu_report_info_updated_at ON public.yuntu_report_info;
CREATE TRIGGER trg_yuntu_report_info_updated_at
BEFORE UPDATE ON public.yuntu_report_info
FOR EACH ROW
EXECUTE FUNCTION public.set_yuntu_report_info_updated_at();
```
### 10.3 字段映射
| 数据库字段 | 来源字段 | 说明 |
| --- | --- | --- |
| `report_id` | `response.data.reportId` | 云图报告 ID |
| `source_type` | 脚本内部标记 | 手动捕获或自动复制 |
| `source_report_id` | `lastReportMeta.reportId` | 自动复制来源报告 ID |
| `aadvid` | query 参数 `aadvid` | 广告主 ID |
| `name` | `payload.name` | 报告名称 |
| `price` | `payload.price` | 价格区间 |
| `rules` | `payload.rules` | 规则配置 |
| `analysis_dims` | `payload.analysisDims` | 分析维度 |
| `categories` | `payload.categories` | 类目树 |
| `transaction_channels` | `payload.channels` | 渠道信息 |
| `start_time` | `payload.startTime` | 报告开始日期 |
| `end_time` | `payload.endTime` | 报告结束日期 |
| `period_type` | `payload.periodType` | 周期类型 |
| `user_name` | `payload.userName` | 用户名 |
| `payload` | 完整 payload | 原始请求体 |
## 十一、错误处理
### 11.1 浏览器脚本侧
- 拦截不到目标接口:不报错,不显示成功态
- 响应中缺少 `data.reportId`:提示“未获取到 report_id无法记录”
- 本地存储失败:立即报错,并终止当前脚本链路
- 用户重复点击:忽略后续点击并保持 loading
- 请求超时:提示失败并恢复按钮
### 11.2 后端侧
- 参数校验失败:返回 4xx 与明确错误信息
- 数据库写入失败:返回 5xx 与错误码
- 重复 `report_id` 写入:按幂等成功处理,不重复插入
## 十二、验收标准
1. 用户在目标页面手动成功创建报告后,脚本能够识别目标接口,并从响应体中正确提取 `data.reportId`
2. 手动创建成功后,本地必须成功写入 `lastReportPayload``lastReportMeta`;若写入失败,页面明确报错且按钮不可点击。
3. 手动创建成功且本地写入成功后,后端数据库新增一条 `source_type = 'MANUAL_CAPTURE'` 的记录,且 `report_id` 与云图响应一致。
4. 当本地不存在有效 payload 时,右侧按钮必须置灰且无法点击。
5. 点击“一键复制同比报告”后,脚本只能修改顶层 `startTime``endTime`,其他字段与来源 payload 保持一致。
6. 自动复制成功后,新报告的时间范围必须按自然年回退一年的规则生成;闰年日期按目标月最后一天处理。
7. 自动复制成功后,本地缓存必须被新 payload 和新 `reportId` 覆盖;数据库新增一条 `source_type = 'AUTO_COPY'` 的记录,且 `source_report_id` 正确指向来源报告。
8. 点击按钮进入 loading 后,用户无法重复触发创建;无论成功还是失败,流程结束后按钮状态必须恢复正确。
9. 后端接口重复接收同一个 `report_id` 时,不得插入重复数据。
10. 整个方案不得在代码或配置中硬编码用户 Cookie、Token 等敏感登录信息。