15 KiB
CaiWuXiTong 开发与部署 SOP
1. 这套流程解决什么问题
本项目由 6 位新人使用 Codex 协作开发。项目负责人只确认方向、验收结果和高风险动作,不负责日常命令操作。
固定链路如下:
任务分支开发 -> 本机验证 -> PR -> 合并 main
-> 开发机复用缓存构建镜像
-> 经 SSH 隧道只上传变化的镜像层
-> 2GB 服务器只拉取本机 Registry 并切换镜像
-> 本机后端检查 + 公网 HTTPS 验收
-> 保留当前与上一个镜像,清理其余本项目产物
服务器永远不构建源码。
2. 每台开发机只做一次的设置
- 安装并启动 Docker Desktop,使用 WSL2 后端。
- 在 Docker Desktop 的
Settings -> Resources -> Advanced中:- Docker 虚拟磁盘上限建议 60GB;
- Windows 磁盘至少保留 15GB 可用空间。
- SSH 私钥放到个人目录,例如
C:\Users\你的用户名\.ssh\caiwu.pem,不要放进 Git。 - 只使用仓库里的发布脚本。不要自行执行
docker system prune -a,也不要重置 Docker Desktop。
说明:Docker 虚拟磁盘清理后,Windows 上看到的 .vhdx 文件可能不会立刻变小,但内部可用空间已经释放。不要为了让文件数字变小而删除虚拟磁盘。
3. 为什么后续不会反复全量下载和上传
项目使用专属构建器 caiwu-builder,缓存与其他项目分开:
- 前端在 Windows 主机使用项目专用 Node 20 构建,避开 Docker 虚拟机内存上限;
- JavaScript、Tailwind CSS 使用独立输入指纹;页面模板变化只重建 CSS,纯后端改动不重复打包前端;
- PHP 扩展层只在 Dockerfile 的基础环境变化时重建;
- Composer 只在
composer.lock变化时重新解析依赖,并复用下载缓存; - npm 优先使用本机下载缓存;
package-lock.json和 Node 版本未变化时直接复用node_modules; - JavaScript 公共依赖抽取为共享包,Node 堆上限为 4GB,压缩线程默认限制为 2;
- 业务代码变化只重新执行镜像中的代码复制;
- 日常命令不带
--pull和--no-cache。 - 首次初始化才上传应用和 MariaDB 的全量包;日常发布通过 SSH 隧道连接服务器本机 Registry。
- Registry 按内容哈希判断镜像层是否已经存在,基础环境、PHP 依赖和未变化的前端层不会重复上传。
- 日常传输使用 BuildKit 原生 OCI 分层包,保持未变化层的压缩摘要稳定;
docker save全量包只用于首次初始化。 - Registry 只监听服务器
127.0.0.1:5000,公网无法直接访问;发布脚本退出时会关闭 SSH 隧道。 - 每次发布后只保留当前和上一个 Registry 版本,并回收旧版本不再引用的镜像层。
首次构建和首次上传必然较慢;后续构建应大量显示 CACHED,上传量由本次实际变化的镜像层决定。
4. 首次初始化服务器
首次发布由 Codex 或当班开发者在仓库根目录执行:
.\scripts\deploy-from-workstation.ps1 `
-Server 118.196.84.79 `
-KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
-BootstrapServer
初始化脚本会:
- 使用服务器现有 Ubuntu 安全更新源安装并启动 Docker 与 Compose;
- 为 2GB 服务器增加 2GB swap(交换空间),仅用于运行时缓冲;
- 创建
/opt/caiwuxitong/runtime/.env; - 在服务器本地生成数据库密码和 Laravel
APP_KEY,不会打印出来; - 创建持久化数据库卷和上传文件卷。
- 上传应用与 MariaDB 首次全量包,并初始化仅限本机访问的增量 Registry。
云平台安全组必须放行 TCP 80 和 TCP 443。应用容器只绑定服务器本机 127.0.0.1:18088,公网由轻量 Nginx 提供 HTTPS;Registry 的 5000 端口仍禁止对公网开放。
首次部署后执行一次 HTTPS 配置:
.\scripts\deploy-from-workstation.ps1 `
-Server 118.196.84.79 `
-KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
-ConfigureHttps
该步骤在服务器安装轻量 Nginx 和独立 Certbot 环境,为公网 IP 申请 Let’s Encrypt 短期证书,并启用每 8 小时检查一次的自动续期。服务器仍不构建任何应用源码或镜像。
首次打开 https://118.196.84.79 后,由负责人或指定人员在网页安装向导中设置管理员账号和密码。
5. 日常正式发布
前提:PR 已审核并合并,当前为干净的 main。
git switch main
git pull --ff-only
.\scripts\deploy-from-workstation.ps1 `
-Server 118.196.84.79 `
-KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem"
脚本自动完成:
- 检查分支和未提交改动;
- 使用项目专用 Node 20 和本机 npm 缓存构建前端资源;
- 使用提交 SHA 构建镜像并复用项目专属缓存;
- 将应用镜像导出到临时文件,通过 SSH 隧道只上传 Registry 缺少的镜像层;
- 服务器从本机 Registry 拉取新版本并启动;
- 检查服务器本机后端页面;
- 失败时恢复上一个镜像;
- 使用服务器互斥锁阻止两个人同时切换版本;
- 删除本地临时镜像包并关闭 SSH 隧道;
- 开发机、服务器 Docker 和服务器 Registry 均仅保留当前和上一个本项目版本;
- 清理 7 天前的本项目构建缓存,并将总量压到最多 3GB。
6. 数据库迁移
默认禁止自动迁移。脚本发现上次部署后新增了 database/migrations 文件时会停止。
只有负责人明确批准、PR 已写清影响与回滚思路后,才允许:
.\scripts\deploy-from-workstation.ps1 `
-Server 118.196.84.79 `
-KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
-AllowMigrations
镜像回滚不能自动撤销已经执行的数据库结构变更,因此迁移授权必须单独处理。
7. 测试分支发布
正式服务器默认只允许 main。确需临时测试时,必须得到明确授权,并添加:
-AllowNonMain
测试完成后仍要回到 main 再正式发布,不能把测试分支当长期生产版本。
8. 多上游财务数据库同步
8.1 统一接入边界
本系统按 3 个以上上游数据库设计,但不会把多个数据库直接挂到页面上。config/upstreams.php 只登记不敏感的来源标识和配置文件名;每个来源再分别提供连接配置、固定查询适配器、允许的数据范围、快照、限流、熔断和刷新器。系统数据库的启动连接只存在于服务器私有 runtime/.env;显式开启 UPSTREAM_STORED_CONFIG_ENABLED 后,云数据库端点及账号保存在系统库中,密码使用当前 APP_KEY 加密。
固定调用链如下:
页面/API -> 本地快照 -> 单来源适配器 -> 只读查询执行器 -> 对应上游
|
+-> 受控物化到本地标准表/快照 -> 跨来源报表
执行规则:
- 页面只能通过
UpstreamQueryCache或现有兼容入口读取,不能直接连接上游; - 新适配器只能通过
UpstreamReadOnlyQuery执行固定、参数化的SELECT/WITH,不能把 PDO 交给控制器,不能执行写入、建表、存储过程或多语句; - 快照键必须包含来源、数据范围、查询条件以及公司/权限范围,不能按“登录账号个数”复制缓存,也不能让不同权限共用结果;
- 每个来源默认最多 2 个冷查询并发、每分钟 6 个查询组、每轮刷新 2 个活跃查询;所有来源的冷查询和后台刷新共用默认 4 个本机总槽位。缓存命中不占这些槽位;
- 一个来源断线或熔断只影响自身,其他来源和本地页面继续工作;
- 跨来源报表禁止在 HTTP 请求中实时 JOIN 多个数据库。先分别同步到本地并记录
source_key、上游业务键和同步时间,再从本地生成报表; - 新来源必须先完成单表、单查询探测并登记
UpstreamSnapshotRefresher,最后才允许开启;仅写入config/upstreams.php不代表已经接入完成; - 当前登记的后续来源模板保持关闭,尚未完成表到业务页面的映射和刷新器前,不得填入真实配置或开启总开关。
通用本机保护参数可在服务器私有配置中调整,默认值通常不需要修改:
UPSTREAM_MAX_CONCURRENCY=4
UPSTREAM_SLOT_WAIT_MS=1500
后台刷新命令按来源执行:
php artisan upstream:snapshots:refresh <source_key>
旧的 php artisan jiansun:snapshots:refresh 保留并转交通用命令,因此现有生产 cron 不需要立即变更。新增来源必须使用独立 cron,且不同来源错开秒数,不能在同一时刻集中唤醒。
8.2 JianSun 现有来源
生产环境默认设置 JIANSUN_ENABLED=false。该来源按不可修改的只读旧系统处理,不要求对方修改账号、表结构、索引、字段或数据库功能;只有现有访问方式通过受控连通性和单查询探测后,才在服务器私有配置中开启。
启用后采用“活跃查询快照”策略:
- 页面优先读取持久化在
caiwu_storage卷中的本地快照;相同数据范围和查询条件复用同一结果,与系统账户数量无关; - 列表和汇总默认保持 2 分钟新鲜,下拉选项等低频数据缓存 60 分钟;
- 新鲜快照直接返回;过期快照也立即返回,同时登记为紧急后台刷新,页面不等待上游;只有本地从未存在该查询时,才允许受控同步获取一次;
/etc/cron.d/caiwu-jiansun-snapshots每分钟触发一次,只处理最近 6 小时实际使用且已经到期的查询;- 相同查询使用独立单飞锁,不同查询最多占用 2 个上游槽位,因此大量缓存命中不会被全局串行;
- 每轮默认最多刷新 2 个活跃查询,每分钟默认最多产生 6 个实际上游查询组;列表优先于汇总,汇总优先于选项;
- 连接 5 秒未建立或单次查询超过 15 秒即失败;慢查询和失败查询进入 5 分钟冷却,连接失败同时触发该来源熔断,不连续重试;
- 服务器和上游不通时继续使用已有本地快照;完全没有本地数据时明确返回暂不可用,不绕过保护直接连接。
相关 API 使用 X-Caiwu-Data-Status(fresh / stale)和 X-Caiwu-Data-Synced-At 响应头报告数据状态,验收时不能只看 HTTP 200。
旧的环境变量连接方式只用于迁移或紧急回滚;日常由超级管理员或普通管理员在“数据库管理”的“云数据库”区域维护,并加密保存到系统数据库。系统数据库连接在同页的独立区域维护,不得与云数据库记录混用。生产服务器私有 runtime/.env 至少保留下列开关和非敏感保护参数,真实值不得写入 Git 或本文档:
UPSTREAM_STORED_CONFIG_ENABLED=true
JIANSUN_ENABLED=false
JIANSUN_ALLOWED_SCOPES=
DB_JIANSUN_HOST=
DB_JIANSUN_PORT=1433
DB_JIANSUN_DATABASE=
DB_JIANSUN_USERNAME=
DB_JIANSUN_PASSWORD=
DB_JIANSUN_ODBC_DRIVER=FreeTDS
JIANSUN_SNAPSHOT_TTL=120
JIANSUN_SUMMARY_TTL=120
JIANSUN_OPTIONS_TTL=3600
JIANSUN_ACTIVE_QUERY_TTL=21600
JIANSUN_ACTIVE_QUERY_LIMIT=300
JIANSUN_REFRESH_LIMIT=2
JIANSUN_MISS_BUDGET=6
JIANSUN_BUDGET_WINDOW=60
JIANSUN_MAX_CONCURRENCY=2
JIANSUN_FAILURE_COOLDOWN=300
JIANSUN_QUERY_FAILURE_COOLDOWN=300
JIANSUN_SLOW_QUERY_MS=5000
JIANSUN_SLOW_QUERY_COOLDOWN=300
JIANSUN_ALLOWED_SCOPES 支持逗号分隔的精确范围和 prefix.*。首次上线先开放完成探测的小表范围,例如 invoice-deductions.*;约 280 万行核心表对应范围必须单独验证,未验证前不得填写 *。
这套策略提供的是近实时查询快照,不等同于复制上游全库。对于没有可靠更新时间或删除标记的旧表,不能声称实现了完整行级增量;禁止为了追求秒级实时而周期性扫描数百万行。页面实时性依靠本地快照和一分钟后台刷新,历史一致性只能通过低频、小批量、可随时停止的核对补齐。
8.3 生产服务器不能直连上游时
使用一台指定开发机作为主中继,另一台作为备用中继。它们只转发 TCP 流量,不保存数据库凭据,也不在开发机执行同步查询。数据库凭据加密保存在系统数据库,中继机只保存已收紧 ACL 的端点和隧道配置;应用仍通过同一套快照、限流和熔断逻辑读取数据。
安全边界:
- 服务器创建专用低权限 SSH 用户,只允许远端端口转发到预先指定的 Docker 私有网桥地址和端口;禁止 Shell、TTY、Agent、本地转发和任意监听地址;
- 监听地址只能是 Docker 私有网桥,不能是
0.0.0.0、公网网卡或宿主机公网端口; - 每台中继开发机生成独立 SSH 私钥,只把公钥交给具备生产权限的 Codex 或运维配置,禁止复制或共享私钥;
- 每个来源最多配置一主一备。同一来源的远端端口只会有一台成功监听,备用机每 30 秒左右低频重试;主机离线后备用机自动接管;
- 隧道断开不触发页面连续查询:已有快照继续返回
stale,没有快照时明确暂不可用。
由具备生产授权的 Codex 或运维在中继开发机执行下面的通用安装脚本,项目负责人不负责日常运行这些命令。尖括号内容从私有配置取得,禁止写入 Git、PR 或群聊:
.\scripts\install-upstream-tunnel-task.ps1 `
-SourceKey '<config/upstreams.php 中的来源标识>' `
-ServerHost '<生产服务器>' `
-PrivateKeyPath '<本机专用隧道私钥>' `
-RemoteBindAddress '<服务器 Docker 私有网桥地址>' `
-RemoteBindPort '<服务器私有监听端口>' `
-UpstreamHost '<上游数据库地址>'
安装脚本按 SourceKey 为每个来源生成独立的任务名、私有配置和 PID 文件,并把端点和私钥路径写入当前用户专属文件;它会移除继承权限,只允许当前用户和 SYSTEM 读取。登录计划任务本身只保存该私有文件路径,不直接暴露端点。实际守护逻辑位于 scripts/keep-upstream-tunnel.ps1。每个来源必须使用不同的服务器私网监听端口;守护进程使用 SSH keepalive 检测断线并带随机延迟重连,避免主备机同时高频连接服务器。
每个来源都要单独按以下顺序验收:容器到该来源隧道端口 TCP 可达;只开放一个已探测的小表范围;第一次查询实际访问上游且在超时内完成;第二次相同查询命中本地快照且不再访问上游;最后才把服务器私有的该来源 *_ENABLED 改为 true。核心大表不随总开关自动开放。
9. 禁止事项
- 禁止登录服务器运行
docker build或docker compose build; - 禁止在服务器运行 Composer、npm 或前端打包;
- 禁止把
.pem、.env、密码、数据库文件或image.tar提交到 Git; - 禁止把服务器 Registry 的
5000端口开放到公网; - 禁止为了省空间删除 Docker 卷;
- 禁止全局清空 Docker,因为同一台电脑可能还有其他项目;
- 禁止跳过健康检查后口头说“部署完成”。
10. 发布完成时应报告
- 发布分支和完整 Git SHA;
- 镜像标签;
- 服务器
caiwu_app、caiwu_db状态; https://118.196.84.79的实际返回结果和证书有效期;- 是否执行数据库迁移;
- 本地临时包是否删除;
- 本地/服务器保留了几个本项目镜像;
- 本次增量新增镜像层大小和传输耗时;
- BuildKit 缓存当前占用。