feat: 生产化改造(服务端安装程序/PWA/Docker+HTTPS)并修复侧边栏点击无反应、SPA 静态托管 dotfiles 两个缺陷
- 修复 web-admin 与 desktop-client 侧边栏点击无反应:el-menu 的 index 是路径,不能当路由名使用 - 修复 Web 产物目录含点号目录(如 .cache)时 res.sendFile 因 dotfiles:ignore 返回 500 - 后端生产模式:静态托管 Web 端 + SPA 回退、helmet、限流(通用/登录)、trust proxy、 /healthz 与 /readyz、请求耗时日志、生产环境变量强校验、首启动自动迁移与创建管理员、PID 文件 - Web 管理端 PWA:manifest + service worker + 图标,可在浏览器安装为应用 - 新增服务端 + Web 管理端 Windows 安装程序(内置 Node 运行时、启停与服务脚本、开机自启、卸载保留数据) - 新增 Docker/Compose/Nginx 部署样例与 docs/production.md - 新增 UI 回归测试:Web 端 Electron 驱动真实点击 7 项、客户端冒烟 11 项(含侧边栏点击与离线回退)
This commit is contained in:
1 parent
6e4c7c1050
commit
5545ef6f55
46 files changed
+3020
-152
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# 班级小助手 · Docker Compose 环境变量
|
||||
# 复制为 deploy/.env 后填写:cp deploy/.env.example deploy/.env
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 服务端对外端口(宿主机)
|
||||
SERVER_PORT=4000
|
||||
|
||||
# 应用版本(镜像标签)
|
||||
APP_VERSION=0.1.0
|
||||
|
||||
# MySQL 配置
|
||||
MYSQL_ROOT_PASSWORD=请改成强密码_root
|
||||
MYSQL_DATABASE=classhelper
|
||||
MYSQL_USER=classhelper
|
||||
MYSQL_PASSWORD=请改成强密码_app
|
||||
|
||||
# JWT 签名密钥(必填,必须 ≥32 位;生成:
|
||||
# docker run --rm node:22-slim node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
||||
# 或本地:node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
||||
JWT_SECRET=
|
||||
|
||||
# 令牌有效期
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 允许的跨域来源;使用域名访问时建议写成具体来源,例如 https://class.example.com
|
||||
CORS_ORIGIN=*
|
||||
|
||||
# 首次启动创建的管理员账号(部署完成后请立即登录修改密码)
|
||||
INITIAL_ADMIN_USERNAME=admin
|
||||
INITIAL_ADMIN_PASSWORD=admin123
|
||||
|
||||
# 第 1 教学周的周一(用于"当前周次")
|
||||
TERM_START_DATE=2026-08-31
|
||||
|
||||
# 日志级别:debug | info | warn | error
|
||||
LOG_LEVEL=info
|
||||
@@ -0,0 +1,73 @@
|
||||
# 班级小助手服务端 · 生产镜像
|
||||
#
|
||||
# 两阶段构建:
|
||||
# build —— 安装依赖并编译 shared / server / web-admin
|
||||
# runtime —— 仅保留运行时所需内容(生产依赖 + 构建产物),镜像更小
|
||||
#
|
||||
# 构建(在仓库根目录执行):
|
||||
# docker build -f deploy/Dockerfile -t classhelper-server:0.1.0 .
|
||||
#
|
||||
# 运行(SQLite + 数据卷):
|
||||
# docker run -d --name classhelper -p 4000:4000 \
|
||||
# -e JWT_SECRET=<48字节随机串> \
|
||||
# -v classhelper-data:/app/data \
|
||||
# classhelper-server:0.1.0
|
||||
#
|
||||
# 生产推荐用 docker compose(见 deploy/docker-compose.yml,含 MySQL 与 HTTPS 反代)
|
||||
|
||||
# ---------------------------------------------------------------- 构建阶段
|
||||
FROM node:22-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
# 启用 pnpm(用 corepack 保证版本与仓库一致)
|
||||
RUN corepack enable && corepack prepare pnpm@11.8.0 --activate
|
||||
|
||||
# 先只拷贝清单以提高缓存命中率
|
||||
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
COPY packages/server/package.json packages/server/
|
||||
COPY packages/web-admin/package.json packages/web-admin/
|
||||
COPY packages/desktop-client/package.json packages/desktop-client/
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
# 再拷贝源码并构建(只构建服务端与 Web 管理端,桌面客户端不参与)
|
||||
COPY packages ./packages
|
||||
COPY scripts ./scripts
|
||||
RUN pnpm build:shared \
|
||||
&& pnpm --filter @classhelper/server build \
|
||||
&& pnpm --filter @classhelper/web-admin build
|
||||
|
||||
# ---------------------------------------------------------------- 运行时阶段
|
||||
FROM node:22-slim AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=4000 \
|
||||
LOG_LEVEL=info \
|
||||
TRUST_PROXY=1 \
|
||||
RATE_LIMIT_ENABLED=true \
|
||||
STARTUP_DB_CHECK=true
|
||||
|
||||
# 生产依赖(清单由 scripts/dist-server.mjs 依据 packages/server/package.json 生成)
|
||||
COPY deploy/package.runtime.json ./package.json
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
|
||||
# 后端产物 + 迁移文件 + Web 管理端 + 工作区共享包
|
||||
COPY --from=build /app/packages/server/dist ./server/dist
|
||||
COPY --from=build /app/packages/server/prisma/migrations ./server/prisma/migrations
|
||||
COPY --from=build /app/packages/web-admin/dist ./web
|
||||
COPY --from=build /app/packages/shared/dist ./node_modules/@classhelper/shared/dist
|
||||
COPY packages/shared/package.json ./node_modules/@classhelper/shared/package.json
|
||||
|
||||
# 数据目录(SQLite 文件 / PID 文件),建议挂载数据卷
|
||||
RUN mkdir -p /app/data /app/logs
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
# 容器健康检查:就绪探针(数据库不可用会返回 503)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=25s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:4000/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
|
||||
CMD ["node", "server/dist/index.js"]
|
||||
@@ -0,0 +1,93 @@
|
||||
# 班级小助手 · 生产部署(MySQL + 服务端 + 可选 HTTPS 反代)
|
||||
#
|
||||
# 使用:
|
||||
# 1) 复制环境变量模板并填写密钥:cp deploy/.env.example deploy/.env
|
||||
# 2) 构建并启动:docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build
|
||||
# 3) 访问:http://<服务器IP>:4000/
|
||||
#
|
||||
# 说明:数据库迁移由一次性的 migrate 服务执行(prisma migrate deploy),
|
||||
# 服务端容器因此无需内置 Prisma CLI。
|
||||
|
||||
name: classhelper
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------- 数据库
|
||||
db:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --default-time-zone=+08:00
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请在 deploy/.env 中设置}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-classhelper}
|
||||
MYSQL_USER: ${MYSQL_USER:-classhelper}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?请在 deploy/.env 中设置}
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ['CMD', 'mysqladmin', 'ping', '-h', '127.0.0.1', '-p${MYSQL_ROOT_PASSWORD}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 40s
|
||||
networks: [classhelper]
|
||||
|
||||
# ---------------------------------------------------------------- 数据库迁移(一次性)
|
||||
migrate:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
target: build
|
||||
restart: 'no'
|
||||
working_dir: /app/packages/server
|
||||
environment:
|
||||
DATABASE_PROVIDER: mysql
|
||||
DATABASE_URL: mysql://${MYSQL_USER:-classhelper}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE:-classhelper}
|
||||
# MySQL 需要 mariadb 驱动适配器(切换数据库时按 docs/mysql.md 安装)
|
||||
NODE_ENV: production
|
||||
command: >
|
||||
sh -c "pnpm --filter @classhelper/server add @prisma/adapter-mariadb &&
|
||||
npx prisma migrate deploy"
|
||||
depends_on:
|
||||
db: { condition: service_healthy }
|
||||
networks: [classhelper]
|
||||
|
||||
# ---------------------------------------------------------------- 服务端 + Web 管理端
|
||||
server:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
target: runtime
|
||||
image: classhelper-server:${APP_VERSION:-0.1.0}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 4000
|
||||
DATABASE_PROVIDER: mysql
|
||||
DATABASE_URL: mysql://${MYSQL_USER:-classhelper}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE:-classhelper}
|
||||
JWT_SECRET: ${JWT_SECRET:?请在 deploy/.env 中设置(48 字节随机串)}
|
||||
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
|
||||
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
||||
TRUST_PROXY: '1'
|
||||
RATE_LIMIT_ENABLED: 'true'
|
||||
STARTUP_DB_CHECK: 'true'
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
TERM_START_DATE: ${TERM_START_DATE:-}
|
||||
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
|
||||
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-admin123}
|
||||
ports:
|
||||
- '${SERVER_PORT:-4000}:4000'
|
||||
depends_on:
|
||||
db: { condition: service_healthy }
|
||||
migrate: { condition: service_completed_successfully }
|
||||
networks: [classhelper]
|
||||
|
||||
networks:
|
||||
classhelper:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
@@ -0,0 +1,106 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# 班级小助手 · Nginx 反向代理样例(HTTPS + WebSocket)
|
||||
#
|
||||
# 作用:把 https://class.example.com 反向代理到本机 4000 端口的服务端,
|
||||
# 同时处理 WebSocket 升级、真实 IP 透传、静态资源缓存与安全响应头。
|
||||
#
|
||||
# 使用:
|
||||
# 1) 把本文件放到 /etc/nginx/conf.d/classhelper.conf(或 sites-available 并做软链)
|
||||
# 2) 修改 server_name 与证书路径
|
||||
# 3) nginx -t && systemctl reload nginx
|
||||
# 4) 服务端 .env 中设置 TRUST_PROXY=1(否则限流按代理 IP 统计)
|
||||
# 以及 CORS_ORIGIN=https://class.example.com
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 上游:本机运行的服务端(Windows 安装包 / Docker / 手动启动均适用)
|
||||
upstream classhelper_backend {
|
||||
server 127.0.0.1:4000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP -> HTTPS 跳转
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name class.example.com;
|
||||
|
||||
# Let's Encrypt 校验目录(如使用 certbot --webroot)
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name class.example.com;
|
||||
|
||||
# ---------------------------------------------------------------- 证书
|
||||
ssl_certificate /etc/nginx/certs/class.example.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/class.example.com.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
|
||||
# ---------------------------------------------------------------- 基础
|
||||
client_max_body_size 8m;
|
||||
charset utf-8;
|
||||
access_log /var/log/nginx/classhelper.access.log;
|
||||
error_log /var/log/nginx/classhelper.error.log warn;
|
||||
|
||||
# 安全响应头(应用层已用 helmet 设置,这里作为兜底)
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
|
||||
|
||||
# gzip(静态资源与接口响应)
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/json application/javascript application/manifest+json image/svg+xml;
|
||||
|
||||
# ---------------------------------------------------------------- WebSocket(Socket.IO)
|
||||
location /socket.io/ {
|
||||
proxy_pass http://classhelper_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 静态资源(带哈希,长缓存)
|
||||
location /assets/ {
|
||||
proxy_pass http://classhelper_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 接口与页面
|
||||
location / {
|
||||
proxy_pass http://classhelper_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_read_timeout 120s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "classhelper-server-runtime",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "班级小助手服务端运行时(安装包 / 容器内置)",
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@prisma/adapter-libsql": "7.10.0",
|
||||
"@prisma/client": "7.10.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"compression": "^1.8.2",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.7.0",
|
||||
"helmet": "^8.3.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"socket.io": "^4.8.3",
|
||||
"zod": "^4.6.2"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user