Files
doodle-store/docs/云服务端迁移方案.md
T
2026-10-12 15:22:27 +08:00

472 lines
51 KiB
Markdown
Raw 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.
# Doodle Mini 现有功能迁移到云服务端方案
> 决策:已确认迁移到自有腾讯云服务器;数据库使用 PostgreSQL。
>
> 文档归属:本方案在 `doodle-store/docs` 中维护,作为迁移实施的唯一方案文档;小程序产品规划与历史设计仍保留在 `doodle-mini`。
>
> 工程组织:小程序继续在独立项目 `doodle-mini` 中维护;新项目 `doodle-store` 使用 pnpm workspace 管理服务端、管理后台和 Web,不包含小程序源码。
>
> 实施状态:已完成基础工程初始化;业务功能、数据迁移与上线尚未实施。本文不是迁移完成报告。
>
> 更新日期:2026-10-12。以 `doodle-mini` 当前仓库代码为迁移依据,线上数据量、文件清单及服务器环境需在实施第一步核实。
## 1. 目标与范围
本次将现有小程序的业务后端、数据、文件及定时任务迁出微信 CloudBase,形成 **一套业务 API、一套主数据库、一套文件存储**。后续 Web 直接复用这套后端。
采用轻量模块化单体,不建立微服务、数据库集群或复杂运维平台。AI 辅助接口、脚本、适配和部署配置开发,人工及实际运行验证负责确认行为、数据和切换结果。
### 1.1 本次交付
- 微信静默登录、用户资料、清除资料和注销账户。
- 题型与分类查询、分龄推荐、首页配置、手动及自动推荐刷新。
- 收藏、下载历史、个人数量统计、题型下载/收藏计数。
- 现有题型发布、上下架、分类管理、批量属性同步等管理能力。
- 头像、预览图、云字体和首页预拉取配置的迁移与访问。
- 小程序 HTTP 接入、短期旧版本兼容、数据迁移、部署、备份、验收与回滚。
### 1.2 后续单独实施
- 独立管理后台 `doodle-store/apps/admin` 的完整页面;本次先迁移管理 API,并保留小程序现有管理操作,不以新后台上线作为切换前置条件。
- 完整 Web 网站、SEO 内容页和 Web 登录/账户绑定。
- 将存量 Canvas 题型全部改为服务端生成。
- 创意涂色、批量 PDF、会员、支付、孩子档案和学习包等新功能。
本次迁移后,现有随机出题、Canvas 绘制、相册保存、微信分享、广告解锁及本地下载配额仍按小程序现有方式工作。迁移后端并不会自动隐藏已经随小程序发布的生成代码。未来 Web 的私有生成器另行接入同一后端。
## 2. 已知条件与实施前核实项
| 项目 | 当前已知条件 | 方案处理 |
| ---------- | ----------------------------------------------------- | ----------------------------------------------------------- |
| 云服务器 | 腾讯云,地区可能为华东;2 核、4 GB 内存、60 GB 系统盘 | 作为初期 API 与数据库部署目标;实施前检查现有业务和可用资源 |
| 服务器期限 | 已付费至 2027 年 10 月,计划长期保留 | 不将整台服务器续费全部归于本项目,但记录实际资源占用 |
| 流量 | 每月 500 GB | 用于 API 和经服务器上传的流量;七牛直出文件单独计费 |
| 域名 | 已持有长期域名 | 实施前确认具体 API/资源子域名、备案、DNS 和 HTTPS |
| 文件存储 | 计划使用七牛 | 新文件统一进入七牛;存储、请求和下载费用按实际用量核算 |
| CloudBase | 当前 19.9 元/月 | 过渡期保留;完全退出兼容函数与旧文件依赖后再停止套餐 |
| 后端规模 | 17 个云函数,入口 JS 合计约 1,875 行 | 主要是查询和 CRUD,适合 AI 辅助迁移;行数不等于全部迁移成本 |
实施前应取得:服务器系统/现有服务/可用内存与磁盘/峰值带宽;域名备案和小程序后台配置权限;现有 AppID 对应的 AppSecret;CloudBase 导出与文件访问权限;七牛空间、区域和 HTTPS 资源域名。密钥通过环境变量配置,不写进文档或仓库。
这些信息用于实施,不影响现在确定架构。备案、审核、DNS 和文件传输等待时间与实际开发工时分开记录。
## 3. 目标架构与技术选择
```mermaid
flowchart TD
Mini[新版微信小程序 · doodle-mini] -->|HTTPS| Nginx[Nginx]
Web[后续 Web · doodle-store] -->|HTTPS| Nginx
Admin[后续管理后台 · doodle-store] -->|HTTPS| Nginx
Old[旧版小程序] --> Bridge[过渡期 CloudBase 兼容函数]
Bridge -->|签名请求| Nginx
Nginx --> API[统一业务 API · NestJS]
API --> DB[(PostgreSQL · 唯一主数据库)]
API --> Files[七牛 · 文件存储]
API --> Wx[微信登录接口]
Mini -->|图片和字体访问| Files
Scheduler[应用内定时任务] --> API
Render[后续私有生成模块] -.-> API
```
CloudBase 兼容函数只转发旧协议,不保留独立业务规则;切换后不再写旧数据库。未来 Web 的页面渲染与练习纸绘制使用同一后端数据,生成任务增长后可拆为独立进程,不需要再建一套业务系统。
| 层级 | 首期选择 | 选择理由与限制 |
| ---------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| 服务端 | TypeScript + NestJS,单应用 | 延续仓库技术栈和已有后端预案;按模块组织现有业务 |
| 工程管理 | 独立 `doodle-store` 项目 + pnpm workspace Monorepo | 管理 `apps/server`、后续 `apps/admin` / `apps/web` 和必要共享包;`doodle-mini` 不加入工作区 |
| 数据库 | PostgreSQL + Prisma | 用唯一键和事务保证收藏/下载一致性;版本与 ORM 兼容性在实施时验证 |
| 运行时 | 受维护的 Node.js LTS | 实施时锁定具体版本及依赖,不沿用旧文档的版本号 |
| 文件 | 七牛对象存储,HTTPS 自定义资源域名 | 与数据库分离;数据库保存对象引用,避免保存会过期的签名 URL |
| 部署 | Docker Compose 管理 API 与 PostgreSQL,Nginx 反向代理 | 固定镜像版本、持久化数据库卷;已有 Nginx 可复用,不覆盖其他业务 |
| 定时任务 | 单实例应用内调度 + 数据库执行记录/锁 | 任务少,不引入 Redis/队列;控制重复执行及手动刷新竞争 |
| 日志与监测 | 结构化日志、健康检查、错误与任务失败通知 | 覆盖基本排障,不额外部署大型监控平台 |
2 核 4 GB 可作为起步配置,但承载能力以实际资源检查和联调结果为准。先部署 API 与数据库;服务端绘制的内存/CPU 需求在后续样板中单独测量。数据库不开放公网端口。
### 3.1 两个独立项目的职责
- `doodle-mini`:继续维护现有微信小程序、微信平台适配、原管理页面及过渡期 CloudBase 兼容函数;HTTP 接入改造也在此项目完成。保持现有小程序目录和开发工具配置,不搬入新项目。
- `doodle-store`:独立项目位于 `/Users/joey-xd/sites/doodle/doodle-store`,采用 pnpm workspace Monorepo;`apps/server` 承载统一业务 API,后续 `apps/admin` 和 `apps/web` 分别提供管理后台与用户网站。当前只完成基础工程与健康检查,尚无业务 API。
- 两个项目通过有版本的 HTTPS API 协作,独立管理依赖、版本与发布;不使用跨仓库源码导入、相对路径引用或本地软链接作为正式依赖。
- Monorepo 只用于代码、依赖与开发任务管理,不改变“一套业务 API、一套主数据库”的目标,也不要求使用 NestJS 自身的多应用 Monorepo 模式。
### 3.2 doodle-store 计划目录与工作区
已初始化 pnpm 工作区、`apps/server` 的 NestJS 骨架和 `packages/contracts` 的基础响应/健康检查类型;`apps/admin`、`apps/web`、Prisma、迁移脚本与部署目录目前仅有用途说明。以下为目标结构,业务模块、数据库接入、完整前端和部署配置尚未实现,不为凑齐目录提前开发完整应用。
```text
doodle-store/
apps/
server/ # 首期统一业务 API
src/
prisma/ # 数据模型与版本化迁移,仅服务端访问
scripts/ # 数据/文件导入、审计与恢复
package.json
.env.example # 变量名示例,不包含真实密钥
admin/ # 后续独立管理后台
web/ # 后续用户网站与 SEO 页面
packages/
contracts/ # 必要的公开请求/响应类型及枚举
deploy/ # Compose、Nginx、备份配置
docs/ # 新系统接口与部署文档
pnpm-workspace.yaml
pnpm-lock.yaml
package.json
```
`doodle-store/pnpm-workspace.yaml`:
```yaml
packages:
- 'apps/*'
- 'packages/*'
```
- 每个应用与共享包有独立 `package.json`,声明自身依赖;根项目设为 `private: true`,主要负责工作区脚本和公共开发工具,不集中安装所有应用的运行依赖。
- `doodle-store` 使用单一 `pnpm-lock.yaml`,锁定 `packageManager` 和 Node.js 版本;CI 使用冻结锁文件安装。`doodle-mini` 保持自身依赖和构建流程,不因新项目使用 pnpm 而强制切换。
- 工作区内部包使用 `workspace:*` 引用,不要求发布到 npm;`packages/contracts` 仅用于新项目内部共享公开契约,不包含数据库模型、密钥或私有生成器实现。
- 使用 pnpm 的筛选与递归脚本执行构建、检查、测试;应用构建需包含其依赖共享包的构建步骤。先采用 pnpm 原生能力,不默认引入 Turborepo、Nx、私有包仓库或复杂发布平台。
- TypeScript 基础配置可以复用,但 Node.js、浏览器和 SSR 应用保留各自运行环境配置;UI、SDK 等共享包仅在出现真实复用需求后提取,不建设无边界的通用 `shared` 包。
`doodle-store/apps/server` 内部目标结构(当前仅实现启动配置与健康检查,业务模块待开发):
```text
apps/server/ # 以下路径相对于 doodle-store
src/
health/ # 已实现:仅 HTTP 进程存活检查
modules/ # 以下业务模块尚未实现,当前仅用途说明
auth/ # 微信登录、用户身份、管理权限
users/ # 用户资料与注销
worksheets/ # 题型、上下架和属性同步
categories/ # 分类
favorites/ # 收藏与计数
downloads/ # 成功保存记录、历史与计数
content/ # 首页配置、推荐和发布
assets/ # 文件上传、引用和访问地址
legacy/ # 旧云函数协议适配,过渡后移除
common/ # 校验、错误格式、日志
prisma/ # 数据结构、版本化迁移
scripts/ # 导入、文件迁移、审计与恢复
package.json # 服务端独立依赖和构建脚本
.env.example # 变量名示例,不包含真实密钥
```
### 3.3 跨项目契约、发布与代码边界
- 公共 API 契约以 `doodle-store` 为权威来源,提供版本化接口文档/OpenAPI。小程序不能直接使用另一个仓库的 `workspace:*` 包;首期可将按契约生成或核对的必要类型同步到 `doodle-mini`,记录来源版本,并由小程序服务层继续使用 `wx.request` / `wx.uploadFile`。
- 公开契约变化时,分别核对 `doodle-mini`、`apps/admin`、`apps/web` 的适配与测试结果。新增兼容字段优先;字段删除、更名或语义变化须保留兼容期,不能假设两个项目或全部客户端同时升级。
- `apps/server`、`apps/admin`、`apps/web` 独立构建和部署。后台若采用 SPA,可发布静态文件;Web 是否采用 SSR/预渲染另行选择。共享包变化时检查所有受影响应用,但不要求所有应用一起发布。
- 数据库结构、访问凭证、管理权限判断、未来私有生成/绘制实现仅由服务端持有。管理后台和 Web 通过 API 访问,不直接导入 Prisma Client 或服务端源码;前端包也不得间接依赖这些私有模块。`private: true` 不是防止代码被前端打包的安全边界。
- 服务端和各前端分别管理环境变量,真实密钥不提交仓库、不写入前端可公开配置。生产仅部署目标应用及必要依赖,不在 2 核 4 GB 服务器上长期运行整套开发模式或无关应用。
## 4. 现有功能与云函数迁移清单
以下 API 是目标契约,实施时生成接口文档。统一前缀为 `/api/v1`;表中使用省略前缀的路径。用户接口从可信登录态取得 userId,不接受客户端指定身份。管理接口一律由服务端校验管理员权限。
| 当前云函数 | 当前功能 | 迁移目标 | 关键要求 |
| ------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `userLogin` | 静默登录、更新昵称头像 | `POST /auth/wechat/login`、`GET /me`、`PATCH /me/profile` | 原用户 ID 不变;AppSecret/session_key 不下发 |
| `userSettings` | 清除资料、注销 | `DELETE /me/profile`、`DELETE /me` | 清资料保留收藏/历史;注销落实页面承诺的关联数据删除 |
| `userFavorites` | 添加/取消/列表/数量/单张检查/批量检查 | `PUT/DELETE /me/favorites/:worksheetId`、`GET /me/favorites`、`GET /me/favorites/count`、`POST /me/favorites/check` | 收藏关系唯一;真实增删与题型计数同一事务 |
| `userDownloadLogs` | 添加成功保存记录、列表、数量 | `POST /me/downloads`、`GET /me/downloads`、`GET /me/downloads/count` | 新版必须传 exportId;只在实际保存成功后上报 |
| `worksheetsQuery` | 题型查询、状态筛选、分类状态数量、展示计数 | `GET /worksheets`、`GET /worksheets/:id`、`GET /admin/worksheets` | 普通用户只看 active;管理查询支持全部状态和原始计数 |
| `worksheetsPublish` | 按固定 ID 新增或更新题型 | `PUT /admin/worksheets/:id` | 保留发布能力;普通内容更新不重置累计计数 |
| `worksheetsUpdateStatus` | draft/active/hidden 切换 | `PATCH /admin/worksheets/:id/status` | 状态变更后刷新受影响首页展示 |
| `worksheetsBatchPatch` | 批量年龄、年级、标签、种子及计数同步 | `POST /admin/worksheets/batch-patch` | 保留 dryRun、逐项结果和错误;计数修订单独审计 |
| `worksheetsStatsUpdate` | 直接递增 likes/downloads | 不向普通用户提供通用加数接口;由收藏/下载服务承接 | 当前仓库工具无调用方;旧协议仅在确认必要时适配,不能绕过幂等 |
| `categoriesQuery` | 分类排序查询 | `GET /categories` | 保留 ID、父子关系、图标、颜色和排序 |
| `categoriesCreate` | 创建分类 | `POST /admin/categories` | ID 唯一;校验父分类和循环引用 |
| `categoriesUpdate` | 更新分类 | `PUT /admin/categories/:id` | 不修改已有主键;分类关系可审计 |
| `ageRecommendedQuery` | 5 个年龄段推荐 | `GET /recommendations/age?ageKey=3-4` | 保留筛选、Top3 下载 + Top3 收藏、去重及不足补齐规则 |
| `pageConfigFetch` | 读取 current 页面配置 | `GET /page-config` | 保留 home/category/age、version、updatedAt 和 previewImg |
| `pageContentBuild` | 手动首页配置、栏目与展示重建 | `POST /admin/page-config/build`、`GET /admin/page-config` | 保留 featuredIds/hotIds/sections 与现有 page 分支语义 |
| `homeAutoRefresh` | 自动更新首页下载/收藏榜 | 服务端任务、`POST /admin/home/refresh` | 保留各 Top5 和其他首页配置;启停、时区、重复执行可核对 |
| `homeTimerControl` | 开关自动推荐 | `GET/PATCH /admin/home/auto-refresh` | 数据从新数据库读写,替代管理页数据库直读 |
### 4.1 不依赖云函数名称也必须迁移的部分
本节代码位置均属于 `doodle-mini`;接入改造留在原项目,新业务实现位于 `doodle-store/apps/server`。
| 当前依赖 | 代码位置 | 迁移处理 |
| -------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| 云初始化、静默登录、首页配置加载 | `miniprogram/app.ts` | 改为 API 配置与登录;预拉取、缓存、兜底保持可用 |
| 云调用与资料缓存 | `miniprogram/utils/auth.ts` | `wx.login` + HTTP;资料缓存与有效登录态分开管理 |
| 收藏/下载业务包装 | `miniprogram/utils/favorites.ts`、`downloadLogs.ts` | 替换内部实现,保留页面使用方式和统计触发时机 |
| 分类与分龄直接调用 | `pages/category/category.ts`、`pages/age/age.ts` | 接入业务服务层,保持排序/推荐/路由与字段兼容 |
| 头像上传 | `pages/profile/profile.ts` | 替换 `wx.cloud.uploadFile`,存入七牛 |
| 预览图上传与题型发布 | `base/pageMixin.ts` | API 上传 + 管理发布;开发环境隐藏入口不等于管理授权 |
| 云文件字体地址 | `core/font/fontLoader.ts`、`fontProfiles.ts` | 改成 HTTPS 文件引用;继续使用 `wx.loadFontFace` 并验证字形 |
| 内置云预览地址 | `pages/home/home.data.ts` | 迁移并替换 cloud://;兼顾本地兜底数据 |
| 管理页云函数调用 | `supportPages/categoryManage`、`categoryContentManage`、`homeContentManage`、`worksheetSync`、`settings` | 接入管理/用户 API,逐页核对 |
| 管理页直读 settings | `supportPages/homeContentManage/homeContentManage.ts` | 改为管理 API;仅换云函数封装无法覆盖此处 |
| 旧适配器与 HTTP 工具 | `platform/cloud-adapter.ts`、`utils/http.ts` | 逐调用点收口;前者目前指向 doodle,不能假设全站均经它调用 |
| 小程序后台预拉取 URL | 仓库之外的微信后台配置 | 改为新公共配置地址,关闭调试绕过后验证 |
| 云开发定时触发配置 | `cloudfunctions/homeAutoRefresh/config.json` 与控制台 | 核实实际执行时间/时区,迁入调度,切换时停旧任务 |
`wx.reportEvent` 是微信统计能力,不依赖业务 CloudBase 数据库,无需为了迁移而更换。分享、广告和相册权限也继续使用微信能力。
## 5. API、身份和统计约定
### 5.1 新接口与兼容格式
新 API 使用统一响应,例如:
```json
{
"code": 0,
"message": "ok",
"data": {},
"requestId": "request-id"
}
```
- HTTP 状态码区分参数错误、未登录、无权限、冲突、服务异常;失败不包装成成功的空列表。
- 列表提供分页和总量。当前一次最多取 100 条的实现不能成为数据库迁移后的隐形上限;小程序服务层负责拉完所需内容或采用分页展示。
- 新服务内部使用 `id`;小程序 DTO/兼容桥继续提供旧 `_id`、`path`、`previewImg`。数据库路径字段可命名 `miniProgramPath`,Web 路径后续另做映射。
- 云函数目前存在 `{code:0,data}` 与 `{success:true,data,stats}` 两种协议,由业务服务层/兼容桥转换,不要求所有页面同时改写数据形状。
- `previewImg` 是唯一预览字段;公共 DTO 不包含生成器私有配置、用户身份密钥或未发布内容。
### 5.2 微信登录与用户延续
1. 小程序调用 `wx.login()` 获取一次性 code。
2. 服务器用配置的 AppID/AppSecret 调用微信 `code2Session`,取得可信 OpenID,UnionID 可为空。
3. 按当前 AppID + OpenID 查找迁入用户,沿用原 `users._id`;确实不存在时才新建。
4. 签发有期限的应用登录凭证。首期可用 JWT,建议初始有效期 24 小时;过期后通过微信静默登录续用,暂不增加独立刷新令牌体系。
5. 每次用户请求核对账户有效状态;角色从服务端配置/数据取得。注销立即使旧凭证失效。
客户端缓存的 user 对象、OpenID、UUID 都不能作为可信登录凭证。请求遇到 401 时合并并发登录,成功后最多重试一次;写请求需具备幂等性。当前 7 天资料缓存不能替代 API 身份验证。
Web 用户不会凭现有小程序 OpenID 自动变成同一账户。跨端身份绑定作为后续业务设计,当前保留稳定 userId 和可选 UnionID,不自动合并用户。
管理权限首期采用明确管理员名单或角色字段,默认不授权。发布、上下架、批量修订和定时任务配置必须服务端验证;不以客户端 `envVersion`、按钮隐藏或自报角色为依据。
### 5.3 成功保存与收藏计数
- 保存成功的判定继续是 `wx.saveImageToPhotosAlbum` 成功回调;点击保存、图片生成完成、拿到文件、接口返回 200 都不是成功保存。
- 保存成功后递增本地配额、记录 `export_success`,用同一个 exportId 向 API 写下载记录。
- 数据库对 `(userId, exportId)` 建唯一键,下载记录和 `worksheets.downloads + 1` 在同一事务内完成。重试只返回已有结果,不再次加数。
- API 上报失败不能把已保存的图片判定为保存失败。新版可用小型本地补报队列重试同一个 exportId;补报不再次递增本地配额或发送 export_success。队列绑定原 userId,换账户不代报,注销时清除。
- 服务端接收的是客户端成功回报,无法独立证明图片已进入手机相册;本次不把该回报设计成支付凭据。
- 收藏唯一键为 `(userId, worksheetId)`;确实新增/删除关系时才更新 likes。重复请求不增减;注销删除收藏时同步处理受影响计数。
- 展示计数与真实计数分开:展示可沿用真实值 + `likes_seed`/`downloads_seed`,分析和管理原始查询不加 seed。迁移期间不改变已有推荐的种子参与规则。
- 常规重新发布题型不能将真实计数重置为 0。显式统计修订保留管理员入口,但记录操作者及前后值。
旧版缺少 exportId 的保存请求不能凭题型 ID 或几秒时间窗口去重,这会误删用户真实多次保存。兼容桥按每次收到的旧请求生成事件 ID,内部转发重试复用该 ID;接受无法跨独立旧请求精确去重的限制,并监测其调用量。
## 6. 数据迁移
数据库详细设计见 [v3.5 数据库设计文档](./v3.5/数据库设计文档.md),使用 PostgreSQL + Prisma,覆盖以下集合及辅助表。
### 6.1 集合映射与主键
代码实际使用 7 个核心集合。历史设计文档中的 `favorites_log` 与当前代码不一致,应迁移实际 `favorites`;`feedback`、`learning_plans` 等只出现在设计中的集合,先盘点线上是否存在,不默认当成已上线功能。
| CloudBase 集合 | 新表 | 迁移要求 |
| --------------- | --------------- | ----------------------------------------------------------------------------- |
| `users` | `users` | `_id → id` 原值保留;OpenID、可选 UnionID、资料、创建/活跃时间及旧字段保留 |
| `categories` | `categories` | ID、parentId、图标、颜色、排序保留 |
| `worksheets` | `worksheets` | ID、路由、年龄、grade、难度、tags、状态、预览、真实/seed 计数及各时间字段保留 |
| `favorites` | `favorites` | 用户/题型关联不变;重复关系按审计规则归并 |
| `download_logs` | `download_logs` | ID、用户/题型关联、时间、可选 exportId 保留;历史缺 exportId 可为空 |
| `page_configs` | `page_configs` | `current` ID、JSON 结构、版本、时间保留;所有嵌套文件引用转换 |
| `settings` | `settings` | 包括 `homeAutoRefresh.enabled`,先盘点全部线上配置 |
新增必要辅助表:文件迁移/引用表 `assets`、任务执行记录 `job_runs`、管理审计记录 `admin_audit_logs`。它们用于迁移与运行,不代表已有业务集合。
主键采用足够长度的字符串,保留大小写语义,不重新生成所有 ID。业务字段用结构化列,tags/页面配置/扩展配置用 JSON;导入前保留完整原始导出,未识别字段进入迁移报告,不能默默丢弃。
主要索引:用户 `(miniAppId, openid)` 唯一;收藏 `(userId, worksheetId)` 唯一及 `(userId, createdAt)`;下载 `(userId, exportId)` 唯一及用户/题型时间索引;题型分类/状态/排序索引。空 exportId 规范为 NULL,历史多条无 ID 下载不得互相冲突。
### 6.2 导入与审计规则
1. 完整分页导出实际集合,记录每个集合总数、主键清单、时间范围和校验摘要;不可只导出首批 100 条。
2. 规范 BSON/JSON 日期为同一 UTC 时间,数据库和 API 时间一致;业务日与任务时区明确使用 Asia/Shanghai。空值与 0 区分,禁止因默认值覆盖已有数据。
3. 先导分类和用户,再导题型、收藏、下载、配置及设置。脚本可重复执行、按主键 upsert,并支持 dryRun;生产切换后不允许再次用旧导出覆盖新写入。
4. 检测重复 OpenID、重复收藏、重复 exportId、失联关联、非法状态/年龄、缺失图片和未知字段。异常输出逐项报告,不以“跳过错误”当成功。
5. 原用户已经注销而留下的收藏/下载记录应隔离归档,不能重建已注销用户。题型缺失时保留历史 worksheetId 和记录,列表 LEFT JOIN 返回空详情,不能整个历史接口报错。
6. 收藏去重、异常隔离等转换明确写进审计报告:原始数 = 正常导入数 + 合并重复数 + 隔离数。不得无说明要求转换后条数与原始条数相同。
7. 保留历史真实计数及 seed;旧计数与历史条数可能已有差异,先报告,不直接按日志总数重算下载量。切换后的新事件用事务建立一致性。
验收既对总量,也对用户集合、主键和关联。重点核对若干有收藏/历史的老用户、无资料用户、缺失题型记录和多个年龄段,不只抽查首页。
## 7. 文件、首页预拉取与定时任务
### 7.1 文件迁移
资源清单同时覆盖云数据库、页面配置嵌套对象、字体配置、首页内置兜底、用户头像和云存储目录。现有 HTTPS 资源(如部分 `cdn.joeyone.cn` 字体)先核实归属与可用性,不重复搬运已符合目标的文件。
生成可追溯映射:`原 cloud:// fileID → 新 objectKey/assetId → checksum/大小/类型 → 迁移状态`。确认源文件后下载、上传七牛并验证内容;仅改 URL 不算文件已迁移。
- 对可公开使用的预览图、共享图片和小程序所需字体使用公共资源空间/域名,采用版本化对象 key 和缓存。
- 头像等用户文件使用私有访问策略;数据库存对象引用,API 生成访问地址。小程序缓存需记录有效期并刷新,不能把短期签名 URL 当永久 avatarUrl。
- 未来私有线稿/模板另行存放;当前小程序使用的字体与最终预览不因迁移就自动变成保密素材。
- 首期头像和预览图较小,统一通过 `POST /api/v1/assets` 上传,由 API 校验身份、用途、类型、大小和归属,再写七牛。普通用户仅上传本人头像;预览上传需要管理权限。
- 后续有大文件需求时再加入七牛直传凭证。AK/SK 只在服务端;上传地址、对象 key 与归属由服务端控制,不接受任意远程 URL 抓取。
- 旧版仍会先上传 CloudBase 再调用更新资料/发布接口。兼容期保留所需上传能力,兼容入口校验旧文件环境、允许路径和所有者后复制到七牛再提交新数据库;不把永久 cloud:// 引用写入新数据库。
新版本完成后,正式代码和兜底资源均不再依赖 cloud://。旧版本硬编码字体/预览无法通过接口转发改掉,所以旧文件需要与兼容期一起保留,退场时单独检查访问量。
### 7.2 首页配置与推荐
现有 `app.ts` 顺序为预拉取 → 本地缓存 → 云函数兜底。迁移后由新公共 `GET /api/v1/page-config` 提供配置,微信后台预拉取 URL 同步更新,失败继续使用可用缓存/内置数据,网络恢复后按版本刷新。
- 新 API 保留预拉取解析所需的数据形状与 `previewImg`;升级缓存命名/版本,避免长期沿用 cloud:// 和旧首页字段。
- PostgreSQL 中的 current 配置是权威源。静态配置文件若为缓存或旧版兼容发布,只是派生产物;发布失败可重试,保留上一有效版本,不形成第二套内容数据库。
- 手动栏目设置、各 page 分支及 category/age 为 null 的现有行为通过固定输入样本比对,避免迁移时无意改变运营配置。
- 自动刷新仍只更新下载/收藏 Top5,保留手动维护的其他栏目。分龄推荐保持现有“ageMin 等于目标下限且 ageMax 覆盖上限”的匹配方式,业务算法调整留到后续。
- 排名并列时定义稳定排序并与旧样本核对。统计更新不等于内容上新,保留 createdAt/contentUpdatedAt/updatedAt 各自用途。
### 7.3 定时任务
仓库存在 7 段定时表达式 `0 0 22 * * * *`,不得直接当作新调度器的表达式或凭它认定北京时间执行时刻。先核对 CloudBase 控制台实际时区与最近执行记录,再换为服务端明确时区的计划。
保留 enabled 开关;首次配置缺失默认启用的规则显式记录。自动执行与手动执行共用内容服务,使用锁/事务避免互相覆盖,记录执行时间、结果与失败原因。服务重启后发现当日应执行任务遗漏,可补跑一次;旧任务在正式切换前停用。
## 8. 小程序接入与旧版兼容
### 8.1 新版改造
- 本节改造在独立的 `doodle-mini` 项目中实施,不移动小程序源码到 `doodle-store`;按新项目公开 API 契约接入,记录联调使用的服务端版本。
- 按开发、体验、正式配置 API 地址,不在生产构建中使用 localhost/IP。接口域名、下载资源域名及实际上传域名按微信后台要求配置 HTTPS 与备案。
- 建立业务服务层(auth/worksheet/category/content/favorite/download/asset/admin),页面调用业务方法。当前散落的调用点逐一替换,不依赖修改单个 cloud-adapter 就完成迁移。
- HTTP 包装补齐登录、错误格式、超时和日志;`wx.uploadFile`、`wx.downloadFile` 分别处理。当前 `utils/http.ts` 下载包装读取 `res.data` 不适用于 `wx.downloadFile` 的临时文件路径,使用前修正契约。
- 所有正式入口及分享 path 保留。收藏/下载历史中的原 worksheetId 能打开原题型,不因存储变化重定向到默认题型。
- 网络故障时保留已有生成/保存能力;远程收藏与历史展示明确失败/缓存状态,不将失败误显示为真实空数据。
- 微信事件名和 worksheet_id/export_id 口径延续;收藏成功事件仍在远端成功后记录。
### 8.2 过渡期兼容桥
发布新版并不意味着所有旧版立即消失。兼容桥覆盖旧正式用户的登录、资料、收藏、历史和内容查询;旧管理页存在数据库直读,要求管理员使用新版,不承诺旧调试管理页完全兼容。
1. 原云函数名称保留,按旧 action 白名单转发统一服务端业务方法,并转换回原响应格式。
2. 身份从 `cloud.getWXContext()` 取得,不能从 event 信任 OpenID/角色。
3. 兼容请求以服务间密钥签名,包含函数/action、可信 AppID/OpenID、请求体、时间和唯一请求 ID;服务端验签、校验时效并拒绝重放。
4. 内部转发重试使用新的传输请求 ID,但复用同一业务幂等键。签名密钥不下发小程序;管理 action 仍需验证管理员身份。
5. 切换后兼容桥只读写新数据库,旧数据库停止写入;旧存储仅用于兼容文件读取/上传与迁移。
这一阶段有两个接入协议,但只有一份业务实现和一个写入源。兼容模块带退场期限和调用统计,不长期维护两套后端。
## 9. 实施里程碑与可执行待办
| 阶段 | 工作与交付 | 完成条件 |
| ------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| S0 盘点 | 线上集合/文件清单、资源和域名核实、固定功能样本、迁移报告模板 | 迁移对象、数量和异常可追溯;有原始备份 |
| S1 纵向样板 | doodle-store pnpm 工作区、apps/server 骨架、数据库迁移、微信登录、题型读取、收藏/下载事务 | 工作区安装/构建通过;老用户身份关联正确;重复收藏/保存请求不重复计数 |
| S2 功能迁移 | 补齐其余 API、管理权限、配置/推荐/任务、文件模块 | 17 个云函数各有落点,管理页直读替代完成 |
| S3 接入与试迁 | doodle-mini 服务层改造、跨项目契约核对、文件与数据库试导、兼容桥、体验版部署 | 新旧用户协议共同访问测试数据库;页面/字体/预拉取实测通过 |
| S4 上线切换 | 生产导入、单写切换、正式版本发布、在线核对 | 用户关联与累计数据一致;新写入只在新数据库 |
| S5 退场 | 持续核对兼容/旧文件调用、移除云开发依赖、归档旧数据 | 达到退场门槛后完全退出 CloudBase |
### 执行清单
- [ ] S0:导出实际集合及文件清单;确认 API 域名、备案、服务器和七牛环境。
- [ ] S0:记录老用户/题型/推荐/配置样本及当前原始计数,准备数据审计规则。
- [x] S1:新建独立 `doodle-store` 项目,建立 pnpm workspace、锁文件、版本约束及 `apps/server` 的独立构建/检查脚本;不迁入 `doodle-mini` 源码。2026-10-12 已完成基础初始化,格式/代码/类型检查、5 项基础测试与构建入口 HTTP 检查通过;不代表 S1 业务样板已完成。
- [ ] S1:在 `doodle-store/apps/server` 建立 PostgreSQL/Prisma 数据结构、统一响应、日志与版本化公开接口契约。
- [ ] S1:微信登录沿用原 userId;服务端权限校验;收藏与下载幂等事务。
- [ ] S2:完成全部公开/用户/管理接口及现有推荐行为对照。
- [ ] S2:迁移文件、首页预拉取配置和定时任务,完成手动/自动刷新互斥。
- [ ] S3:在 `doodle-mini` 替换所有正式云调用、管理直读、上传和字体解析,核对旧缓存升级及新 API 契约;记录两个项目的联调版本。
- [ ] S3:验证兼容签名、身份、响应格式、旧文件新增迁移及无 exportId 请求行为。
- [ ] S3:试导后核对集合总量、主键、关联、转换/隔离项及文件校验。
- [ ] S3:完成备份恢复演练、API 检查和微信体验版真机验收。
- [ ] S4:准备写入冻结、切换顺序、回滚记录,再执行生产切换与正式发布。
- [ ] S5:确认兼容/旧资源访问退场门槛,归档后停止旧任务、函数、存储与套餐。
## 10. 正式切换与回滚
### 10.1 推荐切换:短维护窗口,全量最终导入
现有业务较小,优先使用经过演练的短写入冻结,不建设长期双写或增量同步平台。窗口时长由试导耗时决定,不能提前承诺“零停机”或固定分钟数。
1. 先完成试迁和体验版验收,保持现有正式版仍使用 CloudBase。新正式版完成审核准备,但暂不放量。
2. 预部署全部兼容函数,支持统一的 `cloud / readonly / server` 切换模式;在演练中验证。旧云函数对用户创建/活跃时间、收藏、下载、资料和管理写入均受模式约束。
3. 选择低峰窗口,冻结旧端全部业务写入,停止旧推荐任务和管理发布;明确提示维护,读取与本地生成/保存仍可用。窗口内旧版成功保存但日志未上报的限制应在切换记录中注明;新版补报队列在试迁时验证。
4. 在冻结状态导出最终快照,完成新增文件迁移和全量导入审计;此前试迁目标不得保留混入的测试记录。
5. 将兼容桥整体切到 server,新数据库成为唯一写入源;更新公共配置/预拉取来源,开启新定时任务。核对桥和任务状态,旧数据库保持只读。
6. 发布新版小程序;过渡期新版 HTTP 和旧版桥均调用同一业务服务。新版本未推广完成也不得把一部分用户写回旧数据库。
7. 核对登录、收藏、成功保存记录、头像/字体、首页刷新与真实计数,再进入观察期。
若首次盘点发现数据规模或维护窗口不满足全量导入,应重新设计最终同步方式,不能直接在生产并行写两个库。试迁阶段的测试用户和数据不得混入生产。
### 10.2 回滚的两种情况
| 时点 | 回滚方式 |
| ------------------------ | ------------------------------------------------------------- |
| 新库尚未接收真实业务写入 | 停新任务/新入口,将桥恢复 cloud;解除旧库写入冻结,恢复旧任务 |
| 新库已接收真实业务写入 | 优先回滚 API 版本并继续使用新数据库;不可直接把客户端切回旧库 |
如必须回到旧数据库:同时冻结 HTTP 和兼容桥写入,导出新库最新数据并反向转换,将切换后的新增、修改和删除全部同步到旧库,完成关联/计数审计后再恢复旧入口。反向导入按新库权威快照替换相关数据,不能只补新增,否则会复活已取消收藏或已注销用户。
S3 应验证反向转换和恢复步骤。每次切换保存版本、模式、时间点和审计报告;数据库变更采用可兼容旧 API 版本的方式,避免应用回滚时表结构无法使用。
### 10.3 CloudBase 退场门槛
建议先观察至少 14 天,再结合实际旧版调用决定退场;14 天本身不是可以删除云资源的证明。要求:
- 新版正式功能及管理操作已稳定使用新 API。
- 兼容调用和旧 cloud:// 文件访问在约定观察窗口内已归零,或明确接受剩余旧版必须升级才能继续在线使用;不能凭新版发布推断旧版本消失。
- 新数据库、七牛文件及恢复副本均可使用,旧数据已完成可恢复归档。
- 旧任务停止,新版无 CloudBase 运行依赖;客户端 cloud 初始化及项目云配置在最终清理阶段移除。
删除旧资源和停止套餐应在上述条件满足后执行。兼容函数、上传或云字体仍使用旧环境时,不能宣称已完全迁出或已省下全部 19.9 元费用。
## 11. 验收与最低运行要求
### 11.1 自动验证与真实环境验收
| 验证层 | 检查项目 | 通过标准 |
| ----------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 服务端自动检查 | 登录错误、用户越权、管理权限、分页、重复收藏、并发重复 exportId、事务失败、注销 | 不越权、不重复计数;失败无半条业务写入 |
| 工程与契约检查 | doodle-store 工作区安装/构建、应用依赖边界、公开契约与 doodle-mini 适配 | 服务端可独立构建;客户端不导入私有模块;联调版本及兼容行为可追溯 |
| 行为对照 | 固定输入的分类、分龄、手动配置、自动榜单、展示/真实计数 | 字段、去重、排序、兜底与预期一致;差异显式记录 |
| 迁移审计 | ID、数量、日期、关系、重复/隔离项、旧计数、文件 checksum | 数据量可对账;可用关联完整;无未解释丢失 |
| 小程序体验版/真机 | 老用户登录、首页/分类/年龄、预览生成、实际保存、收藏/历史、资料、注销、管理发布 | 使用有效域名和证书;功能链路与原版一致 |
| 故障与回滚 | token 过期、离线、保存后上报失败、API 重启、备份恢复、切换后新增/删除 | 本地保存结果准确;同事件重试不重复;恢复不遗漏切换后的变化 |
微信开发者工具“跳过域名校验”下通过不算正式网络验收。至少检查 iOS、Android 的上传、图片/字体加载和保存回调。服务端和脚本可以自动测试;相册权限、实际保存、微信分享、广告、预拉取与真机字体效果需实际微信环境验证,不能以静态检查替代。
管理员验收包括:首页手动推荐/栏目、定时开关、分类创建/更新、题型上传/发布/上下架、年龄标签同步及 dryRun。正式用户验收还应覆盖所有分类入口、首页推荐、分龄和收藏历史中的跳转路径。
### 11.2 轻量运行基线
- 每日自动备份数据库到服务器之外,备份访问受控并保留滚动版本;上线前成功恢复一次。七牛文件迁移清单和必要配置随数据备份保留。
- 初始目标可设数据库 RPO 不超过 24 小时、恢复操作 RTO 不超过 60 分钟,以演练结果确认;若需要更小数据损失窗口,再增加 binlog/更频繁备份。每日备份不等于零数据损失。
- HTTPS 自动续期;检查 API/数据库健康、磁盘、错误、备份失败和定时任务失败。日志轮转,不记录 AppSecret、登录 code、令牌、签名 URL 或完整用户敏感信息。
- 版本和数据库变更可追踪;更新前备份,保留上一可运行应用版本。对本项目采用简单可执行步骤,不要求集群或复杂运维平台。
- 发布记录关联 `doodle-store` 服务端版本、数据库迁移版本和 `doodle-mini` 小程序版本;后续后台/Web 各自记录版本。回滚服务端时保持已发布客户端的 API 兼容,不要求两个项目同步回滚。
- 成本按 API 流量、数据库体量、七牛存储/下载/请求和实际维护工时记录,不以“已有服务器”推导零总成本。
## 12. AI 辅助工作量与排期
以下是初步预算,不是已验证的工期承诺。假设线上数据和文件较少、服务器可部署、权限齐备、沿用本方案接口/数据库选择;包含 `doodle-store` 基础 pnpm 工作区与 `apps/server`,以及 `doodle-mini` 接入改造,不包含独立后台/Web 页面、私有包发布平台或存量绘图重写。
| 工作包 | AI 辅助后的初步投入 |
| -------------------------------------------------------------- | ------------------------------------------------ |
| 实际盘点、固定样本、接口与导入字段确认 | 2–3 小时 |
| doodle-store 工作区、服务端骨架、登录/权限、现有业务与管理 API | 8–12 小时 |
| 数据/文件导入、对账与反向转换脚本 | 4–8 小时 |
| 小程序服务层、登录、上传、缓存/字体与页面适配 | 6–10 小时 |
| 部署、兼容桥、必要测试/真机联调与切换演练 | 6–10 小时 |
| 合计 | 26–43 小时;约加 25% 修正余量后按 32–54 小时准备 |
按每天 8 小时集中投入,可先预算约 4–7 个工作日;兼职开发的日历周期更长,外部审核/备案等待另算。S5 还有至少 14 天的退场观察,代码与正式切换完成时间不等于关闭 CloudBase 的时间,观察期也不代表连续全日投入。先完成 S0 + S1 的纵向样板,以实际用时、数据量和发现的问题更新后续预算。
AI 主要承担接口/模型初稿、SQL 转换、数据脚本、小程序调用替换、部署配置和测试用例;必须运行验证的内容是老用户身份、导入结果、文件可用性、实际保存、管理权限及生产切换。无需将常规备份和升级夸大为单独的大型工程。
## 13. 与现有规划的关系及参考
本方案覆盖旧文档“暂缓核心 API/数据库迁移”“只替换 cloud-adapter 即可”的表述。目标改为自有服务器统一后端;过渡期保留 CloudBase 仅用于兼容和恢复。现有产品功能待办、Web 试点和服务端绘图的范围分别管理。
工程组织以本方案为准:不在 `doodle-mini` 中新增服务端工程;独立 `doodle-store` pnpm Monorepo 管理服务端、后续后台和 Web,目前只完成基础初始化,业务迁移仍待实施。本方案迁入 `doodle-store/docs/云服务端迁移方案.md`,不在两个项目中维护重复副本。
实施依据是 `doodle-mini` 当前代码,历史文档中的 Prisma 示例、免费额度、Node 版本和“零成本 CDN”表述不能直接作为部署事实。
- [微信小程序网络与服务器域名要求](https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html):HTTPS、备案、request/upload/download 域名与真实环境校验。
- [微信小程序登录流程](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html):code2Session 与身份/会话密钥边界。
- [七牛上传凭证](https://developer.qiniu.com/kodo/1208/upload-token):凭证由服务端生成,限制范围和有效期;直传作为后续可选优化。
- [PostgreSQL 数据库设计(v3.5)](./v3.5/数据库设计文档.md):表结构、约束、事务、导入映射与数据库验收;具体 PostgreSQL / Prisma 版本在实施时验证并锁定。
- [pnpm 工作区](https://pnpm.io/workspaces):Monorepo、内部包引用及工作区锁文件。
- [pnpm 筛选命令](https://pnpm.io/filtering):按应用、依赖关系执行开发与检查任务。
- `doodle-mini` 仓库依据:`cloudfunctions/*/index.js`、`miniprogram/app.ts`、`utils/auth.ts`、`favorites.ts`、`downloadLogs.ts`、`downloadPrint.ts`、`base/pageMixin.ts`、`pages/profile/profile.ts`、`core/font/*` 及上述管理页面。