# 数字我 / 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/` 就能看到注入了 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/ 能聊 # 广场 /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/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 数字分身,非真人」声明;专家回答强制免责声明。