263 lines
15 KiB
Markdown
Executable File
263 lines
15 KiB
Markdown
Executable File
# 数字我 / 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 数字分身,非真人」声明;专家回答强制免责声明。
|