406 lines
14 KiB
Markdown
406 lines
14 KiB
Markdown
# 云图报告自动创建 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 等敏感登录信息。
|