Files
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

263 lines
15 KiB
Markdown
Executable File
Raw Permalink 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.
# 数字我 / Digital Me — 部署清单(DEPLOY)
> 目标:把 `AlexLi2244/digital-me` 上线到阿里云 ECS(已配 CI/CD:push `master` 自动部署)。
## 0. 架构与端口
| 组件 | 位置 | 说明 |
|---|---|---|
| 后端 API | ECS `/var/www/digitalme-api.invoratec.cn`,PM2 `digitalme-api`,**端口 3603** | Node20+Express |
| 前端 SPA | ECS `/var/www/digitalme-spa`,Nginx 静态 | Vue3 build |
| 域名 | API `digitalme-api.invoratec.cn` / 前端 `digitalme.invoratec.cn` | 需各自 DNS + 证书 |
| 数据库 | MongoDB `creator_cn` 库 + **`COLLECTION_PREFIX=dm_`** 隔离 | 与其它产品共库不撞表 |
| 媒体 | 配齐 `OSS_*` → 阿里云 OSS(推荐);否则后端 `data/uploads`(Nginx 经 API 暴露 `/uploads`) | 见下方 `.env` OSS 段 |
> CI/CD `rm:false` 保留服务器上的 `.env / node_modules / data`——**`.env` 只在服务器上手建一次**,不进仓库。
## 1. GitHub 仓库 Secrets(一次)
Settings → Secrets and variables → Actions:
- `ECS_HOST` `ECS_USER` `ECS_PASSWORD`
## 2. 服务器一次性准备
```bash
# Node 20 + PM2
node -v # >=20;如无:nvm install 20
npm i -g pm2
# 目录
mkdir -p /var/www/digitalme-api.invoratec.cn /var/www/digitalme-spa
# MongoDB:复用现有 creator_cn 库即可(dm_ 前缀隔离),或自建 digital_me 库
```
### 后端 `.env`(手建于 `/var/www/digitalme-api.invoratec.cn/.env`)
```ini
NODE_ENV=production
PORT=3603
PUBLIC_URL=https://digitalme-api.invoratec.cn # 必须公网可达(捏脸/声音/口播/作品 靠它回拉)
CORS_ORIGINS=https://digitalme.invoratec.cn
WEB_BASE_URL=https://digitalme.invoratec.cn # 支付后跳回
# 安全(生产强制:JWT_SECRET 缺省会拒绝启动)
JWT_SECRET=<强随机 32+ 位>
DATA_ENC_KEY=<强随机> # 访客记录加密,强烈建议
SUPER_ADMIN_EMAILS=lu.huang0912@gmail.com # 蓝V 认证后台
TRUST_PROXY=true # 在 Nginx 后,取真实 IP
# 数据库(共库方案)
MONGO_URI=mongodb://127.0.0.1:27017
MONGO_DB=creator_cn
COLLECTION_PREFIX=dm_
# AI(DashScope 一把钥匙)
DASHSCOPE_API_KEY=sk-...
AVATAR_PROVIDER=dashscope
MODERATION_MODE=llm # 用 Qwen 做审核(或接 MODERATION_URL 专业服务)
# 媒体存储(阿里云 OSS,推荐):配齐即自动用 OSS,否则回落本地磁盘 + /uploads。
# OSS 直链公网可达 → 免公网穿透、重部署不丢片、DashScope 直接按 URL 拉头像/音频。
OSS_REGION=oss-cn-hangzhou # 必带 oss- 前缀
OSS_BUCKET=invoratec-aimall # 复用共享 bucket,靠 OSS_PREFIX 隔离
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_PREFIX=digitalme
# bucket 非「公开读」时,填 CDN/自定义域名,否则存库的头像直链取不到:
# OSS_PUBLIC_BASE=https://cdn.invoratec.cn
# 支付(先 stub,拿到密钥后切 stripe/wechat)
PAYMENT_PROVIDER=stub
TOKENS_PER_YUAN=10
# STRIPE_SECRET_KEY=sk_live_...
# STRIPE_WEBHOOK_SECRET=whsec_...
```
> 改完 `.env` 需 `pm2 restart digitalme-api` 才生效(CI/CD 也会重启)。
### Nginx(两个 server 块)
```nginx
# 后端 API
server {
server_name digitalme-api.invoratec.cn;
client_max_body_size 30m; # 头像/录音/试卷上传
location / { proxy_pass http://127.0.0.1:3603; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; }
# Stripe webhook 需原始 body——proxy 默认即可,勿在此改写 body。
# listen 443 ssl; + 证书
}
# 前端 SPA
server {
server_name digitalme.invoratec.cn;
root /var/www/digitalme-spa; index index.html;
location / { try_files $uri $uri/ /index.html; } # history 路由
# listen 443 ssl; + 证书
}
```
证书:`certbot --nginx -d digitalme-api.invoratec.cn -d digitalme.invoratec.cn`。
### 分享卡片 og meta(微信/IM 富卡片)——默认休眠,按需启用
分身分享链接 `/me/:slug`、`/s/:token` 甩进微信时,抓取器只读静态 HTML、不跑 JS,
默认卡片没有对方头像/名字。后端已内置 SSR:`src/routes/share.js` 会给这两个路径注入
per-分身 的 og meta(头像+名字+一句话)。**代码常驻但默认不接**,启用只需两步:
1. **后端 `.env` 增一行**(让它注入真 SPA index、页面仍是完整 App):
```ini
SPA_INDEX_PATH=/var/www/digitalme-spa/index.html
# 可选:无头像时的兜底卡片图(绝对 URL)
# OG_FALLBACK_IMAGE=https://digitalme.invoratec.cn/og-default.png
```
不填 `SPA_INDEX_PATH` 也能用——回落成「meta + 跳转到 web 页」的独立壳(爬虫拿卡片、
真人被转走)。`pm2 restart digitalme-api` 生效。
2. **前端 SPA 的 Nginx server 块**:把这两个路径的文档请求回源到后端,其余静态照旧:
```nginx
server {
server_name digitalme.invoratec.cn;
root /var/www/digitalme-spa; index index.html;
# 分享页文档 → 后端注入 og meta(资产 /assets 仍走下面的静态)
location ~ ^/(me|s)/ { proxy_pass http://127.0.0.1:3603; proxy_set_header Host $host; }
location / { try_files $uri $uri/ /index.html; } # history 路由
}
```
注入的 index.html 保留原 `/assets/...` 相对引用 → 浏览器仍从 web 域名静态取,App 照常启动。
> 先验证:改前直接打 API 域名 `https://digitalme-api.invoratec.cn/me/<slug>` 就能看到注入了 og:title/og:image 的 HTML(此时资产指向 API 域名会 404,仅用于验证卡片数据)。
### 微信「转发给朋友/朋友圈」自定义卡片(JS-SDK)——默认休眠,需公众号资质
上面的 og meta 解决**被动抓取**卡片;微信里点「•••→转发给朋友/分享到朋友圈」时的**自定义**
卡片(自定义标题/描述/缩略图)要用 JS-SDK。代码已内置、默认休眠,启用三步:
1. **有一个认证公众号(服务号最佳)**,拿到它的 appId + appSecret。
2. **后端 `.env` 增两行**(是【公众号】的,不是开放平台扫码登录那对):
```ini
WX_MP_APP_ID=wx____
WX_MP_APP_SECRET=____
```
`pm2 restart digitalme-api`。签名接口 `GET /api/wx/jssdk?url=` 即从 `{configured:false}` 变为返回真签名。
3. **公众号后台 → 设置 → 公众号设置 → 功能设置**:把 `digitalme.invoratec.cn` 填进
**「JS 接口安全域名」**(需下载校验文件放到 SPA 根目录,Nginx 能返回即可)。
前端 `composables/useWxShare.ts` 只在**微信内置浏览器**且接口 `configured:true` 时才加载 jweixin
并注册分享卡;非微信/未配资质全程 no-op。已接入 `/me/:slug`、`/s/:token` 两个公开页,
分享卡自动用分身的名字+一句话+头像。
> CSP 注意:SPA 若加了 CSP,`script-src` 要放行 `https://res.wx.qq.com`(jweixin 从这里加载)。当前 Nginx 静态无 CSP,暂无需改。
## 3. 部署(日常)
**push `master` 即自动部署**(改 `digital-me-backend/**` 触发后端、`digital-me-web/**` 触发前端)。
- 前端 CI 用 `build:fast`(跳过 vue-tsc);本地请先 `npm run build` 过一遍严格类型检查再推。
- 手动兜底:后端 scp `src/ package*.json` 到目标目录 → `npm install --production` → `pm2 restart digitalme-api`;前端 `npm ci && npm run build:fast` → scp `dist/*` 到 `/var/www/digitalme-spa` → `nginx -s reload`。
## 4. 上线后冒烟(逐条核对)
```bash
API=https://digitalme-api.invoratec.cn
curl -s $API/health # {ok:true}
# 注册→建分身→喂知识→对话(带 RAG 来源)
# 公开页 https://digitalme.invoratec.cn/me/<slug> 能聊
# 广场 /plaza、排行榜 /leaderboard 可访问
```
- [ ] `/health` 通
- [ ] 注册/登录、建分身、喂知识、对话有来源
- [ ] 形象/声音/口播(确认 `PUBLIC_URL` 公网可达,DashScope 能回拉)
- [ ] 社交卡片 `/s/:token`、广场 `/plaza`、互考 `/challenge/:slug`、排行榜
- [ ] 群聊 `/room/:token`、错题反考、作品集内联展示
- [ ] 钱包充值(stub 即时)、专家付费咨询扣费+分成、免责声明
- [ ] 超管 `/admin` 可授蓝V(你的邮箱在 `SUPER_ADMIN_EMAILS`)
- [ ] 内容审核拦截、限流 429、账号注销物理删除
## 5. 支付正式上线(拿到密钥后)
- **Stripe**:填 `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`、`PAYMENT_PROVIDER=stripe`;
Stripe 后台 Webhook 指向 `https://digitalme-api.invoratec.cn/api/wallet/webhook/stripe`,
事件选 `checkout.session.completed`。重启后端。无需改代码。
- **微信支付**:拿到商户号后在 `services/payments.js` 的 `wechat` 分支补 Native/H5 下单 +
回调验签(结构与 Stripe 一致),`PAYMENT_PROVIDER=wechat`。
## 5b. 两个独立系统(成人 / 青少年)
同一套代码,用 `EDITION` 锁定为单一系统,**各自独立部署**(独立域名 + 独立 DB + 独立用户体系):
| 系统 | EDITION | 功能 | 建议域名 / DB |
|---|---|---|---|
| 成人系统 | `adult` | 社交分身/广场/群聊/专家付费/钱包 + 建分身/对话 | `digitalme.invoratec.cn` / `MONGO_DB=digital_me_adult`(或 `COLLECTION_PREFIX=dma_`)|
| 青少年系统 | `teen` | 错题反考/学友互考/排行榜/家长监护 + 建分身/对话 | `edu.digitalme.invoratec.cn` / `MONGO_DB=digital_me_teen`(或 `dmt_`)|
每个系统 = 一套后端(PM2 各自端口) + 一套前端 build。前端 build 时设 `VITE_EDITION=adult|teen`
(隐藏非本系统导航、登录页文案:成人系统无选择器;青少年系统区分「学生/家长」)。
后端 `EDITION` 决定挂载哪些路由(非本系统 API 直接 404)。两系统 DB 独立 → 用户/数据完全隔离。
> 家长监护属于**青少年系统**:家长在青少年系统内注册(身份=家长),与孩子同库才能绑定。
> 成人系统不含监护/错题。两系统都共享「建分身/克隆/对话/审核/安全」内核代码。
### CI/CD(已配 4 条独立工作流)
`.github/workflows/`:`deploy-adult-backend.yml`·`deploy-adult-web.yml`·`deploy-teen-backend.yml`·`deploy-teen-web.yml`。
push `master` 改 `digital-me-backend/**` → 两套后端都重部署(共享代码);改 `digital-me-web/**` → 两套前端都重 build。
| 系统 | 后端目录 / PM2 / 端口 | 前端目录 | API 域名 / 前端域名 |
|---|---|---|---|
| 成人 | `/var/www/digitalme-adult-api` · `digitalme-adult-api` · **3603** | `/var/www/digitalme-adult-spa` | `digitalme-api.invoratec.cn` / `digitalme.invoratec.cn` |
| 青少年 | `/var/www/digitalme-teen-api` · `digitalme-teen-api` · **3604** | `/var/www/digitalme-teen-spa` | `edu-api.invoratec.cn` / `edu.invoratec.cn` |
工作流在 pm2 启动命令里固定 `EDITION`+`PORT`(dotenv 不覆盖已存在的 env);前端 build 时注入 `VITE_EDITION`+`VITE_API_BASE_URL`。
每个后端目录的 `.env` 各自配 **独立 `MONGO_DB`**(`digital_me_adult` / `digital_me_teen`)+ AI/安全密钥。
**一次性服务器准备**:创建上述 4 个目录 + 两份 `.env`;4 个 Nginx server 块(2 API 反代 :3603/:3604 + 2 SPA history 路由)+ 4 域名证书。
> 迁移注意:旧单体 `digitalme-api`(PM2) 也用 3603——上线成人系统前先 `pm2 delete digitalme-api` 释放端口。
## 5c. 单点工具 MVP(独立打包)
3 个工具可作为**独立单页产品**发布(复用同一后端,无需全平台导航)。前端 build 时设
`VITE_APP`:根路径直达该工具、隐藏平台导航。
| 工具 | VITE_APP | 路由 | 复用 |
|---|---|---|---|
| 口播侠 TalkShot | `talkshot` | /talkshot | avatar+voice+EMO |
| 反考机 ReQuiz | `requiz` | /requiz | errors quiz(需后端含 twins,所有版本都有)|
| 互考擂台 Arena | `arena` | /arena | publish+challenge+leaderboard(需 EDITION=teen/both)|
例:`VITE_APP=talkshot VITE_API_BASE_URL=https://digitalme-api.invoratec.cn npm run build:fast`,
dist 部署到该工具的独立域名即可。`VITE_APP` 未设=`full`(整平台,3 工具仍可经 /requiz 等访问)。
## 5d. 产品工厂(商店 + 购买即开通)
把多个 MVP 聚成一个「工厂」站点:商店列 SKU、每个 SKU 独立购买链接、买后
自动开通账号(权益) + magic link 即用。**上架新 MVP = 在 `src/products.js` 加一条
SKU**(无需新部署)。
**部署形态**:`VITE_FACTORY=c`(2C 工厂) / `=b`(2B 工厂)。根路径→`/store`,隐藏平台
导航,工具按 entitlement 门控(未购→`/buy/:sku`)。`=all`/不设=全量平台(不门控)。
```bash
# 2C 产品工厂
VITE_FACTORY=c VITE_API_BASE_URL=https://digitalme-api.invoratec.cn npm run build:fast
```
建议两个工厂各一域名:2C `shop.invoratec.cn`、2B `b.invoratec.cn`,同一后端。
**购买链路**:`/buy/:sku` 填邮箱 → `POST /api/store/checkout` → (stub 通道立即开通
并返回 redeemToken;真实通道返回 payUrl,支付回调里调 `fulfill()`) → 前端经
`/api/store/redeem` 兑换 magic token → 自动登入并进工具。magic token 30 分钟、单次。
**接真实支付(微信 Native 扫码付,已实现)**:商户平台拿齐 `WXPAY_APP_ID/MCH_ID/
API_V3_KEY/CERT_SERIAL` + 商户私钥(`WXPAY_PRIVATE_KEY` 或 `_PATH`),设 `STORE_PROVIDER=wechat`。
回调地址 `https://<api 域名>/api/store/webhook/wechat`(微信商户平台配置 + 服务器需公网可达)。
链路:checkout→`nativeOrder` 拿 code_url→前端二维码→用户扫码付→微信 POST 回调→
`decryptNotify`(AES-256-GCM 认证解密)→`fulfill(out_trade_no)` 开通→前端轮询 `/order/:id`
拿 redeemToken→自动登入进工具。密钥未配齐时自动回落 stub(不中断)。
(Stripe 同理:在其 webhook 调 `store.js#fulfill`。)
**渠道归因**:进站捕获 `utm_*`(utm.ts) → 随 `checkout` 存订单;埋点
`store_view/buy_view/checkout_start/purchase` → 超管 `/api/admin/funnel` 看「哪条新媒体带来购买」。
## 6. 安全红线(上线必做)
- `JWT_SECRET`、`DATA_ENC_KEY` 用强随机;`SUPER_ADMIN_EMAILS` 仅限你本人。
- `MODERATION_MODE=llm` 或接 `MODERATION_URL` 专业审核(勿只靠本地正则)。
- 备份 MongoDB;媒体走 OSS:配齐 `OSS_*` 即自动启用(`services/storage.js` 的 `saveMedia` 统一落地,OSS 失败自动回落本地磁盘,不中断上传/口播)。
- 合规:公开页已带「AI 数字分身,非真人」声明;专家回答强制免责声明。