From 08d9a2792a408fcb640d9f26d16a600488e7c8f8 Mon Sep 17 00:00:00 2001 From: R524809 Date: Thu, 24 Sep 2026 17:14:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=203.5=E7=89=88=E6=9C=AC=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudfunctions/homeAutoRefresh/index.js | 2 +- cloudfunctions/userDownloadLogs/index.js | 13 ++ docs/v3.5/README.md | 30 ++++ docs/v3.5/Web与服务端生成策略.md | 154 ++++++++++++++++++ docs/v3.5/产品路线图.md | 115 +++++++++++++ docs/v3.5/埋点事件字典.md | 25 +++ docs/v3.5/待办与里程碑.md | 124 ++++++++++++++ miniprogram/base/pageMixin.ts | 118 +++++++++++++- miniprogram/focusPages/focusDraw/focusDraw.ts | 9 + miniprogram/focusPages/shape/shape.ts | 19 +++ miniprogram/pages/category/category.data.ts | 2 +- miniprogram/pages/category/category.ts | 14 +- miniprogram/pages/home/home.ts | 22 +++ .../supportPages/focusIndex/focusIndex.ts | 6 +- .../unreleasedDebug/unreleasedDebug.config.ts | 2 +- miniprogram/utils/downloadLogs.ts | 8 +- miniprogram/utils/downloadPrint.ts | 29 +++- miniprogram/utils/favorites.ts | 17 +- miniprogram/utils/tracker.ts | 105 ++++++++++-- 19 files changed, 769 insertions(+), 45 deletions(-) create mode 100644 docs/v3.5/README.md create mode 100644 docs/v3.5/Web与服务端生成策略.md create mode 100644 docs/v3.5/产品路线图.md create mode 100644 docs/v3.5/埋点事件字典.md create mode 100644 docs/v3.5/待办与里程碑.md diff --git a/cloudfunctions/homeAutoRefresh/index.js b/cloudfunctions/homeAutoRefresh/index.js index dd3fb96..e3795b3 100644 --- a/cloudfunctions/homeAutoRefresh/index.js +++ b/cloudfunctions/homeAutoRefresh/index.js @@ -31,7 +31,7 @@ function toHomeDisplayItem(ws) { ageBand: ageBand(ws.ageMin || 0, ws.ageMax || 0), difficulty: DIFFICULTY_LABELS[ws.difficulty] || '基础', icon: '', - img: ws.previewImg || '', + previewImg: ws.previewImg || '', badge: ws.isNew ? 'NEW' : ws.isHot ? 'HOT' : '', path: ws.path || '', available: true, diff --git a/cloudfunctions/userDownloadLogs/index.js b/cloudfunctions/userDownloadLogs/index.js index 2d24caf..57a1b1c 100644 --- a/cloudfunctions/userDownloadLogs/index.js +++ b/cloudfunctions/userDownloadLogs/index.js @@ -36,13 +36,26 @@ exports.main = async (event) => { async function handleAdd(userId, event) { const { worksheetId } = event; + const exportId = String(event.exportId || '').trim(); if (!worksheetId) return { code: -1, message: 'worksheetId 不能为空' }; const collection = db.collection('download_logs'); + // 同一次成功导出重试时只能计一次,避免网络重试抬高下载量。 + if (exportId) { + const { data: existing } = await collection + .where({ userId, exportId }) + .limit(1) + .get(); + if (existing.length > 0) { + return { code: 0, data: { ...existing[0], duplicate: true } }; + } + } + const record = { userId, worksheetId, + ...(exportId && { exportId }), createdAt: db.serverDate(), }; const { _id } = await collection.add({ data: record }); diff --git a/docs/v3.5/README.md b/docs/v3.5/README.md new file mode 100644 index 0000000..d870390 --- /dev/null +++ b/docs/v3.5/README.md @@ -0,0 +1,30 @@ +# Doodle Mini v3.5 规划索引 + +> 状态:待评审 +> +> 更新日期:2026-09-24 + +v3.5 的目标不是继续堆叠题型,而是把「生成一张练习纸」升级为家长可持续使用的家庭打印学习服务,并为未来 Web 内容平台建立服务端生成能力。 + +## 本阶段的三项决策 + +1. **小程序仍是当前主产品。** 优先改善家长首次找到内容、完成打印、再次回来使用的体验。 +2. **创意涂色纸是下一项内容型 MVP。** 它覆盖低龄儿童、视觉传播力强,也最适合作为服务端生成的首个试点。 +3. **现在开始建设 Web 底座,不启动完整 Web 平台。** 新增高价值题型优先接入服务端生成;既有小程序题型不做一次性迁移。 + +## 文档 + +| 文档 | 用途 | +| --- | --- | +| [产品路线图](./产品路线图.md) | 用户问题、功能优先级、阶段目标与验收指标 | +| [待办与里程碑](./待办与里程碑.md) | 可执行的产品、设计、研发待办与依赖关系 | +| [Web 与服务端生成策略](./Web与服务端生成策略.md) | Web 试点范围、服务端渲染边界及技术演进决策 | + +## 不在 v3.5 范围内 + +- 完整的 Kiddoleap 风格 Web 资源平台、全量 SEO 内容站。 +- 一次性迁移所有云数据库、云函数和既有 Canvas 题型到独立服务器。 +- 社区晒图、公开儿童信息、复杂学习报告。 +- 会员支付、PDF 多页合并等商业化闭环;仅预留数据与服务端能力。 + +这些项目需要在 v3.5 的内容需求、留存和服务端生成成本被验证后再启动。 diff --git a/docs/v3.5/Web与服务端生成策略.md b/docs/v3.5/Web与服务端生成策略.md new file mode 100644 index 0000000..e05af75 --- /dev/null +++ b/docs/v3.5/Web与服务端生成策略.md @@ -0,0 +1,154 @@ +# Doodle Mini v3.5 Web 与服务端生成策略 + +> 状态:架构决策草案 +> +> 更新日期:2026-09-24 + +## 1. 决策结论 + +### 1.1 Web 的定位 + +Web 是长期的搜索获客、内容沉淀、电脑打印和教师/机构扩展渠道;小程序仍是当前的微信内打开、分享、每日推荐和家庭留存主产品。 + +因此,v3.5 采用: + +> **70% 投入小程序核心体验,30% 投入 Web 与服务端生成试点。** + +不等待小程序“达到很大用户量”才准备 Web,因为搜索内容、服务端渲染和内容生产机制都需要时间积累;但也不在内容库、内容生产和核心留存未验证时建设完整 Web 平台。 + +### 1.2 生成位置 + +未来 Web 的高价值题型采用**服务端生成与渲染优先**。浏览器负责选择参数、展示预览、发起下载和账户交互;不得承载完整题目生成规则、排版参数与私有素材。 + +小程序既有题型暂可保留客户端 Canvas 生成,以避免高风险的一次性重写。创意涂色纸及后续主题包、批量 PDF 等新能力必须按服务端模式实现。 + +## 2. 为什么不把核心生成放在 Web 客户端 + +Web 客户端代码、网络请求和静态资源可被用户查看、保存和分析。即使经过压缩或混淆,也只能提高复制成本,无法提供真正的保护。 + +服务端模式保护的是: + +- 题目随机规则、难度控制、答案逻辑。 +- 模板坐标、排版参数和批量内容生产能力。 +- 原始 SVG/高清线稿、字体、商用素材以及素材关联关系。 +- 未发布题型、主题包和会员规则。 + +服务端无法阻止他人观察最终 PNG/PDF 并模仿视觉效果;产品壁垒仍来自稳定的内容供给、素材版权、生成质量、运营数据和用户体验。 + +## 3. 目标架构 + +```text +小程序 / Web + └─ 仅提交受控参数:worksheetId、年龄、难度、主题、孩子姓名 + ↓ + 统一业务 API + ↓ + Render API(鉴权、配额、限流、参数校验) + ↓ +私有题型配置 + 私有素材 + 生成器 + 渲染器 + ↓ + 生成 PNG / PDF,写入对象存储 + ↓ +返回短期签名预览/下载 URL 或异步任务状态 +``` + +示例请求: + +```json +{ + "worksheetId": "coloring-animal-01", + "variant": "easy", + "childProfileId": "child_123", + "output": "png" +} +``` + +服务端根据 `worksheetId` 查询内部配置,生成随机种子和题目内容;客户端不能传素材 URL、任意渲染器名称、自由模板参数或数据库字段。 + +## 4. 数据与接口边界 + +### 4.1 平台无关的 worksheet 模型 + +当前内容数据中存在小程序专用 `path`,例如 `/mathPages/mathDraw/mathDraw?id=number-find`。v3.5 起应建立平台无关的核心字段: + +```ts +type Worksheet = { + id: string; + category: string; + title: string; + ageMin: number; + ageMax: number; + difficulty: 1 | 2 | 3 | 4; + tags: string[]; + rendererKey: string; // 仅服务端可解释 + generatorKey: string; // 仅服务端可解释 + assetRefs: string[]; // 私有资源引用 + status: 'draft' | 'active' | 'hidden'; +}; +``` + +小程序路由与 Web URL 是展示层映射,不能作为内容本体。旧字段可在迁移期保留为 `miniProgramPath`,但新服务端生成不依赖它。 + +### 4.2 业务服务接口 + +页面不得再散落调用 `wx.cloud.callFunction`。逐步收口为业务接口: + +```ts +worksheetService.list(filters) +worksheetService.getDetail(id) +renderService.createJob(request) +renderService.getJob(jobId) +favoriteService.toggle(worksheetId) +historyService.list() +``` + +小程序适配器可以暂时使用云函数,Web 使用 HTTP;上层页面不感知具体实现。 + +## 5. 后端演进路径 + +### 阶段 A:现在至创意涂色 MVP + +- 保留 CloudBase 数据库、存储和既有云函数。 +- 新建可通过 HTTP 调用的 Render API;可先使用 CloudBase 云托管容器,无需立即维护独立云服务器。 +- 私有素材存放在不可公开枚举的对象存储路径;最终文件通过短期 URL 交付。 +- 只迁移创意涂色纸,验证延迟、成本和保护边界。 + +### 阶段 B:Web 小规模试点 + +- Web 调用统一 HTTP API;小程序继续调用业务服务层。 +- 新题型优先服务端生成;旧题型按商业价值与迁移成本逐步处理。 +- 增加异步渲染任务、缓存、批量导出和基础监控。 + +### 阶段 C:扩张决策 + +当 Web 需要 SEO、会员支付、复杂搜索、教师版、批量 PDF、AI/异步任务,或 CloudBase 成本与能力成为瓶颈时,再迁核心 API 到 NestJS + MySQL/对象存储。 + +迁移的是业务 API 和数据,不是让前端重新实现一套生成逻辑。 + +## 6. 安全、成本与体验基线 + +| 维度 | v3.5 最低要求 | +| --- | --- | +| 素材保护 | 原始素材不公开;仅输出最终位图/PDF;下载链接短时有效 | +| 接口控制 | 登录/游客策略、参数白名单、按用户/IP 限流、每日生成配额 | +| 缓存 | 以题型、变体、随机种子、个性化字段计算缓存键;个性化内容谨慎缓存 | +| 任务模式 | 单张同步生成;多页学习包/PDF 使用异步任务与状态查询 | +| 可观测性 | 记录生成耗时、失败原因、缓存命中、文件大小、单位成本 | +| 体验目标 | 单张预览 P95 小于 3 秒;异步任务需有明确进度和失败重试提示 | + +## 7. Web MVP 范围与升级门槛 + +Web MVP 仅包含:首页、10–20 个高意图主题/详情页、2–3 类生成器、预览与打印/PDF 下载。首选试点为创意涂色纸。 + +满足以下条件后,才扩张为完整 Kiddoleap 风格的资源平台: + +- 可稳定按周上新内容,并有明确的素材/审核/发布流程。 +- Web 已获得自然搜索访问、有效下载与回访,不只是小程序导流。 +- 生成服务的时延、成功率和成本达到产品目标。 +- 主题包或批量导出表现出明确的付费/订阅意愿。 + +## 8. 与既有文档的关系 + +本策略更新了既有架构文档中“前端为主、核心生成保持客户端执行”的长期方向:该原则仍适用于存量小程序题型的成本与离线体验,但**不适用于未来 Web 的高价值题型**。 + +在 v3.5 评审通过后,应同步更新《技术架构设计文档》《PC-Web端技术预案》《小程序云开发方案和数据库设计》中的阶段说明,避免文档之间出现相反结论。 diff --git a/docs/v3.5/产品路线图.md b/docs/v3.5/产品路线图.md new file mode 100644 index 0000000..2efcd80 --- /dev/null +++ b/docs/v3.5/产品路线图.md @@ -0,0 +1,115 @@ +# Doodle Mini v3.5 产品路线图 + +> 状态:待评审 +> +> 更新日期:2026-09-24 + +## 1. 产品判断 + +涂鸦丫的核心用户不是“想要一道题”的孩子,而是希望快速安排孩子在家学习、又不愿每天筛选和购买练习册的家长。 + +当前产品已经具备动态生成 A4 练习纸、下载、收藏、分龄和题型分类能力。下一阶段应解决三件事: + +1. **找得到:** 家长能按年龄、能力和主题快速找到需要的内容。 +2. **用得起来:** 一次打开就能决定今天或本周打印什么,而不是在题型列表中犹豫。 +3. **愿意回来:** 纸上完成的练习能形成轻量打卡、下次推荐和主题包复用。 + +因此,v3.5 的产品定位为: + +> 为 3–8 岁家庭提供“今天就能打印、每周不必费心安排”的早教练习纸服务。 + +## 2. 用户问题与机会 + +| 用户问题 | 当前表现 | v3.5 机会 | +| --- | --- | --- | +| 不知道该给孩子练什么 | 有许多题型,但缺少任务化推荐 | 今日推荐、每周学习包、按年龄目标推荐 | +| 找一张特定练习纸费时 | 搜索逻辑存在,但界面未开放;分类偏题型 | 搜索、年龄/能力/主题筛选、主题集合页 | +| 低龄儿童可用内容与视觉吸引力不足 | 数学和书写题较多,3–5 岁的自由活动较少 | 创意涂色纸、颜色指令涂色、按数字涂色 | +| 打印后没有后续关系 | 仅有下载日志和收藏 | 完成打卡、连续练习、下次推荐 | +| 家长希望资料“属于孩子” | 练习纸为通用内容 | 孩子档案、姓名/日期/今日目标页眉 | + +## 3. 功能优先级 + +### P0:先让核心体验完整 + +| 功能 | 用户价值 | v3.5 最小范围 | 成功指标 | +| --- | --- | --- | --- | +| 现有链路修复 | 防止用户打开错题、首页内容缺图 | 修复“识别形状”入口、时钟调试死链、首页推荐封面字段 | 关键入口 100% 可到达正确题型 | +| 搜索与筛选 | 快速定位练习纸 | 分类页恢复关键词搜索;增加年龄、能力/分类、主题标签 | 搜索后进入题型率;搜索零结果率 | +| 今日推荐 | 降低“今天练什么”的决策成本 | 每日 3 张:主练习、趣味活动、书写/语言;允许换一张 | 首页推荐点击率、首次导出率 | +| 创意涂色纸 MVP | 覆盖低龄、强化视觉吸引和传播 | 24–30 张原创/授权线稿;动物、食物水果、交通、恐龙、节日五主题 | 涂色纸导出率、收藏率、分享率 | + +### P1:把单次下载变成每周复用 + +| 功能 | 用户价值 | v3.5/后续范围 | +| --- | --- | --- | +| 每周学习包 | 一键得到 5–7 天的打印安排 | 选择年龄、目标、每日时长;每天 2–3 张;可替换单张与收藏整包 | +| 孩子档案与个性化页眉 | 资料更有归属感、便于多孩家庭 | 孩子昵称、年龄、学习目标;导出时写入姓名、日期和练习主题 | +| 轻量完成打卡 | 建立线下练习后的回访理由 | 家长点击“已完成”;显示本周进度、连续天数和奖励贴纸 | +| 主题学习包 | 扩大内容复用与节日传播 | 如“恐龙周”“交通工具周”“入园准备包”“春节 7 日包” | + +### P2:在数据验证后再做 + +| 功能 | 触发条件 | 说明 | +| --- | --- | --- | +| 家长难度反馈 | 推荐使用稳定后 | 每张练习只反馈“太简单 / 正好 / 有点难”,用于调整推荐 | +| 深度自定义 | 高意图用户占比明确 | 数字范围、题量、汉字词库、颜色主题等参数开放 | +| 教师/机构模式 | 有明确的机构试用用户 | 班级名单、姓名批量写入、随机变体、答案页 | +| 会员与 PDF 学习包 | 批量打印需求、支付意愿得到验证 | 多页 PDF、无广告、主题包、更多导出额度 | + +## 4. 创意涂色纸 MVP + +### 4.1 为什么优先做 + +- 面向 3–5 岁孩子,弥补“不会算、不愿写”的低龄使用场景。 +- 首页预览和分享效果强,适合作为拉新入口。 +- 与“打印后离开屏幕”的产品定位天然一致。 +- 可作为 Web 服务端生成与私有素材管理的第一个试点。 + +### 4.2 第一版内容结构 + +| 类型 | 第一版内容 | 适龄 | +| --- | --- | --- | +| 自由创意涂色 | 动物、食物水果、交通、恐龙、节日五主题 | 3–6 岁 | +| 颜色指令涂色 | “把苹果涂红、鱼涂蓝”等识色/听指令任务 | 3–5 岁 | +| 按数字涂色 | 1–5 对应颜色,完成后呈现图案 | 4–6 岁 | + +首发应准备 24–30 张高质量线稿,每主题 4–6 张。每张分为低龄版(轮廓大、元素少、留白多)与进阶版(背景、颜色任务或规律任务)。 + +### 4.3 非功能约束 + +- 仅使用原创、已购买商用授权,或经人工审核且可商用的素材;不得使用知名卡通 IP。 +- 原始线稿、SVG 源文件和排版配置不得暴露给 Web 客户端。 +- 预览可使用带水印低清图;下载生成清晰 A4 PNG/PDF。 +- 每张内容具备主题、年龄、难度、关键词、版权来源和素材版本元数据。 + +## 5. 关键用户路径 + +```text +选择孩子/年龄 + → 首页看到“今天练什么”或主题包 + → 预览、替换或加入本周包 + → 生成并保存练习纸 + → 打印、纸上完成 + → 家长勾选已完成 + → 下次按年龄、目标与完成情况推荐 +``` + +## 6. 验收指标 + +v3.5 不以“新增多少题型”验收,而以以下漏斗衡量: + +| 指标 | 定义 | 初始目标 | +| --- | --- | --- | +| 首次导出转化率 | 新用户首次会话中成功保存练习纸的比例 | 持续提升;先建立基线 | +| 首次决策时长 | 打开首页至第一次点击“生成/导出”的时间 | 中位数低于 60 秒 | +| 7 日复用率 | 首次生成后 7 日内再次生成或下载的用户比例 | 持续提升;先建立基线 | +| 涂色内容表现 | 涂色纸的预览→导出、收藏、分享转化 | 高于当前题型中位数 | +| 学习包完成率 | 创建包后至少导出其中 3 张的比例 | 用于判断学习包是否成立 | + +## 7. 明确暂不做 + +- 为了“看起来丰富”而新增大量同质数学题。 +- 依赖儿童上传照片、公开晒图的社区功能。 +- 没有验证批量使用需求前的多页 PDF 付费功能。 +- 没有内容生产能力支撑前的完整 Web 分类站。 diff --git a/docs/v3.5/埋点事件字典.md b/docs/v3.5/埋点事件字典.md new file mode 100644 index 0000000..728de12 --- /dev/null +++ b/docs/v3.5/埋点事件字典.md @@ -0,0 +1,25 @@ +# Doodle Mini 埋点事件字典 + +> 更新日期:2026-09-24。事件名和字段名须先在微信小程序后台注册,再随版本发布。 + +## 通用字段 + +所有事件自动携带 `count`、`date_time`、`uuid`、`page_name`。内容事件使用稳定的 `worksheet_id`,而非可变的展示标题;可选字段包括 `worksheet_title`、`mode`、`source`、`category_id`、`export_id`、`duration_ms`、`fail_reason`。 + +| 事件 | 含义 | 触发时机 | +| --- | --- | --- | +| `page_view` | 页面浏览 | 首页、分类页首次进入 | +| `worksheet_click` | 点击某张练习纸 | 首页或分类列表点击有效内容 | +| `preview_view` | 打开或切换练习纸预览 | 练习纸页面初始化或切换题型 | +| `generate_start` | 开始生成 | 初始随机生成或点击“换一张” | +| `generate_success` / `generate_fail` | 生成结果 | Canvas 绘制结束;包含耗时和失败原因 | +| `export_click` | 点击保存 | 调用相册保存前 | +| `export_success` / `export_fail` | 导出结果 | 仅以相册保存 API 的结果为准 | +| `favorite_add` / `favorite_remove` | 收藏变化 | 云端收藏操作成功后 | +| `share_click` | 发起分享 | 调起小程序分享时 | + +## 导出判定 + +`export_click` 不能作为下载量或转化。只有 `wx.saveImageToPhotosAlbum` 返回成功时才触发 `export_success`,并在同一时机递增本地下载配额、写入下载历史和更新内容下载量。 + +每次导出会生成 `export_id`。云端以 `userId + exportId` 去重,因此同一次保存的网络重试不会重复写下载历史或重复增加 `worksheets.downloads`。 diff --git a/docs/v3.5/待办与里程碑.md b/docs/v3.5/待办与里程碑.md new file mode 100644 index 0000000..0b27f7a --- /dev/null +++ b/docs/v3.5/待办与里程碑.md @@ -0,0 +1,124 @@ +# Doodle Mini v3.5 待办与里程碑 + +> 状态:待排期 +> +> 更新日期:2026-09-24 + +## 0. 排期原则 + +按依赖关系推进:**先修复与可发现性 → 再验证涂色内容 → 再形成学习计划 → 同时建设小规模 Web/服务端样板。** + +以下周期为相对迭代,不绑定具体人天;每个里程碑完成后应先检查数据,再决定是否扩大范围。 + +## M0:体验与数据基线(第 1 个迭代) + +### 产品与研发 + +- [x] 兼容 `shape-recognition` 的旧通用页路径,并统一本地/旧入口至独立“识别形状”页面,避免回退到错误题型。 +- [x] 修复未上线调试页中时钟连线的过期路由。 +- [x] 统一首页推荐数据字段,确保自动刷新结果使用 `previewImg`。 +- [x] 为首页、分类、预览、生成、导出、收藏、分享补齐统一埋点口径。 +- [x] 明确“成功导出”的唯一判定:实际保存成功后才记录下载、计数和转化。 + +### 产品验收 + +- [ ] 核对所有分类入口、首页推荐、分龄推荐和收藏历史中的跳转路径。 +- [ ] 建立当前题型的预览→生成→保存漏斗基线。 +- [ ] 建立按年龄段、分类和题型的下载/收藏基线。 + +### 完成标准 + +- 所有正式入口可打开预期题型;无静默回退到其他题型。 +- 首页定时/手动推荐均能展示预览图。 +- 可以从统计中区分“点击生成”和“实际成功保存”。 + +## M1:发现与选择(第 2 个迭代) + +### 搜索与筛选 + +- [ ] 恢复分类页关键词搜索 UI,复用已有标题/副标题过滤能力。 +- [ ] 增加年龄筛选:3–4、4–5、5–6、6–7、7–8 岁。 +- [ ] 增加主题/标签筛选,优先支持动物、食物水果、交通、恐龙、节日等。 +- [ ] 设计无结果状态和“推荐相近内容”兜底。 +- [ ] 首页搜索入口只在具备完整结果页后开放,避免假搜索框。 + +### 今日推荐 + +- [ ] 定义推荐位:主练习、趣味练习、语言/书写练习各一张。 +- [ ] 为推荐增加替换动作和推荐原因,如“适合 4–5 岁”“本周热门”。 +- [ ] 明确手工运营、定时热度榜与分龄规则的优先级。 + +### 完成标准 + +- 家长能从首页或分类页在一分钟内筛出一个适龄题型。 +- 推荐位可跳转、可更换、可追踪点击与导出。 + +## M2:创意涂色纸 MVP(第 3–4 个迭代) + +### 内容与设计 + +- [ ] 制定线稿素材规范:A4 留白、安全线宽、低龄/进阶分级、预览图规格。 +- [ ] 建立素材授权登记表:来源、授权类型、作者、可用范围、版本、审核人。 +- [ ] 完成五主题 24–30 张首发线稿及预览图。 +- [ ] 设计涂色纸题型卡、主题入口、预览页和“换一张”交互。 + +### 研发 + +- [ ] 增加 `coloring`/`craft` 业务分类及 worksheet 元数据。 +- [ ] 建立涂色纸服务端生成样板,详见《Web 与服务端生成策略》。 +- [ ] 支持低龄自由涂色与一类结构化玩法(颜色指令或按数字涂色)。 +- [ ] 实现预览图、收藏、导出日志、下载统计和首页推荐接入。 +- [ ] 对生成接口进行鉴权、限流、参数白名单和短期下载地址控制。 + +### 完成标准 + +- 首发素材均可追溯商用权利。 +- 用户可完成“主题 → 预览 → 生成 → 保存”的闭环。 +- 原始素材与生成规则不向 Web 客户端返回。 + +## M3:从单张到每周学习(第 5 个迭代起) + +- [ ] 定义孩子档案:昵称、年龄、学习目标;多孩支持作为后续增强。 +- [ ] 设计学习包:年龄、目标、每日时长、天数、每天的练习组合。 +- [ ] 实现单张替换、整包收藏、导出进度与“已完成”打卡。 +- [ ] 定义推荐去重和难度分配规则,避免连续出现相同题型。 +- [ ] 以轻量周进度和连续天数做回访,不做高负担的学习报告。 + +### 完成标准 + +- 用户可创建一套至少 5 天的练习安排。 +- 创建后可完成至少“替换一张、导出一张、标记完成”三项动作。 +- 能统计创建包后导出 3 张以上的比例。 + +## M4:Web 试点(与 M2 并行,受服务端样板依赖) + +- [ ] 完成服务端 Render API 的认证、任务与文件交付能力。 +- [ ] 搭建一个小型 Web 站点:首页、主题聚合页、详情/生成页、打印/PDF 下载页。 +- [ ] 只上线 10–20 个高意图内容页和 2–3 类服务端生成器。 +- [ ] 建立 Web 独立埋点:搜索来源、关键词、预览、生成、下载、回访。 +- [ ] 制定 SEO 内容模板与发布流程,确保每新增内容具有独立标题、描述、主题和相关推荐。 + +### 完成标准 + +- Web 能完成一次独立于小程序的访问、生成和下载。 +- 浏览器不包含核心生成规则、完整模板坐标或原始线稿素材。 +- 先获得自然搜索和下载数据,再决定是否扩成完整资源平台。 + +## 决策检查点 + +| 检查点 | 决策问题 | 继续投入条件 | +| --- | --- | --- | +| 涂色 MVP 上线后 | 是否扩大内容库和主题包? | 预览→导出、收藏或分享表现显著高于现有题型中位数 | +| 学习包试点后 | 是否做 PDF/会员? | 用户确实会连续导出多张,且有明确的批量打印需求 | +| Web 试点后 | 是否建设完整 Web 平台? | 自然流量、下载和回访有增长;内容可稳定周更 | +| 服务端渲染试点后 | 是否迁移更多旧题型? | P95 生成时延、成功率和单位生成成本满足目标 | + +## 风险清单 + +| 风险 | 应对 | +| --- | --- | +| 涂色素材版权不清晰 | 建立授权台账;不使用知名 IP;发布前人工审核 | +| 服务端生成成本失控 | 参数哈希缓存、热门主题预生成、限额与异步任务 | +| Web 内容薄、没有搜索价值 | 先做少量高意图页面,不以“站点页面数”作为目标 | +| 小程序和 Web 各自维护内容 | 以 `worksheetId` 和平台无关元数据作为单一内容源 | +| 过早会员化影响首次体验 | 先验证学习包和批量下载需求;单张基础内容保持可试用 | diff --git a/miniprogram/base/pageMixin.ts b/miniprogram/base/pageMixin.ts index 5f9b868..25434ef 100644 --- a/miniprogram/base/pageMixin.ts +++ b/miniprogram/base/pageMixin.ts @@ -85,6 +85,7 @@ type SharePathData = { functionId?: string; worksheetId?: string; currentMode?: string; + pageTitle?: string; }; type SharePathPage = { @@ -143,9 +144,13 @@ export function getAppShareAppMessage( trackerLabel?: string, page?: SharePathPage, ) { - if (trackerLabel) { - tracker.reportShare(trackerLabel); - } + tracker.reportShare(page?.route || trackerLabel || '', { + ...(trackerLabel && { worksheet_title: trackerLabel }), + ...(page?.data?.functionId || page?.data?.worksheetId + ? { worksheet_id: page.data.functionId || page.data.worksheetId } + : {}), + ...(page?.data?.currentMode ? { mode: page.data.currentMode } : {}), + }); return { title: defaultShareConfig.title, path: buildPageSharePath(page), @@ -158,9 +163,13 @@ export function getAppShareTimeline( trackerLabel?: string, page?: SharePathPage, ) { - if (trackerLabel) { - tracker.reportShare(trackerLabel); - } + tracker.reportShare(page?.route || trackerLabel || '', { + ...(trackerLabel && { worksheet_title: trackerLabel }), + ...(page?.data?.functionId || page?.data?.worksheetId + ? { worksheet_id: page.data.functionId || page.data.worksheetId } + : {}), + ...(page?.data?.currentMode ? { mode: page.data.currentMode } : {}), + }); return { title: defaultShareConfig.title, query: buildPageShareTimelineQuery(page), @@ -171,10 +180,10 @@ export function getAppShareTimeline( /** 供 Page({}) 直接展开:统一转发 / 分享到朋友圈(path 为当前页) */ export const appSharePageMethods = { onShareAppMessage(this: SharePathPage) { - return getAppShareAppMessage(undefined, this); + return getAppShareAppMessage(this.data?.pageTitle, this); }, onShareTimeline(this: SharePathPage) { - return getAppShareTimeline(undefined, this); + return getAppShareTimeline(this.data?.pageTitle, this); }, }; @@ -331,6 +340,7 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { errorToast: '请先生成内容', trackerName: this.data.pageTitle, trackerMode: this.data.currentMode, + trackerPage: this.route, worksheetId, }; @@ -394,6 +404,7 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { errorToast: '请先生成内容', trackerName: this.data.pageTitle, trackerMode: this.data.currentMode, + trackerPage: this.route, worksheetId, }); } @@ -591,6 +602,11 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { }); wx.setNavigationBarTitle({ title }); + tracker.reportPreviewView(this.route || '', { + worksheet_id: functionId, + worksheet_title: title, + ...(this.data.currentMode ? { mode: this.data.currentMode } : {}), + }); }, }; } @@ -607,6 +623,90 @@ export function applyPageMixin( config: PageCommonMethodsConfig = {}, ) { const commonMethods = getPageCommonMethods(config); + const trackedOptions = { ...pageOptions }; + const generationStartKey = '__generationTrackingStartedAt'; + + const beginGenerationTracking = function (this: PageCanvasInstance) { + if ((this as any)[generationStartKey]) return; + const worksheetId = + typeof this.getWorksheetStatsId === 'function' + ? this.getWorksheetStatsId() + : this.data.functionId; + (this as any)[generationStartKey] = Date.now(); + tracker.reportGenerateStart(this.route || '', { + worksheet_id: worksheetId || '', + ...(this.data.currentMode ? { mode: this.data.currentMode } : {}), + }); + }; + + const originalOnRandom = pageOptions.onRandom; + if (typeof originalOnRandom === 'function') { + trackedOptions.onRandom = function ( + this: PageCanvasInstance, + ...args: any[] + ) { + beginGenerationTracking.call(this); + return originalOnRandom.apply(this, args); + }; + } + + const originalOnPreviewRefresh = pageOptions.onPreviewRefresh; + if (typeof originalOnPreviewRefresh === 'function') { + trackedOptions.onPreviewRefresh = function ( + this: PageCanvasInstance, + ...args: any[] + ) { + beginGenerationTracking.call(this); + return originalOnPreviewRefresh.apply(this, args); + }; + } + + const originalDrawCanvas = pageOptions.drawCanvas; + if (typeof originalDrawCanvas === 'function') { + trackedOptions.drawCanvas = async function ( + this: PageCanvasInstance, + ...args: any[] + ) { + try { + const result = await originalDrawCanvas.apply(this, args); + const startedAt = (this as any)[generationStartKey] as + | number + | undefined; + if (startedAt) { + delete (this as any)[generationStartKey]; + const worksheetId = + typeof this.getWorksheetStatsId === 'function' + ? this.getWorksheetStatsId() + : this.data.functionId; + tracker.reportGenerateResult( + this.route || '', + this.data.hasContent !== false, + { + worksheet_id: worksheetId || '', + duration_ms: Date.now() - startedAt, + ...(this.data.currentMode + ? { mode: this.data.currentMode } + : {}), + }, + ); + } + return result; + } catch (error) { + const startedAt = (this as any)[generationStartKey] as + | number + | undefined; + if (startedAt) { + delete (this as any)[generationStartKey]; + tracker.reportGenerateResult(this.route || '', false, { + worksheet_id: this.data.functionId || '', + duration_ms: Date.now() - startedAt, + fail_reason: 'draw_exception', + }); + } + throw error; + } + }; + } // 提取 pageOptions 中的 data(如果有) const pageData = pageOptions.data || {}; @@ -615,7 +715,7 @@ export function applyPageMixin( // 注意:pageOptions 放在后面,这样页面可以覆盖公共方法 const mergedOptions: any = { ...commonMethods, - ...pageOptions, + ...trackedOptions, // 处理 data 的合并(需要特殊处理,避免覆盖) data: { ...pageData, diff --git a/miniprogram/focusPages/focusDraw/focusDraw.ts b/miniprogram/focusPages/focusDraw/focusDraw.ts index f44ed25..d4feede 100644 --- a/miniprogram/focusPages/focusDraw/focusDraw.ts +++ b/miniprogram/focusPages/focusDraw/focusDraw.ts @@ -55,6 +55,15 @@ createFocusPage({ onLoad(options: { id?: string; mode?: string }) { const routeId = options.id || FOCUS_TYPE_CONFIGS[0].id; + + // “识别形状”仍由独立页面承载。兼容已发布的旧路径和本地兜底 + // /focusPages/focusDraw/focusDraw?id=shape-recognition,避免通用注册表 + // 找不到该 id 时静默回退为第一个题型。 + if (routeId === 'shape-recognition') { + wx.redirectTo({ url: '/focusPages/shape/shape' }); + return; + } + const result = findTypeByRouteId(routeId); if (!result) return; diff --git a/miniprogram/focusPages/shape/shape.ts b/miniprogram/focusPages/shape/shape.ts index 42163c1..ec53ec5 100644 --- a/miniprogram/focusPages/shape/shape.ts +++ b/miniprogram/focusPages/shape/shape.ts @@ -10,6 +10,7 @@ import { shouldShowShareGuide, shouldUnlockWithAd, } from '../../utils/shareGuide'; +import tracker from '../../utils/tracker'; Page({ canvas: null as Canvas | null, @@ -20,6 +21,8 @@ Page({ timer: null as any, data: { + worksheetId: 'shape-recognition', + pageTitle: '识别图形,涂一涂', shapeList: [] as ShapeCard[], showSelectShapePopup: false, selectedShapeIndex: -1, @@ -31,6 +34,10 @@ Page({ }, onLoad() { + tracker.reportPreviewView('focusPages/shape/shape', { + worksheet_id: 'shape-recognition', + worksheet_title: '识别图形,涂一涂', + }); // 判断是否在PC端开发工具上运行 // const systemInfo = wx.getDeviceInfo(); // if (systemInfo.platform === 'devtools') { @@ -213,6 +220,10 @@ Page({ * 随机生成6个图形 */ refreshShapeCard() { + const startedAt = Date.now(); + tracker.reportGenerateStart('focusPages/shape/shape', { + worksheet_id: 'shape-recognition', + }); // 随机取6个图形 const shapesCopy = [...SHAPES]; for (let i = shapesCopy.length - 1; i > 0; i--) { @@ -244,6 +255,10 @@ Page({ () => { // 绘制canvas this.drawCanvas(); + tracker.reportGenerateResult('focusPages/shape/shape', true, { + worksheet_id: 'shape-recognition', + duration_ms: Date.now() - startedAt, + }); }, ); }, @@ -312,6 +327,8 @@ Page({ await downloadPrint(this.canvas, { errorToast: '请先生成图形', trackerName: '图形涂色', + trackerPage: 'focusPages/shape/shape', + worksheetId: 'shape-recognition', }); }, @@ -327,6 +344,8 @@ Page({ await downloadPrint(this.canvas, { errorToast: '请先生成图形', trackerName: '图形涂色', + trackerPage: 'focusPages/shape/shape', + worksheetId: 'shape-recognition', }); } }, diff --git a/miniprogram/pages/category/category.data.ts b/miniprogram/pages/category/category.data.ts index 3bd7be8..6f5e8f9 100644 --- a/miniprogram/pages/category/category.data.ts +++ b/miniprogram/pages/category/category.data.ts @@ -443,7 +443,7 @@ const CATEGORY_ITEMS_BY_ID: Record = { previewImg: '/assets/entrancePicture/focus/shape-recognition.png', ageBand: age(3, 5), difficulty: 1, - path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', + path: '/focusPages/shape/shape', available: true, }), item({ diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts index e1ef16b..fc91ecf 100644 --- a/miniprogram/pages/category/category.ts +++ b/miniprogram/pages/category/category.ts @@ -1,6 +1,7 @@ import { appSharePageMethods } from '../../base/pageMixin'; import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories'; -import { takePendingCategoryTabId } from '../../utils/index'; +import { parseMiniProgramUrl, takePendingCategoryTabId } from '../../utils/index'; +import tracker from '../../utils/tracker'; import { CATEGORY_DATA, type CategoryItem } from './category.data'; type CategoryTab = { @@ -224,6 +225,7 @@ Page({ _defaultCategoryId: 'all', onLoad(options: Record) { + tracker.reportPageView('pages/category/category'); if (options.id && options.id !== 'all') { this._defaultCategoryId = options.id; this.setData({ activeCategoryId: options.id }); @@ -285,6 +287,9 @@ Page({ displayItems: filtered, scrollIntoViewId: '', }); + tracker.report('category_select', 'pages/category/category', { + category_id: id, + }); }, onSearchInput(e: WechatMiniprogram.Input) { @@ -319,6 +324,13 @@ Page({ return; } + tracker.reportWorksheetClick('pages/category/category', { + worksheet_id: String(parseMiniProgramUrl(path).query.id || ''), + worksheet_title: title || '', + category_id: this.data.activeCategoryId, + source: 'category_list', + }); + if (TAB_BAR_PATHS.has(path)) { wx.switchTab({ url: path }); return; diff --git a/miniprogram/pages/home/home.ts b/miniprogram/pages/home/home.ts index 3d2e592..eda4f59 100644 --- a/miniprogram/pages/home/home.ts +++ b/miniprogram/pages/home/home.ts @@ -4,6 +4,7 @@ import { parseMiniProgramUrl, stashPendingCategoryTabId, } from '../../utils/index'; +import tracker from '../../utils/tracker'; import { HOME_DAILY_CHECKIN_ITEMS, HOME_NOTICES, @@ -124,6 +125,23 @@ function navigateByPath(path?: string, fallbackTitle?: string) { }); } +function worksheetIdFromPath(path?: string): string { + if (!path) return ''; + return String(parseMiniProgramUrl(path).query.id || ''); +} + +function trackWorksheetClick( + path: string | undefined, + title: string | undefined, + source: string, +) { + tracker.reportWorksheetClick('pages/home/home', { + worksheet_id: worksheetIdFromPath(path), + worksheet_title: title || '', + source, + }); +} + // ── Skeleton data ── function skeletonItem(id: string): HomeDisplayItemView { @@ -196,6 +214,7 @@ Page({ }, onLoad() { + tracker.reportPageView('pages/home/home'); void this.initData(); }, @@ -274,12 +293,14 @@ Page({ onTapFeatured(e: WechatMiniprogram.TouchEvent) { const path = e.currentTarget.dataset.path as string | undefined; const title = e.currentTarget.dataset.title as string | undefined; + trackWorksheetClick(path, title, 'home_featured'); navigateByPath(path, title); }, onTapHotRecommend(e: WechatMiniprogram.TouchEvent) { const path = e.currentTarget.dataset.path as string | undefined; const title = e.currentTarget.dataset.title as string | undefined; + trackWorksheetClick(path, title, 'home_hot'); navigateByPath(path, title); }, @@ -310,6 +331,7 @@ Page({ const title = detail.title || (e.currentTarget.dataset.title as string | undefined); + trackWorksheetClick(path, title, 'home_section'); navigateByPath(path, title); }, diff --git a/miniprogram/supportPages/focusIndex/focusIndex.ts b/miniprogram/supportPages/focusIndex/focusIndex.ts index 37d633b..875ec09 100644 --- a/miniprogram/supportPages/focusIndex/focusIndex.ts +++ b/miniprogram/supportPages/focusIndex/focusIndex.ts @@ -23,8 +23,12 @@ Page({ return; } - // 统一跳转到 focusDraw 页面 + // “识别形状”仍使用独立页面;其余题型统一跳转到 focusDraw 页面 if (functionItem.page) { + if (functionId === 'shape-recognition') { + wx.navigateTo({ url: '/focusPages/shape/shape' }); + return; + } let url = `/focusPages/focusDraw/focusDraw?id=${functionId}`; if (functionItem.mode) { url += `&mode=${functionItem.mode}`; diff --git a/miniprogram/supportPages/unreleasedDebug/unreleasedDebug.config.ts b/miniprogram/supportPages/unreleasedDebug/unreleasedDebug.config.ts index dc6715e..2b8fd7c 100644 --- a/miniprogram/supportPages/unreleasedDebug/unreleasedDebug.config.ts +++ b/miniprogram/supportPages/unreleasedDebug/unreleasedDebug.config.ts @@ -23,6 +23,6 @@ export const UNRELEASED_PAGE_ENTRIES: UnreleasedPageEntry[] = [ id: 'clock-connect', title: '时钟连线', subtitle: '看钟表连对应时间,认识时刻', - path: '/mathPages/clockConnect/clockConnect', + path: '/mathPages/clockReading/clockReading?id=clock-connect', }, ]; diff --git a/miniprogram/utils/downloadLogs.ts b/miniprogram/utils/downloadLogs.ts index 144d4ba..3f0f823 100644 --- a/miniprogram/utils/downloadLogs.ts +++ b/miniprogram/utils/downloadLogs.ts @@ -4,6 +4,7 @@ export interface DownloadLogRecord { _id: string; userId: string; worksheetId: string; + exportId?: string; createdAt: string; worksheet?: { _id: string; @@ -22,13 +23,16 @@ export interface DownloadLogRecord { /** * 记录下载日志 */ -export async function addDownloadLog(worksheetId: string): Promise { +export async function addDownloadLog( + worksheetId: string, + exportId?: string, +): Promise { if (!worksheetId) return false; try { await getUser(); const res = await wx.cloud.callFunction({ name: 'userDownloadLogs', - data: { action: 'add', worksheetId }, + data: { action: 'add', worksheetId, exportId }, }); const result = (res?.result as any) || {}; return result.code === 0; diff --git a/miniprogram/utils/downloadPrint.ts b/miniprogram/utils/downloadPrint.ts index c6111bf..58f8fa6 100644 --- a/miniprogram/utils/downloadPrint.ts +++ b/miniprogram/utils/downloadPrint.ts @@ -6,6 +6,7 @@ import { checkAndSaveImage } from './saveImage'; import tracker from './tracker'; import { addDownloadLog } from './downloadLogs'; +import { generateUUID } from './uuid'; import { getUnlockMethod, isDownloadLimitEnabled, @@ -131,23 +132,32 @@ function doDownload( errorToast?: string; trackerName?: string; trackerMode?: string; + trackerPage?: string; worksheetId?: string; }, ): Promise { - if (options.trackerName) { - if (options.trackerMode) { - tracker.reportDownload(options.trackerName, options.trackerMode); - } else { - tracker.reportDownload(options.trackerName); - } - } + const exportId = generateUUID(); + const trackerPage = options.trackerPage || options.trackerName || ''; + const trackerParams = { + export_id: exportId, + ...(options.worksheetId && { worksheet_id: options.worksheetId }), + ...(options.trackerName && { worksheet_title: options.trackerName }), + ...(options.trackerMode && { mode: options.trackerMode }), + }; - return checkAndSaveImage(canvas).then((saved) => { + // 用户点击保存不等于保存成功,二者必须分开统计。 + tracker.reportExportClick(trackerPage, trackerParams); + + return checkAndSaveImage(canvas).then(async (saved) => { if (saved) { + // 只在 wx.saveImageToPhotosAlbum 的 success 回调后进入此分支。 recordDownloadSuccess(); + tracker.reportExportResult(trackerPage, true, trackerParams); if (options.worksheetId) { - addDownloadLog(options.worksheetId); + await addDownloadLog(options.worksheetId, exportId); } + } else { + tracker.reportExportResult(trackerPage, false, trackerParams); } return saved; }); @@ -163,6 +173,7 @@ export async function downloadPrint( errorToast?: string; trackerName?: string; trackerMode?: string; + trackerPage?: string; worksheetId?: string; } = {}, ): Promise { diff --git a/miniprogram/utils/favorites.ts b/miniprogram/utils/favorites.ts index 7283c22..afc81e8 100644 --- a/miniprogram/utils/favorites.ts +++ b/miniprogram/utils/favorites.ts @@ -1,4 +1,10 @@ import { getUser } from './auth'; +import tracker from './tracker'; + +function getCurrentPageRoute(): string { + const pages = getCurrentPages(); + return pages[pages.length - 1]?.route || ''; +} export interface FavoriteRecord { _id: string; @@ -31,7 +37,9 @@ export async function addFavorite(worksheetId: string): Promise { data: { action: 'add', worksheetId }, }); const result = (res?.result as any) || {}; - return result.code === 0; + const success = result.code === 0; + if (success) tracker.reportFavorite(getCurrentPageRoute(), worksheetId, true); + return success; } catch (e) { console.error('addFavorite failed', e); return false; @@ -49,7 +57,11 @@ export async function removeFavorite(worksheetId: string): Promise { data: { action: 'remove', worksheetId }, }); const result = (res?.result as any) || {}; - return result.code === 0; + const success = result.code === 0; + if (success) { + tracker.reportFavorite(getCurrentPageRoute(), worksheetId, false); + } + return success; } catch (e) { console.error('removeFavorite failed', e); return false; @@ -133,4 +145,3 @@ export async function batchCheckFavorited( return {}; } } - diff --git a/miniprogram/utils/tracker.ts b/miniprogram/utils/tracker.ts index c5624b5..4625ca2 100644 --- a/miniprogram/utils/tracker.ts +++ b/miniprogram/utils/tracker.ts @@ -115,18 +115,96 @@ class Tracker { }; } + /** + * 统一业务埋点入口。pageName 使用页面 route,内容使用稳定的 worksheet_id。 + */ + report( + eventName: string, + pageName: string, + extraParams?: Record, + ): void { + try { + this.sendEvent( + eventName, + this.getCommonEventParams(eventName, pageName, extraParams), + ); + } catch (error) { + console.error(`上报 ${eventName} 事件失败:`, error); + } + } + + reportPageView(pageName: string): void { + this.report('page_view', pageName); + } + + reportWorksheetClick( + pageName: string, + extraParams: Record, + ): void { + this.report('worksheet_click', pageName, extraParams); + } + + reportPreviewView( + pageName: string, + extraParams: Record, + ): void { + this.report('preview_view', pageName, extraParams); + } + + reportGenerateStart( + pageName: string, + extraParams: Record, + ): void { + this.report('generate_start', pageName, extraParams); + } + + reportGenerateResult( + pageName: string, + success: boolean, + extraParams: Record, + ): void { + this.report( + success ? 'generate_success' : 'generate_fail', + pageName, + extraParams, + ); + } + + reportExportClick( + pageName: string, + extraParams: Record, + ): void { + this.report('export_click', pageName, extraParams); + } + + reportExportResult( + pageName: string, + success: boolean, + extraParams: Record, + ): void { + this.report( + success ? 'export_success' : 'export_fail', + pageName, + extraParams, + ); + } + + reportFavorite( + pageName: string, + worksheetId: string, + favorited: boolean, + ): void { + this.report(favorited ? 'favorite_add' : 'favorite_remove', pageName, { + worksheet_id: worksheetId, + }); + } + /** * 上报分享点击事件 * @param pageName 页面名称 */ - reportShare(pageName: string): void { - try { - const params = this.getCommonEventParams('share_click', pageName); - - this.sendEvent('share_click', params); - } catch (error) { - console.error('上报分享事件失败:', error); - } + reportShare(pageName: string, extraParams?: Record): void { + this.report('share_click', pageName, extraParams); } /** @@ -135,15 +213,8 @@ class Tracker { * @param mode 模式(可选) */ reportDownload(pageName: string, mode?: string): void { - try { - const params = this.getCommonEventParams('download', pageName, { - ...(mode && { mode }), - }); - - this.sendEvent('download', params); - } catch (error) { - console.error('上报下载事件失败:', error); - } + // 兼容旧调用;新代码请使用 reportExportClick / reportExportResult。 + this.reportExportClick(pageName, { ...(mode && { mode }) }); } /**