Files
digital-me-history/DEPLOY.md
T
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

15 KiB
Executable File
Raw Blame History

数字我 / 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. 服务器一次性准备

# 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)

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 块)

# 后端 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):

    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 块:把这两个路径的文档请求回源到后端,其余静态照旧:

    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 增两行(是【公众号】的,不是开放平台扫码登录那对):
    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. 上线后冒烟(逐条核对)

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/不设=全量平台(不门控)。

# 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 数字分身,非真人」声明;专家回答强制免责声明。