feat: 初始化项目

This commit is contained in:
R524809
2026-10-12 15:22:27 +08:00
commit 3c6fd2c8ca
37 changed files with 4859 additions and 0 deletions
+471
View File
@@ -0,0 +1,471 @@
# 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/*` 及上述管理页面。