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