部署与调度指南
文档用途
- 描述当前生产环境真实的部署拓扑与调度模型
- 对齐时间:
2026-09-13 - 全新服务器部署先看
docs/guide/deploy/bt-panel.md(宝塔面板)或docs/guide/deploy/bare-metal.md(无面板、无容器的原生服务部署) - **容器化部署(Docker Compose / 1Panel / CI 自动打包)**先看
docs/guide/deploy/docker-1panel.md - 配套文档:
docs/README.md(文档入口)docs/develop/local-development.md(本地启动)docs/develop/architecture.md(总体架构)
1. 当前生产拓扑
项目跑在单台宝塔服务器上,结构如下:
宝塔站点 (Nginx)
├── 官网/用户入口 -> frontend-user-v3-www/dist 静态资源
├── 用户控制台 -> frontend-user-v4-console/dist 静态资源
├── 管理端站点 -> frontend-admin-v3/dist 静态资源
└── 后端 API 站点 -> backend/public (PHP-FPM)
│
├── MySQL 8 (本机或独立实例)
├── Redis (缓存)
├── 宝塔计划任务 每分钟执行一次 schedule:run
└── 宝塔守护进程 常驻 php artisan vnc:relay生产环境特征:
- 四端各自独立宝塔站点,浏览器从三个前端站点直接访问 API 域名;前端 Nginx 不反代 API
- 后端入口走 PHP-FPM,不使用
php artisan serve或app:serve - 没有常驻 Queue Worker,每分钟
schedule:run驱动三个入口:scheduler:heartbeat(唯一时钟源,按 15 分钟槽位去重派发定时任务)、queue:drain(后台子进程消费队列,按provision、业务组referral,notification,coupon,default、定时组automation三个队列组分别建 Worker,drain 锁保证同一队列不并发,空闲时自动退出)、scheduler:liveness(心跳存活探针,心跳异常时仍能告警) - VNC Relay 独立常驻:Relay 重启不会阻塞心跳、支付、新购或续费
- 新购、续费支付后同步履约:支付回调内直接调用上游开通/续费,用户付款后立即生效;同步失败才回退
provision队列,由每分钟心跳 Worker 重试,不再依赖队列轮询延迟
2. 计划任务配置
2.1 宝塔计划任务
在宝塔面板 → 计划任务 → 添加:
- 任务类型:Shell 脚本
- 执行周期:每分钟执行
- 脚本内容:
cd /你的项目/backend
php artisan schedule:run >> /dev/null 2>&1说明:
- 这条计划每分钟同时触发:心跳派发(15 分钟槽位去重)、队列消费(
queue:drain后台按队列组建 Worker)与存活探针;任一队列仍在消费时,drain 锁只跳过该队列的重复 Worker,其余队列组继续按分钟消费 - 不要再配置覆盖同一队列的常驻
queue:work,避免重复消费;需要低延迟时应按队列边界单独评估 - 任务执行超时强杀(
--timeout与JobTimedOut收敛)依赖 Linuxpcntl/SIGALRM,Windows 开发环境不生效;超时类问题必须在 Linux 环境验证,Windows 上长任务不会被自动终止 - 错过的 15 分钟心跳槽位默认不自动回放,需依赖任务自身幂等补偿或管理端手动触发
2.2 VNC Relay 守护
在宝塔进程守护或其他进程管理器中增加常驻命令:
cd /www/wwwroot/你的项目/backend
php artisan vnc:relay上线前确认 127.0.0.1:8100 已监听;Relay 异常时由守护进程自动重启。vnc:ensure-relay 仅保留为手工健康检查/故障修复命令,不再注册到心跳任务。
2.3 后端调度内容
当前调度能力以真实命令与路由注册为准。查看真实启用清单请看:
backend/routes/console.phpbackend/app/Console/Commands/- 管理端 "调度总览":
GET /api/v2/admin/schedules/overview
典型任务(以代码为准):
scheduler:heartbeat/queue:drain/scheduler:liveness:每分钟,调度与队列主干tickets:cleanup-unused-upstream-uploads:每分钟,删除超保留期未被工单回复引用的上游上传文件open-api:prune-usage-logs:每周,按open_api.usage_log_retention_days(默认 90 天)清理 API 用量日志- 自动续费检查、账单逾期处理、服务到期停用与终止、上游状态同步、各类日志清理:由心跳按槽位规则派发,清单见管理端「调度总览」
3. 部署流程
3.1 后端部署
# 项目根下切到 backend
cd backend
# 拉代码(宝塔一般自带 Git 钩子或手动 git pull)
git pull
# 安装依赖
composer install --no-dev --optimize-autoloader
# 数据库初始化/更新
# 新环境首次部署:使用 schema baseline
python scripts/install_db.py
# 后续增量更新:
php artisan migrate --force
# 清旧缓存
php artisan config:clear
php artisan route:clear
php artisan cache:clear
# 生产缓存
php artisan config:cache
php artisan route:cache数据库初始化说明:
database/schema/mysql-schema.sql是当前生产库完整结构快照,包含 62 张表和migrations记录- 历史迁移文件已归档至
database/migrations/_archive/,不再逐个执行- 增量迁移(
database/migrations/*.php)用于后续新功能上线install_db.py最后会经SettingsSeeder::seed()幂等种入:
- 系统核心默认配置(
settings表,含notification.email_enabled/sms_enabled/sms_template_code)- 通知模板默认数据(
notification_templates表):email 全量模板 + sms 验证码模板- schema baseline 仅导出表结构、不含模板数据,模板种子必须经该 Seeder 执行,切勿依赖迁移或手工补种
首次部署后请检查(管理后台 → 站点设置 → 通知总开关):
- 邮件通知 / 短信通知默认关闭,按需在后台打开
- 邮件/短信插件需在「插件管理」安装、配置并绑定,再前往「通知与接口」维护对应模板
3.2 前端部署
# 项目根目录:只安装一次 workspace 依赖(pnpm monorepo,四个 workspace)。
# --shamefully-hoist:多个 vite build(尤其 www 的 element-plus 深路径 import)
# 会从根 node_modules 解析依赖,必须扁平提升,否则构建报 load-fallback ENOENT。
# verify-deps-before-run=false:关闭 pnpm run 前的依赖检查,避免它重新 purge 掉 hoist 布局。
pnpm install --frozen-lockfile --shamefully-hoist
pnpm config set verify-deps-before-run false
# 每个前端构建时读取各自目录下的 .env 获取后端/前端地址,
# 构建三个完全独立的 dist,不复制任何文件到 backend/public。
pnpm run build:frontends
# 若在无 TTY 的 CI/脚本环境跑 build,需 CI=true(跳过 pnpm 的模块目录移除交互确认)。
CI=true pnpm run build:frontends产物目录分别是 frontend-user-v3-www/dist、frontend-user-v4-console/dist、frontend-admin-v3/dist,各自指向对应宝塔站点根目录。
VitePress 文档官网不属于上述三端,必须通过 pnpm run build:docs 独立构建和发布。内容维护、产物目录与发布检查见文档官网维护指南。
dev 与 build 的 env 分开:本地开发(
pnpm run dev)读取各前端目录下的.env.dev(dev 脚本用--mode dev);构建(pnpm run build:frontends)读取各前端目录下的.env。前端构建不依赖 backend 环境文件。每个前端目录(
frontend-admin-v3、frontend-user-v3-www、frontend-user-v4-console)需要两份 env(均 gitignore,不入库):
.env.dev:本地开发地址(127.0.0.1各端端口、本地 API).env:构建地址(生产域名),部署打包前填写真实域名部署前确认
.env中VITE_API_BASE_URL、VITE_PUBLIC_SITE_URL、VITE_CONSOLE_SITE_URL、VITE_SESSION_COOKIE_DOMAIN与生产一致;缺项或填错会让产物指向错误地址。站点地址的真源是
backend/.env的四个公开地址:backend/scripts/build_frontends.mjs会把它们解析成VITE_*注入构建,--print-env可单独打印这套变量供其他构建入口复用。这样各前端目录下的.env只是本地便利,不会与线上配置漂移。
构建配置来源的区别:
- 直接执行前端构建(
pnpm run build):各前端目录读取自身目录下的.env(或.env.production),仅用该目录内的配置。 - 通过
pnpm run build:frontends统一构建:由backend/scripts/build_frontends.mjs读取backend/.env中的四个公开地址,解析后作为VITE_*环境变量注入到各前端构建中。这样各前端目录下的.env只是本地便利,不会与线上配置漂移。
backend/scripts/build_frontends.mjs --print-env 可单独打印这套变量,供其他构建入口(如 deploy/deploy-www.sh 的 inject_backend_site_env)复用,保证所有构建入口读到同一份站点地址真源,避免与实际入口行为矛盾。
# 只预览构建目标,不实际构建
pnpm run build:frontends:dry
# 只打印从 backend/.env 解析出的 VITE_* 站点地址(供部署脚本复用)
node backend/scripts/build_frontends.mjs --print-env3.2.1 单域名子路径部署下的控制台地址
现网把官网、控制台、管理端、API 合并到同一个域名,用子路径区分:
| 端 | 访问路径 | 对应 backend/.env |
|---|---|---|
| 官网 | / | FRONTEND_URL=https://你的域名 |
| 控制台 | /console/ | CLIENT_CONSOLE_URL=https://你的域名/console |
| 管理端 | /admin/ | ADMIN_URL=https://你的域名/admin |
| API | /api | APP_URL=https://你的域名 |
单域名部署时 VITE_CONSOLE_SITE_URL 必须带 /console 子路径。控制台的 router base 就是 /console/,写成不带子路径的裸域名会让官网的「登录 / 免费注册」跳到 https://你的域名/client/login——那条路径落在官网 SPA 上,永远到不了控制台登录页。
为此 frontend-user-v3-www/vite.config.js 加了构建守卫:VITE_CONSOLE_SITE_URL 留空,或与 VITE_PUBLIC_SITE_URL 同源却没有子路径时,pnpm build 直接失败。控制台地址配错会在构建期暴露,不会带着坏链接发版。
单域名部署时 CLIENT_SESSION_COOKIE_DOMAIN 建议留空(登录态本来就在同源);需要跨子域共享登录态时才填父域。
CLIENT_CONSOLE_URL必须带/console:后端AuthService::normalizeConfiguredUrl()允许地址带路径(单域名部署的前提),管理员代登录链接就是在该地址后追加/client/login-as。如果把它写成不带子路径的裸域名,官网的登录/注册会跳到https://你的域名/client/login(官网 SPA),代登录也会落到官网页面上。 管理端会把后端下发的地址再拼一次/client/login-as,两边都按「base path 保留」处理。
3.3 .env 关键项
最小化需要核对的 .env:
APP_ENV=productionAPP_DEBUG=falseAPP_URL=https://api.你的域名(API、/media/*、/uploads/*、VNC WebSocket)FRONTEND_URL=https://www.你的域名(单域名子路径部署写https://你的域名,见 3.2.1)CLIENT_CONSOLE_URL=https://console.你的域名(单域名子路径部署写https://你的域名/console)ADMIN_URL=https://admin.你的域名(单域名子路径部署写https://你的域名/admin)CLIENT_SESSION_COOKIE_DOMAIN=.你的域名(官网和控制台需要跨子域共享登录态时设置;本地留空)SESSION_SECURE_COOKIE=true(HTTP 环境改为false)DB_*:MySQL 8 连接REDIS_*:Redis 连接CACHE_STORE=redisSESSION_DRIVER=file(当前生产口径)QUEUE_CONNECTION=database(当前生产口径)TURAIDC_PROVISION_QUEUES=provision(开通队列组,Worker 超时可达 1200 秒)TURAIDC_BUSINESS_QUEUES=referral,notification,coupon,default(业务队列组;若存量.env仍含provision,运行时会自动剔除,不会重复消费)TURAIDC_SCHEDULE_QUEUE=automation(定时任务队列组)
四个公开地址可统一使用 HTTP 或 HTTPS;HTTPS 前端不能指向 HTTP API。CORS 会从三个前端 URL 自动生成精确 allowlist,无需在前端 Nginx 增加代理。
Redis 当前用于缓存;不要在没有专项方案和验证的情况下把会话或队列切到 Redis。
3.4 受信代理与来源 IP 契约
后端在 backend/bootstrap/app.php 配置了 trustProxies:只信任回环与 RFC1918/ULA 私网段,且只采信 X-Forwarded-For、X-Forwarded-Port、X-Forwarded-Proto(不信任 X-Forwarded-Host)。request()->ip() 依赖该配置解析真实客户端 IP,直接影响:
- 审计字段(如
last_login_ip)与推荐风控的“注册 IP == 推荐人 IP”判断; /upload_image上游附件上传的启用开关、白名单/限流中间件(TicketUpstreamUploadThrottle)。接口默认关闭,管理端配置工单传递规则前必须先开启;白名单外上传默认拒绝。
部署必须满足以下契约,否则 IP 白名单与限流会被绕过或误判:
- 边缘反向代理是唯一公网入口:后端 API 端口(宝塔 PHP-FPM 站点 / 容器
8080)只能被受信反代访问,不能直接暴露给公网或不可信私网来源。 - 代理必须真实生成来源头:外部反代需携带
X-Forwarded-For、X-Forwarded-Proto。单层受信代理应把X-Forwarded-For重置为$remote_addr,不要用$proxy_add_x_forwarded_for原样追加——否则客户端可自带伪造 XFF 直接污染request()->ip()。只有明确存在多层受信代理且每层都清洗时才允许追加。 - 不得把公网代理地址加入
trustProxies:当前配置只信任回环与私网段;若实际入口代理不在这些网段(例如托管在公有云的 LB),必须先评估暴露面并收窄为明确的入口 IP/CIDR,再修改配置。 - 宝塔 PHP-FPM 直连形态(前端 Nginx 不反代 API)本身不产生 XFF;后端以 PHP-FPM 接收到的
REMOTE_ADDR为准,不受上述契约影响,但也不能依赖 XFF 取客户端 IP。
容器部署注意:
deploy/docker/.env.example默认把API_PORT=8080暴露到所有网卡。上线前应改为仅本机绑定(127.0.0.1:8080),由宿主机 Nginx/1Panel 反代进入;否则私网来源可直接访问 API 并伪造 XFF。
3.5 增量更新的实测坑
以下是现网增量更新(git pull + 重建 dist)时真实踩过的问题。
3.5.1 前端必须走 pnpm run build:frontends,不能逐个 pnpm run build
逐个构建读的是各前端目录自己的 .env / .env.production,而项目真源是 backend/.env 的四个公开地址(由 build_frontends.mjs 注入)。两处一旦不同步,产物里 canonical、og:url、结算回跳地址就会全部写成前端目录里的旧值。
实测过:官网 .env.production 是 idc.25y.cn,而 backend/.env 的 FRONTEND_URL 是 next2.25y.cn。逐个构建后产物里 idc.25y.cn 出现 4 处、next2.25y.cn 0 处—— 站点 HTTP 全 200,但 SEO 与回跳地址全是错的。改用标准脚本重建后归零。
部署后按真源核对,不要只看能不能访问:
grep -ro "$FRONTEND_HOST" frontend-user-v3-www/dist/ | wc -l # 应 > 0
grep -ro "<其他域名>" frontend-user-v3-www/dist/ | wc -l # 应为 03.5.2 生产机没有 pnpm,标准脚本会 ENOENT
build_frontends.mjs 内部 spawn pnpm。若生产 PATH 里只有 node 与 corepack (packageManager 指定了 pnpm 版本、corepack 缓存里也有,但没 enable 过), pnpm run build:frontends 会以 ENOENT: spawn pnpm 失败。
一次性修复:
corepack enable pnpm # 生成 /usr/bin/pnpm shim
pnpm -v # 确认版本与 packageManager 一致在 corepack enable 之前只能用 corepack pnpm run build 逐个构建,但那正是 3.5.1 说的 会绕过地址注入的方式,不要当成常态。
3.5.3 git pull 会被 untracked 文件阻塞
为紧急止血手工放进 backend/public/media/console-panel/ 的静态资产,与后续提交里的同名路径 冲突,pull 直接拒绝:
error: Untracked working tree files ... Please move or remove them before you merge.先比对内容再删工作区副本,让 git 接管(内容一致才可删):
sha1sum backend/public/media/console-panel/jquery.min.js
git show origin/main:backend/public/media/console-panel/jquery.min.js | sha1sum # 两行一致才继续
rm -rf backend/public/media/console-panel && git pull --ff-only origin main3.5.4 不要用 git checkout origin/main -- <file> 精确同步
那条命令只改工作区文件内容、不推进 HEAD。用它"热修"几个文件后,生产 HEAD 会长期停在 很早的提交;等后来要做一次完整 pull 时,会一次性把中间所有他人的提交(虚拟主机面板、 换绑手机、官网结算栏等)全量带进生产,部署范围失控。
对策:要么全量 git pull --ff-only;要么坚持精确同步,但必须清楚自己欠了多少提交, 并在部署前把「这次会连带上线哪些别人的提交」讲清楚,让人来决定。
3.5.5 构建前先备份 dist
TS=$(date +%Y%m%d-%H%M%S)
tar -czf /tmp/backup/www-dist-$TS.tar.gz frontend-user-v3-www/dist保留位置 /tmp/backup/,回滚时解包覆盖即可。
3.5.6 PHP 改动不需要重启 PHP-FPM
本机 opcache 为 validate_timestamps=On、revalidate_freq=2,同步 PHP 文件后最多 2 秒自动生效。 确认方式:php -i | grep -i opcache。只有改了 php.ini、扩展或 bootstrap/cache 才需要重启。
3.5.7 多台服务器时,先确认当前 SSH 连接指向哪台
有多个已连接的资源时,远程命令会打到「当前活动连接」,可能不是生产机。表现为 cd /www/wwwroot/TuraIDC: No such file or directory。每轮操作前先 ssh_list 确认当前连接, 或用 ssh_connect_profile 显式指定。
3.5.8 长任务用 nohup 后台 + 轮询
远程命令有超时上限(默认 30 秒),构建几十秒就会被中断。正确姿势:
nohup bash -c 'cd /www/wwwroot/TuraIDC && CI=true pnpm run build:frontends > /tmp/build.log 2>&1; echo "exit=$?" >> /tmp/build.log' > /dev/null 2>&1 &
sleep 100 && tail -10 /tmp/build.log # 轮询直到出现 exit=3.5.9 部署后必须 grep 产物验证
HTTP 200 只说明站点能开,不代表内容正确(3.5.1 就是 200 全绿但地址错)。收尾至少核对三项: 产物含本次改动的新关键字、新改动前的旧关键字为 0、硬编码域名与 backend/.env 真源一致。
3.5.10 带 TTL 的缓存会让「改了没生效」
console/capabilities 等响应缓存 300 秒、settings 分组缓存 600 秒,管理员改完设置不会立刻生效, 旧值也不会因为重新部署而消失。验证前先手动清对应缓存键。
4. 本地启动差异
本地同样使用直连 API:三个前端分别在 127.0.0.1:5175/5173/5174,API 在 127.0.0.1:8000。前端开发环境的 VITE_API_BASE_URL 已指向 http://127.0.0.1:8000/api,因此会真实验证 CORS 和 VNC WebSocket。
后端用一条封装命令同时拉起 HTTP、Relay 与队列:
cd backend
php artisan app:serve
# 等价于:HTTP + VNC Relay + 业务队列 Worker(queue:work 消费业务队列组)
# 如果本地想同时拉起调度,Relay 另行独立运行;
# 带调度时不再另起常驻 Worker,队列由每分钟 queue:drain 按队列组后台消费
php artisan app:serve --with-schedule --without-vnc
php artisan vnc:relay细节见 docs/develop/local-development.md 与 backend/app/Console/Commands/ServeBackendStackCommand.php。
5. 常见运维操作
5.1 查看调度运行情况
- 宝塔计划任务页可看脚本日志
backend/storage/logs/laravel.log- 管理端
/api/v2/admin/schedules/overview与/api/v2/admin/logs/tasks
5.2 手动触发一次调度
cd backend
php artisan schedule:run5.3 手动跑一次队列消费
正常不需要。只在排查"队列已堆积但未消费"时按目标队列手动运行:
cd backend
php artisan queue:work --once --queue=provision
php artisan queue:work --once --queue=referral,notification,coupon,default
php artisan queue:work --once --queue=automation5.4 清理失败队列
cd backend
php artisan queue:retry all # 重试全部失败
php artisan queue:flush # 清空失败队列(慎用)5.5 OPcache / 路由 / 配置缓存刷新
上线后如果路由或配置没生效,依次跑:
cd backend
php artisan route:clear && php artisan config:clear && php artisan cache:clear
php artisan route:cache && php artisan config:cache
# 重启 PHP-FPM(宝塔面板 → PHP → 重启)5.6 PHP 禁用函数问题
Composer 报 Call to undefined function putenv() 等错误时,优先核对宝塔 PHP 的禁用函数和 CLI / FPM 配置差异;当前仓库未保留单独修复指南。
6. 监控与告警
当前已集成 Sentry SDK(需配置 SENTRY_LARAVEL_DSN 激活),临时监控靠:
- Laravel 日志(
backend/storage/logs/laravel.log) - 宝塔监控面板
/api/v2/admin/logs/*管理端日志页- 健康检查端点:
/api/health(存活)、/api/ready(就绪,检查 DB/Cache/Storage/Scheduler)
/api/ready 会分别报告心跳存活、任务运行状态和队列状态;心跳只处理当前 15 分钟槽位,错过的槽位默认不自动回放,需依赖任务幂等补偿或管理端手动触发。
建议的整改方向:
- 配置 Sentry DSN 并激活错误追踪
- 关键任务失败告警(自动续费、支付回调、实名回调)
- 数据库慢查询日志启用
long_query_time=1
7. 回滚策略
- 后端:代码回退 +
composer install+php artisan migrate:rollback(仅当迁移可逆) - 前端:按站点独立回滚,直接恢复旧
dist - 数据库:禁止在生产直接
migrate:rollback不可逆迁移,走备份还原 - 关键业务(支付、开通、返佣)回退前必须先冻结
schedule:run,避免边回滚边产生新异步动作
8. 文件权限(宝塔部署后)
sudo chown -R www:www storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
sudo chmod -R 775 public/uploads public/media
# .env 文件必须严格权限,防止凭据泄露
sudo chmod 600 .env9. 数据库备份
项目提供 MySQL 定时备份脚本 backend/scripts/backup_mysql.sh,从 .env 读取数据库连接信息。
9.1 宝塔计划任务配置
在宝塔面板 → 计划任务 → 添加:
- 任务类型:Shell 脚本
- 执行周期:每天凌晨 3:00
- 脚本内容:
cd /你的项目/backend
bash scripts/backup_mysql.sh9.2 备份配置
| 环境变量 | 默认值 | 说明 |
|---|---|---|
BACKUP_DIR | backend/storage/backups/mysql | 备份文件存放目录 |
BACKUP_RETENTION_DAYS | 14 | 备份保留天数 |
9.3 手动备份
cd backend
bash scripts/backup_mysql.sh9.4 恢复备份
gunzip -c /path/to/backup.sql.gz | mysql -u用户名 -p密码 数据库名10. OPcache 生产配置
Laravel 生产环境强烈建议启用 OPcache 并优化配置。在宝塔面板 → PHP → 配置文件中添加或修改:
[opcache]
opcache.enable=1
opcache.enable_cli=1
; 根据项目大小调整,建议 256MB
opcache.memory_consumption=256
; Laravel 项目约 8000+ 文件,建议 20000
opcache.max_accelerated_files=20000
; 生产环境关闭时间戳验证,部署后需手动重启 PHP-FPM 或清除 OPcache
opcache.validate_timestamps=0
; 禁用文件保存,减少 I/O
opcache.save_comments=1
opcache.fast_shutdown=1
; 仅缓存项目文件,排除 vendor 中不常用的文件
opcache.blacklist_filename=/www/server/php/82/etc/opcache_blacklist.txt部署后必须重启 PHP-FPM 使 OPcache 和路由/配置缓存生效:
# 宝塔面板 → PHP → 重启
# 或命令行
sudo systemctl restart php-fpm-82OPcache 黑名单文件(可选,排除不需要缓存的文件):
# /www/server/php/82/etc/opcache_blacklist.txt
/vendor/phpunit/
/vendor/mockery/
/vendor/phpspec/
/vendor/phpstan/关联文档
- 启动指南:本地启动与验证。
- 四端 Nginx 伪静态配置:站点伪静态规则。
- 日志归档与 MySQL 维护:日志与数据库运维。
- 工程踩坑记录:开发、验证与提交环节的实测坑。