From 70bf1907b53ad78cc116e4c10504dd262eebb329 Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Tue, 15 Sep 2026 13:43:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=8F=8C=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 15 ++ .env | 7 + .env.development | 3 + .env.production | 3 + .env.public | 28 +++ Dockerfiles/Dockerfile.admin | 23 ++ Dockerfiles/Dockerfile.public | 23 ++ Dockerfiles/admin.conf | 34 +++ Dockerfiles/api-public-callback.conf.example | 22 ++ Dockerfiles/edge-domains.conf.example | 82 +++++++ Dockerfiles/public.conf | 45 ++++ Dockerfiles/双前端域名部署.md | 117 ++++++++++ build/plugins.ts | 17 +- index.html | 5 +- package.json | 3 + src/PublicApp.vue | 13 ++ src/api/modules/biz/onboarding.ts | 44 ---- src/api/modules/open/onboarding.ts | 91 ++++++++ src/api/request/publicRequest.ts | 216 +++++++++++++++++++ src/config/index.ts | 2 +- src/public-main.ts | 60 ++++++ src/routers/modules/dynamicRouter.ts | 5 +- src/routers/modules/staticRouter.ts | 6 - src/routers/public.ts | 55 +++++ src/typings/global.d.ts | 4 + src/views/biz/onboarding/task/index.vue | 14 +- src/views/onboarding/fill/index.vue | 216 +++++++++++++++---- src/views/onboarding/public-status/index.vue | 68 ++++++ 28 files changed, 1125 insertions(+), 96 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.public create mode 100644 Dockerfiles/Dockerfile.admin create mode 100644 Dockerfiles/Dockerfile.public create mode 100644 Dockerfiles/admin.conf create mode 100644 Dockerfiles/api-public-callback.conf.example create mode 100644 Dockerfiles/edge-domains.conf.example create mode 100644 Dockerfiles/public.conf create mode 100644 Dockerfiles/双前端域名部署.md create mode 100644 src/PublicApp.vue create mode 100644 src/api/modules/open/onboarding.ts create mode 100644 src/api/request/publicRequest.ts create mode 100644 src/public-main.ts create mode 100644 src/routers/public.ts create mode 100644 src/views/onboarding/public-status/index.vue diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8de817f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.idea +.vscode +node_modules +dist +dist-* +coverage +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +stats.html diff --git a/.env b/.env index a78f9e6..3b49cfb 100644 --- a/.env +++ b/.env @@ -1,6 +1,13 @@ # title VITE_GLOB_APP_TITLE = 员工管理系统 +# 前端构建类型与入口。默认保持完整后台版本。 +VITE_APP_VARIANT = admin +VITE_APP_ENTRY = /src/main.ts +VITE_APP_FAVICON = /api/sys/ico +# 员工公开入职前端地址;各模式可在对应 .env 文件中覆盖。 +VITE_PUBLIC_ONBOARDING_URL = http://localhost:8849 + # 本地运行端口号 VITE_PORT = 8848 diff --git a/.env.development b/.env.development index 005b91e..25bdd5b 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,9 @@ # 本地环境 VITE_USER_NODE_ENV = development +# 本地后台生成的员工链接固定指向独立公开前端开发端口 +VITE_PUBLIC_ONBOARDING_URL = http://localhost:8849 + # 公共基础路径 VITE_PUBLIC_PATH = / diff --git a/.env.production b/.env.production index c87dc05..e1d0b8c 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,9 @@ # 线上环境 VITE_USER_NODE_ENV = production +# 后台生成员工入职链接时使用的固定公开域名 +VITE_PUBLIC_ONBOARDING_URL = https://onboardx.cnzhyx.cn + # 公共基础路径 VITE_PUBLIC_PATH = / diff --git a/.env.public b/.env.public new file mode 100644 index 0000000..a81fa93 --- /dev/null +++ b/.env.public @@ -0,0 +1,28 @@ +# 员工公开入职前端 +VITE_USER_NODE_ENV = production +VITE_APP_VARIANT = public +VITE_APP_ENTRY = /src/public-main.ts +VITE_APP_FAVICON = /logo.png +VITE_GLOB_APP_TITLE = 入职服务 + +VITE_PORT = 8849 +VITE_OPEN = true +VITE_DEVTOOLS = false +VITE_REPORT = false +VITE_CODEINSPECTOR = false + +VITE_PUBLIC_PATH = / +VITE_ROUTER_MODE = hash +VITE_BUILD_COMPRESS = none +VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false +VITE_DROP_CONSOLE = true + +# 生成自销毁 Service Worker,用来清除该域名历史遗留的后台 PWA 缓存。 +VITE_PWA = true + +VITE_HTTP_PROXY = true +VITE_API_URL = http://127.0.0.1:5566 +VITE_PROXY = [["/api","http://127.0.0.1:5566"]] + +VITE_MQTT = false +VITE_PUBLIC_ONBOARDING_URL = http://localhost:8849 diff --git a/Dockerfiles/Dockerfile.admin b/Dockerfiles/Dockerfile.admin new file mode 100644 index 0000000..c06d02f --- /dev/null +++ b/Dockerfiles/Dockerfile.admin @@ -0,0 +1,23 @@ +# 完整后台前端 +FROM registry-vpc.cn-chengdu.aliyuncs.com/zhixueli-prod/node:18.20.3 AS builder + +WORKDIR /app +RUN npm config set registry https://registry.npmmirror.com/ +RUN npm config set fetch-retries 3 +RUN npm config set fetch-retry-mintimeout 5000 +RUN npm config set fetch-retry-maxtimeout 60000 +RUN npm install -g pnpm@10.16.1 + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm config set registry https://registry.npmmirror.com/ +RUN pnpm install --frozen-lockfile --fetch-timeout 300000 + +COPY . . +RUN npm run build:admin + +FROM registry-vpc.cn-chengdu.aliyuncs.com/zhixueli-prod/nginx:alpine +COPY --from=builder --chown=nginx:nginx /app/dist-admin /usr/share/nginx/html +COPY Dockerfiles/admin.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfiles/Dockerfile.public b/Dockerfiles/Dockerfile.public new file mode 100644 index 0000000..c8a1dfc --- /dev/null +++ b/Dockerfiles/Dockerfile.public @@ -0,0 +1,23 @@ +# 纯员工入职前端 +FROM registry-vpc.cn-chengdu.aliyuncs.com/zhixueli-prod/node:18.20.3 AS builder + +WORKDIR /app +RUN npm config set registry https://registry.npmmirror.com/ +RUN npm config set fetch-retries 3 +RUN npm config set fetch-retry-mintimeout 5000 +RUN npm config set fetch-retry-maxtimeout 60000 +RUN npm install -g pnpm@10.16.1 + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm config set registry https://registry.npmmirror.com/ +RUN pnpm install --frozen-lockfile --fetch-timeout 300000 + +COPY . . +RUN npm run build:public + +FROM registry-vpc.cn-chengdu.aliyuncs.com/zhixueli-prod/nginx:alpine +COPY --from=builder --chown=nginx:nginx /app/dist-public /usr/share/nginx/html +COPY Dockerfiles/public.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfiles/admin.conf b/Dockerfiles/admin.conf new file mode 100644 index 0000000..fb38486 --- /dev/null +++ b/Dockerfiles/admin.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + + client_max_body_size 100m; + root /usr/share/nginx/html; + index index.html; + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri =404; + } + + location = /sw.js { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://onboardx-api-svc:5566/; + 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_connect_timeout 60s; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} diff --git a/Dockerfiles/api-public-callback.conf.example b/Dockerfiles/api-public-callback.conf.example new file mode 100644 index 0000000..52cc282 --- /dev/null +++ b/Dockerfiles/api-public-callback.conf.example @@ -0,0 +1,22 @@ +# onboardx-api.cnzhyx.cn 的外层 Nginx 示例。 +# 只暴露契约锁回调;后台和员工接口分别通过两个前端域名反向代理。 +server { + listen 80; + server_name onboardx-api.cnzhyx.cn; + + location = /api/open/onboarding/qiyuesuo/callback { + proxy_pass http://onboardx-api-svc:5566/open/onboarding/qiyuesuo/callback; + 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_connect_timeout 60s; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + location / { + return 404; + } +} diff --git a/Dockerfiles/edge-domains.conf.example b/Dockerfiles/edge-domains.conf.example new file mode 100644 index 0000000..24cafc0 --- /dev/null +++ b/Dockerfiles/edge-domains.conf.example @@ -0,0 +1,82 @@ +# 服务器最外层 Nginx 示例。 +# 使用前请确认三个本机端口及证书路径与实际环境一致。 +upstream onboardx_admin_web { + server 127.0.0.1:18081; + keepalive 16; +} + +upstream onboardx_public_web { + server 127.0.0.1:18082; + keepalive 16; +} + +upstream onboardx_backend { + server 127.0.0.1:15566; + keepalive 16; +} + +server { + listen 80; + server_name onboardx.cnzhyx.cn onboardx-admin.cnzhyx.cn onboardx-api.cnzhyx.cn; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name onboardx-admin.cnzhyx.cn; + + ssl_certificate /etc/nginx/certs/cnzhyx.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/cnzhyx.cn/privkey.pem; + + location / { + proxy_pass http://onboardx_admin_web; + 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; + } +} + +server { + listen 443 ssl http2; + server_name onboardx.cnzhyx.cn; + + ssl_certificate /etc/nginx/certs/cnzhyx.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/cnzhyx.cn/privkey.pem; + + location / { + proxy_pass http://onboardx_public_web; + 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; + } +} + +server { + listen 443 ssl http2; + server_name onboardx-api.cnzhyx.cn; + + ssl_certificate /etc/nginx/certs/cnzhyx.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/cnzhyx.cn/privkey.pem; + + client_max_body_size 10m; + + location = /api/open/onboarding/qiyuesuo/callback { + proxy_pass http://onboardx_backend/open/onboarding/qiyuesuo/callback; + 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_connect_timeout 60s; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + location / { + return 404; + } +} diff --git a/Dockerfiles/public.conf b/Dockerfiles/public.conf new file mode 100644 index 0000000..e07c21c --- /dev/null +++ b/Dockerfiles/public.conf @@ -0,0 +1,45 @@ +server { + listen 80; + server_name _; + + client_max_body_size 100m; + root /usr/share/nginx/html; + index index.html; + + add_header X-Robots-Tag "noindex, nofollow" always; + + # 公开构建中的 sw.js 只负责注销该域名历史遗留的后台 PWA。 + location = /sw.js { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri =404; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri =404; + } + + # 员工域名只允许入职公开接口。内部后端路由不包含外层 /api。 + location ^~ /api/open/onboarding/ { + proxy_pass http://onboardx-api-svc:5566/open/onboarding/; + proxy_http_version 1.1; + proxy_request_buffering off; + 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_connect_timeout 60s; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # 公开域名拒绝系统、后台业务及其他 API。 + location ^~ /api/ { + return 404; + } + + # Hash 路由的有效链接在服务器看来也是根路径,必须返回公开版 index.html。 + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/Dockerfiles/双前端域名部署.md b/Dockerfiles/双前端域名部署.md new file mode 100644 index 0000000..5e9d173 --- /dev/null +++ b/Dockerfiles/双前端域名部署.md @@ -0,0 +1,117 @@ +# OnboardX 双前端域名部署说明 + +## 1. 最终访问结构 + +- 员工入职:https://onboardx.cnzhyx.cn +- 后台管理:https://onboardx-admin.cnzhyx.cn +- 契约锁回调:https://onboardx-api.cnzhyx.cn/api/open/onboarding/qiyuesuo/callback +- 后端仍然只有一套,不需要复制数据库或部署第二套 API。 + +两个前端是同一份源码的不同构建结果: + +- build:admin 生成完整后台,输出到 dist-admin。 +- build:public 只生成员工填报、链接失效和 404 页面,输出到 dist-public。 + +员工域名的容器只转发 /api/open/onboarding/。任何 /api/sys/、/api/biz/ 或其他 API 都会直接返回 404。 + +## 2. 本次涉及的配置文件 + +- Dockerfile.admin:构建、运行完整后台前端。 +- Dockerfile.public:构建、运行纯员工入职前端。 +- admin.conf:后台前端容器内部 Nginx。 +- public.conf:员工前端容器内部 Nginx,并限制公开 API。 +- edge-domains.conf.example:服务器最外层三个 HTTPS 域名的参考配置。 +- api-public-callback.conf.example:只单独配置 API 回调域名时的精简参考。 + +## 3. 上线前准备 + +### 3.1 DNS + +在域名服务商处确保以下记录都指向同一台服务器公网 IP: + + onboardx.cnzhyx.cn + onboardx-admin.cnzhyx.cn + onboardx-api.cnzhyx.cn + +### 3.2 HTTPS 证书 + +证书必须覆盖新域名 onboardx-admin.cnzhyx.cn。如果已有 *.cnzhyx.cn 通配符证书,可以继续使用。 + +### 3.3 后端容器服务名 + +两个前端容器内部配置默认访问: + + http://onboardx-api-svc:5566 + +因此两个前端容器必须与后端处于同一个 Docker 网络,并且后端在该网络中的名称或别名为 onboardx-api-svc。如果线上服务名不同,只修改 admin.conf 和 public.conf 中的 upstream 地址,不要改公开接口范围。 + +## 4. 构建两个前端镜像 + +在前端项目根目录执行: + + docker build -f Dockerfiles/Dockerfile.admin -t onboardx-admin-web:20260915 . + docker build -f Dockerfiles/Dockerfile.public -t onboardx-public-web:20260915 . + +VITE_PUBLIC_ONBOARDING_URL 是构建期配置,生产环境已经设置为: + + https://onboardx.cnzhyx.cn + +以后如果更换员工域名,修改 .env.production 后必须重新构建后台镜像,不能只改 Nginx。 + +## 5. 启动两个前端容器 + +下面仅作为普通 Docker 示例;如果使用宝塔、Portainer、Kubernetes 或云平台,就在对应界面填写相同的镜像、端口和网络关系。 + + docker run -d --name onboardx-admin-web --restart always --network onboardx-net -p 127.0.0.1:18081:80 onboardx-admin-web:20260915 + docker run -d --name onboardx-public-web --restart always --network onboardx-net -p 127.0.0.1:18082:80 onboardx-public-web:20260915 + +如果现有后端不在 onboardx-net,应让三个容器加入同一个已有网络。不要为了省事把后端所有端口直接暴露到公网。 + +## 6. 配置服务器最外层 Nginx + +1. 复制 edge-domains.conf.example 到服务器 Nginx 的站点配置目录。 +2. 把证书路径改为服务器上的真实路径。 +3. 确认端口: + - 18081 是后台前端。 + - 18082 是员工前端。 + - 15566 是后端映射到服务器本机的端口;如果后端没有映射该端口,应改为实际地址。 +4. 执行 nginx -t 检查配置。 +5. 检查通过后执行 nginx -s reload 平滑重载。 + +服务器最外层 Nginx 只负责域名、HTTPS 和转发。真正的公开 API 白名单在员工前端容器的 public.conf 内再次限制,形成两层保护。 + +## 7. 推荐发布顺序 + +1. 先完成 onboardx-admin.cnzhyx.cn 的 DNS 和证书。 +2. 启动后台前端容器,确认新后台域名登录及业务功能正常。 +3. 启动员工前端容器,但先不要发送新链接。 +4. 将原来的 onboardx.cnzhyx.cn 切换到员工前端容器。 +5. 打开一次员工域名,确认显示“入职链接无效”。 +6. 从后台复制一条有效链接,用电脑和安卓手机各走一遍。 +7. 最后确认契约锁回调仍然成功。 + +员工域名以前部署过后台 PWA。本次公开构建会发布一个自销毁 Service Worker,自动注销旧 PWA 并清理旧缓存。老手机第一次打开时可能自动刷新一次;若仍看到旧页面,关闭该页面后重新打开即可。 + +## 8. 安卓下载处理 + +- 普通桌面和 iOS 浏览器通过不携带后台凭证的文件流下载。 +- 安卓及安卓 WebView 直接访问同域公开文件接口,避免 blob: 下载被系统 WebView 拦截。 +- 安卓微信如果不支持文件接管,页面会提示用户点击右上角“在浏览器打开”后重新下载。 +- 后台 PWA 的 Service Worker 不再拦截 /api/,所以文件接口不会错误回退成 index.html。 + +## 9. 上线验收清单 + +- 访问 https://onboardx.cnzhyx.cn/,只显示“入职链接无效”。 +- 访问 /#/onboarding/fill 但不带 Token,自动进入链接无效页。 +- 修改 Token 任意一个字符,提示链接不存在或无效。 +- 公开域名访问 /api/sys/ 返回 404。 +- 公开域名访问 /api/biz/ 返回 404。 +- 公开域名不能出现后台登录页、菜单、员工档案或系统管理页面。 +- 后台生成的员工链接始终以 https://onboardx.cnzhyx.cn 开头。 +- onboardx-admin.cnzhyx.cn 的登录、上传、下载和业务操作正常。 +- 安卓 Chrome、安卓微信、桌面浏览器均测试合同下载。 +- 契约锁回调地址保持不变且能正常更新签署状态。 + +## 10. 回滚原则 + +后台前端和员工前端可以分别回滚到各自上一个镜像版本。不要把员工域名重新指向完整后台作为临时回滚方案,否则会重新暴露后台入口。若员工端出现故障,应只回滚 onboardx-public-web 镜像。 diff --git a/build/plugins.ts b/build/plugins.ts index afa7d89..3d973d9 100644 --- a/build/plugins.ts +++ b/build/plugins.ts @@ -129,12 +129,25 @@ const createCompression = (viteEnv: ViteEnv): PluginOption | PluginOption[] => { * @param viteEnv */ const createVitePwa = (viteEnv: ViteEnv): PluginOption | PluginOption[] => { - const { VITE_GLOB_APP_TITLE } = viteEnv; + const { VITE_GLOB_APP_TITLE, VITE_APP_VARIANT } = viteEnv; + + // 员工公开域名曾部署过完整后台 PWA。发布自销毁 SW 可主动注销旧注册并清理旧缓存, + // 后续公开页面不再由 Service Worker 接管。 + if (VITE_APP_VARIANT === "public") { + return VitePWA({ + registerType: "autoUpdate", + manifest: false, + selfDestroying: true + }); + } + return VitePWA({ registerType: "autoUpdate", workbox: { // 添加此项配置,增加需要缓存的最大文件大小 - maximumFileSizeToCacheInBytes: 4 * 1024 * 1024 + maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, + // API、下载和回调均不能回退到 SPA 的 index.html。 + navigateFallbackDenylist: [/^\/api\//] }, manifest: { name: VITE_GLOB_APP_TITLE, diff --git a/index.html b/index.html index 5aea4eb..e56d367 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,8 @@
- + +