项目:运营搭建平台(vue3-course) | 版本:v1.0 | 生成日期:2026-10-05 | 依据:源码分析
| 项 | 要求 | 依据 |
|---|---|---|
| Node.js | 22.x | CI(.github/workflows/ci.yml 的 setup-node@4, node-version: 22)与 @tsconfig/node22 基线 |
| 包管理器 | pnpm 10(package.json 的 packageManager 字段锁定 pnpm@10.34.5 + sha512) | Corepack 可直接启用 |
| 数据库 | MySQL 8.x(CI 用 mysql:8.4 镜像) | 平台主库;PostgreSQL 仅作数据源查询驱动 |
| 操作系统 | macOS / Linux 均可(dev 脚本用 NODE_ENV=development nodemon,Windows 需交叉环境) | 根 package.json scripts |
| 端口 | 8001(work-server)、6001(portal-server)、8002/6002(对应前端 dev server) | .env.example 45-47 行注释 |
安装注意:根 postinstall 会自动执行 npm run build && npm run build:mock-cdn && npm run build:mock-logger && npm run build:materials——首次 pnpm i 即完成整包构建,CI 中因此显式使用 --ignore-scripts 分步执行以便失败定位(ci.yml 注释)。pnpm-workspace.yaml 配置了 publicHoistPattern: '*'(注释说明等价 shamefully-hoist,vite-node/ts-node 直跑需要)与 vue/@types/koa 的 overrides 单版本锁定。
pnpm i # 安装依赖 + 自动构建全部共享包与物料
cp .env.example .env # 生成环境配置(见第 3 节,至少改 MYSQL_* 与两个密钥)
npm run db:init # 建库建表 + 种子(admin001/admin002,初始密码 88888888)
npm run start:work # 构建 work-front(admin 桶)+work-server 并启动 :8001
npm run start:portal # 构建 work-front(visitor 桶)+portal-server 并启动 :6001
# 或守护进程方式(README 推荐的后台服务):
npm run start:work-daemon # 经 packages/work-server/start-daemon.cjs:cluster 按 CPU 核数 fork 多 worker + 崩溃 refork(上限 1000 次);`start:work-process` 为不带 refork 的同型脚本启动后验证路径:登录 http://127.0.0.1:8001/page/sign-in → 看板管理 /page/manage/dashboard-list → 新建/编辑/发布 → 访客 http://127.0.0.1:6001/d/<uuid>。运维手册要求的冒烟三连:管理台登录、新建并发布一个看板、访客端 /d/:uuid 打开且能取数(docs/plans/deploy-ops-2026-09.md 末节)。
npm run dev:work-server # nodemon + ts-node 直跑 work-server/src/index.ts,:8001
npm run dev:work-front # vite-node scripts/dev-work-front.ts,Vite dev server :8002
npm run dev:portal-server # 同构,:6001
npm run dev:portal-front # 同一个 work-front 包的第二个 dev server :6002README 特别说明了 6002 的代理拓扑:它与 8002 是同一个包的两个 dev server,代理目标不同——6002 把 /api 转给 8001、其余转给 6001。访问开发页面用 8002(http://127.0.0.1:8002/page/sign-in)。
npm run dev:banner # 独立开发 material-banner-slides(vite -c vite.config.common.ts)
npm run dev:products # 独立开发 material-product-list
npm run dev:business # @my/business 业务组件开发模式README 提醒:物料开发完成后必须提升版本号再执行 npm run build:materials 重新构建——物料按 <name>/<version>/ 目录进 mock CDN,不升版本等于白改。
完整注释见 .env.example(104 行);下表按域归组:
| 变量 | 必填 | 默认 | 说明 |
|---|---|---|---|
MYSQL_HOST / MYSQL_PORT / MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD | 是 | 127.0.0.1 / 3306 / — | 平台主库连接(packages/init-database/src/util.ts 与 packages/server-kit/src/db.ts 读取) |
JWT_SECRET | 生产必填 | 空(开发仅 warn) | 登录态签名;生产缺失/为空直接拒绝启动(packages/work-server/src/index.ts 38-47 行启动自检);生成 openssl rand -hex 32 |
SECRET_BOX_KEY | 有密文即必填 | 空 | 数据源/连接凭据的 AES-256-GCM 主密钥(64 hex 或 base64,解码须 32 字节,packages/work-server/src/util/secret-box.ts)。库中已有 enc:v1: 行而密钥缺失 → 启动即退出(index.ts 101-118 行异步预检)。丢失 = 全部凭据不可恢复;轮换必须先 db:decrypt-secrets 再换钥 db:encrypt-secrets,且轮换后重启 work-server(数据源连接池持有旧解密口令,池不失效重建——见 14 篇第 4 节) |
PASSWORD_SALT | 迁移期保留 | — | 🔴 已废弃:旧 HMAC 口令哈希校验依赖;全部账号登录一次后(SELECT COUNT(*) FROM user_info WHERE password NOT LIKE '$2%' 归零)可删(.env.example 18-20 行) |
REQUIRE_DB | 仅 CI | 空 | 置 true 时数据库探活失败直接中断测试(否则 19 个真库用例静默跳过——"假绿") |
DB_AVAILABLE | 自动 | 自动 | 由 packages/work-server/__tests__/setup.ts 探活后写入,门控 dbDescribe 用例 |
AI_API_URL / AI_API_KEY / AI_MODEL | 用 AI 时 | — | OpenAI 兼容上游(示例 deepseek);密钥只在服务端,前端经 /api/post/ai/chat/stream 代理 |
INTERNAL_TOKEN | 建议配置 | 空(通道禁用 fail-closed) | portal→work-server 取数通道共享密钥(packages/work-server/src/config.ts);未配置时 portal 查询一律 403 |
WORK_SERVER_ORIGIN | portal 需要 | http://127.0.0.1:8001 | portal 转发取数的目标 |
WORK_SERVER_PORT / PORTAL_SERVER_PORT | 否 | 8001 / 6001 | 端口可配:开发生产同机并跑、CI 并行错开 |
TRUST_PROXY | 有 TLS 代理时=1 | 空 | 置 1 才采信 X-Forwarded-For/Proto;不置的后果:限流把所有访客算同一 IP、cookie 不带 Secure(.env.example 49-51);HSTS 也仅在置 1 时下发(README 安全基线) |
CSRF_ALLOWED_ORIGINS | 跨域部署时 | 空(仅同源) | 状态变更请求的跨源白名单,逗号分隔 |
RATE_MAX_SIGN_IN=20 / RATE_WINDOW_SIGN_IN_MS=600000、RATE_MAX_SIGN_UP=10 / …=3600000、RATE_MAX_TOKEN_VERIFY=20、RATE_MAX_PORTAL_QUERY=300/min、RATE_MAX_FRONT_LOG=120/min | 否 | 见左 | IP 桶限流阈值,0 关闭(packages/server-kit/src/http-security.ts) |
RATE_MAX_AI_BURST=20/min、QUOTA_MAX_AI_DAILY=200、RATE_MAX_CODEGEN_BURST=30/min、QUOTA_MAX_CODEGEN_DAILY=100、QUOTA_TZ_OFFSET_MINUTES=480 | 否 | 见左 | 用户桶配额(按 user.uuid 分桶,非 IP);480=东八区日界 |
RATE_LIMIT_STORE | 建议对外部署=mysql | memory | memory=进程内(重启清零;work-server 与 portal 两进程各算各的,daemon/cluster 启动时 work-server 多 worker 每 worker 各一份,标称阈值被放大 worker 倍,见 14 篇第 2 节);mysql=集中式 rate_limit_bucket 表(随 db:init 自建) |
DATA_QUERY_DENY_PRIVATE | 否 | 空=拒绝内网 | API 数据源出网策略;置 false 仅保留恒定项(启动打 warn,warnIfPrivateEgressAllowed) |
ALLOW_PAGE_METHODS_ON_PUBLIC | 否 | 空=拦截 | 页面方法公开闸门逃生阀;置 1 关闭"含方法体看板禁止公开"的两条拦截(明确接受风险才开) |
CSV_UPLOAD_DIR | 否 | ./uploads | CSV 上传暂存目录(router.ts koa-body formidable 配置) |
npm run db:init 必须先于新版代码上线——这是 README 开篇就点名的第一条硬约束,机理在 packages/init-database/src/sql/db_connection_info.sql(password 列注释:256 字符明文加密后约 387 字符,列宽已放宽到 1024)与 user_info.sql(password 列放宽到 255 以容纳 bcrypt 60 字符与存量 64 hex):老库若未跑新 DDL,新版代码写入即触发 MySQL 1406(data too long)。db:init 本身全部幂等:CREATE TABLE IF NOT EXISTS、ensureColumns 查 information_schema 缺列才 ALTER、种子 INSERT 幂等,重跑安全(index.ts 84-90 行注释)。
启动自检(fail-fast,均非 bug):
| 报错 | 原因与处理 |
|---|---|
[启动自检] JWT_SECRET 未配置…生产模式拒绝启动 | 配置后重启;开发模式降级为 warn |
[启动自检] 库中已有 N 行加密凭据,但 SECRET_BOX_KEY 缺失… | 修好密钥再启动,不要清库(GCM 认证失败会被误判成"对方库挂了",故必须在启动期暴露) |
PASSWORD_SALT is required | 迁移期校验旧口令需要盐,迁移完成前保留 |
根 package.json 的构建链是理解部署的关键(全部经 vite-node 执行 scripts/*.ts):
npm run build
├─ build:module scripts/build-module.ts rollup 出 @my/business + @my/shared 的 ESM/CJS
├─ build:pkg-types pnpm --filter shared/charts/server-kit run build 各包自有 tsc/vite
├─ build:dts scripts/build-dts.ts ts-morph 生成声明
└─ build:css scripts/build-css.ts 产出 business/css(⚠️ build:module 会清 dist,
两步必须成对跑,否则消费方解析不到 @my/business/css/index.css)
npm run build:work-front scripts/build-work-front.ts 一个包两个产物桶:
dist/admin/** = admin/page/app.js(编辑器+管理台)+ admin/page/preview-amd.js
dist/visitor/** = visitor/page/dashboard.js、home、demo 产物 + 各自 lib/(vue、vue-router global 产物,
build 末尾 copyVueRuntimeLib 从 work-front/node_modules 硬拷,两桶各一份)
npm run build:work-server scripts/build-work-server.ts work-server 打 CJS(index.cjs)并只拷 admin 桶(剥前缀)
npm run build:portal-server 同构,只拷 visitor 桶 → portal 公开目录永不含编辑器代码静态资源三层约定:packages/work-server/src/public/dist 是指向 work-front/dist 的符号链接(.gitignore 忽略,仅影响 8001);/public/cdn/<包名>/<版本>/ 路径自带版本号 → 一年 immutable;其余产物无内容 hash → 协商缓存 + 显式 304(两个服务端 index.ts 同策略,portal 注释记载了踩坑原因:"改了 footer 源文件仍发旧产物")。createSyncCDNMirror 在本地 404 时回源 mock CDN 并落盘。
升级发布的固定动作(docs/plans/deploy-ops-2026-09.md 第八节):git pull → pnpm i(lockfile 变更时)→ npm run db:init → npm run build(共享包改动必须完整跑)→ start:work + start:portal(或 daemon 重启)。改 @my/shared/@my/charts/@my/business 后必须重新构建对应包——两个服务端经 node_modules 软链消费其 dist 产物(README"共享包与构建顺序"节)。
已知单实例约束(deploy-ops 第七节):daemon 启动是单机多服务而非集群;横向扩容前需解决访客 PV 去重的进程内存态与构建产物落盘(共享盘或对象存储);量级上来后限流建议上移网关层(nginx limit_req)。
test.projects 把用例按是否需要 DOM 分两线,而非全局 jsdom——文件头注释给出量化依据:jsdom 环境初始化约 10s/文件,全局 jsdom 时整批 environment 累计 666-885s,撞上 vitest 硬编码的 START_TIMEOUT=60s/WORKER_START_TIMEOUT=90s 导致整批 worker 超时假红;分线后同一批用例 environment 累计 666s → 16ms。
| project | 环境 | 覆盖包 |
|---|---|---|
server | node | work-server、server-kit、portal-server |
web | jsdom | work-front、charts、shared、dashboard-codegen |
maxWorkers: 2 写进每个 project 配置(不靠命令行传参):默认并发让机器饱和,两种症状——worker 启动超时、http.test.ts 起真服务的 beforeAll 报 Hook timed out 假红。新增包必须显式归入某一侧:漏掉的包不会被任何 project 收进去,等于静默不跑。
写库用例统一形如 const dbDescribe = process.env.DB_AVAILABLE === 'true' ? describe : describe.skip;DB_AVAILABLE 由 packages/work-server/__tests__/setup.ts 探活写入。库连不上时用例静默跳过且退出码 0(README 明言这曾让 CI 假绿)。因此 CI 用 services: mysql:8.4 起库 + db:init + REQUIRE_DB=true(探活失败直接抛错)。用例夹具必须自删:服务层删除是软删(status=0),行永远在库里;新用例要在 dbDescribe 块内加 afterAll 调 pruneTestData(packages/work-server/__tests__/test-data.ts)按文件专属前缀硬删,并顺着 uuid 清三阶段表/快照/访问日志——README 记载了教训(曾堆到 534 看板/824 数据源/98 页面)。
npm run test # 全量两线
pnpm exec vitest --run --project server # 只跑服务端线(秒级反馈)
pnpm exec vitest --run --project web # 只跑前端线
npm run coverage # v8 覆盖率(分母 packages/*/src/**)
npm run test:bench # 基准测试
npm run typecheck # web(vue-tsc) + node(tsc) + 两份 __tests__ 全量 0 error 才算过
npm run lint # eslint(CI 硬门禁:卡 error 不卡 warning).github/workflows/ci.yml 五道门禁顺序:Install(--frozen-lockfile --ignore-scripts)→ Lint → Build workspace packages → Init database(写角色/权限种子,RBAC 用例依赖)→ Test(--maxWorkers=2)→ Typecheck。CI 环境变量含独立库名 vue3_course_ci、占位密钥(非生产值)、REQUIRE_DB=true。README 补充了把 __tests__ 纳入类型检查的理由:"曾给 pruneTestData 接线时漏 import,vue-tsc/tsc 全绿、vitest 一跑才炸"。
README 安全基线末尾点名了两套策略的守护用例,动相关代码前先读:packages/work-server/__tests__/http.test.ts(起真服务验证安全响应头,含 404 路径)、packages/portal-server/__tests__/server-headers.test.ts(守"门户必须能被嵌入")、packages/work-server/__tests__/page-methods-guard.test.ts(公开闸门纯函数矩阵+真库端到端,"分别破坏两条闸门时变红用例集合互不重叠")、packages/work-server/__tests__/net-guard.test.ts(SSRF 判定矩阵,含不误伤公网域名的反例)、api-query.test.ts({{host}} 被拒、302 被拒且不重试)、packages/portal-server/__tests__/asset-name-guard.test.ts(SAFE_ASSET_NAME 白名单)。