24 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;本地新人流程使用不含密码的加密团队连接资料,开发者只填写系统数据库密码即可解锁固定主机、端口、库名和账号,TLS 由系统自动启用。显式开启 UPSTREAM_STORED_CONFIG_ENABLED 后,云数据库端点及账号保存在系统库中。密码同时保留旧版 APP_KEY 密文用于紧急回滚,并使用系统数据库登录密码派生的密钥写入 shared_password,因此不需要复制生产 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 upstream:snapshots:refresh-all
旧的 php artisan jiansun:snapshots:refresh 继续保留兼容。新增来源只需登记自己的 UpstreamSnapshotRefresher,不再为每台开发机单独添加 cron。
8.2 开发者需要尚未缓存的上游数据时
系统数据库是共享协调和快照存储,不是会主动发起 SQL 的数据库代理。开发者页面、生产后台进程与系统数据库之间的关系是:
开发者页面登记 scope + 参数
|
v
共享系统数据库中的活跃查询登记
|
v
已部署 main 中的固定刷新器 -> 生产刷新节点只读查询上游
|
v
共享系统数据库中的查询快照 -> 所有开发环境复用
开发者需要先确认表名和字段时,进入“系统设置 → 数据库管理 → 查看数据字典”。后台刷新节点每 24 小时只读取两套上游的 INFORMATION_SCHEMA,把表、视图、字段、类型和可空信息保存到共享系统数据库 upstream_schema_columns;普通开发机不直接连接云数据库。目录同步失败时保留最近一次完整结果,不会清空其他来源。负责人可在受控刷新节点手动执行:
php artisan upstream:schema:sync
php artisan upstream:schema:sync operations_replica
数据字典只解决“有哪些表和字段”,不复制业务行,也不允许在系统数据库中保存任意 SQL。开发者从目录选定对象后,仍按下面的固定查询闭环实现实际业务数据读取。
需要新表、视图、字段或查询条件时,按下面的固定闭环执行:
- 先声明边界:任务和 PR 必须写明
source_key、业务用途、表或视图、字段、过滤条件、排序、最大返回量、公司/权限范围、期望 TTL 和没有数据时的页面表现。禁止只写“同步整张表”; - 实现固定查询:Repository/适配器通过
UpstreamReadOnlyQuery执行参数化的SELECT/WITH;控制器和页面只调用 Repository,不接触 PDO,不把 SQL 保存到系统数据库; - 登记读取契约:为列表、汇总和选项定义稳定且可审查的
scope,通过UpstreamQueryCache读取,并在对应UpstreamSnapshotRefresher中登记同名范围。参数必须有限制,列表必须有稳定排序和行数上限; - 分支内先用替身验证:功能分支尚未进入生产镜像,生产刷新进程不认识新代码。普通开发机无法直连上游时,使用脱敏 fixture 或测试替身验证正常、空数据、过期和不可用状态,不得分发上游凭据;
- 先部署代码,后开放范围:代码审查和测试通过后合并到
main并完成部署;确认生产容器已经包含刷新器后,再由负责人在系统数据库对应云数据库记录中增加一个精确allowed_scopes。不得提前开放,也不得填写全局*; - 生成首份快照:调用一次实际页面/API,使其把来源、范围、参数及公司/权限上下文登记到共享系统数据库。本机网络被拦截时首次请求可以明确返回暂不可用;生产的
upstream:snapshots:refresh-all会在下一轮识别登记并受控读取上游; - 完成双实例验收:确认首份快照有同步时间,页面响应头为
fresh或允许的stale;随后从第二个应用实例用相同访问范围读取,并证明 resolver 没有再次访问上游; - 发现压力立即收口:慢查询、返回量异常或上游告警时,先从共享记录移除该精确范围并保留证据,再缩小字段、时间窗或分页。禁止通过提高并发、扩大
*或全表扫描解决。
部署脚本已经统一安装 upstream:snapshots:refresh-all,新增业务查询通常不需要新增 cron 或修改服务器 Compose。发布包含新范围时,发布记录必须额外写明:对应刷新器是否在镜像中、开放了哪些精确范围、首次快照是否成功、第二实例是否复用以及是否观察到慢查询/熔断。未完成这些验收时,范围保持关闭。
8.3 JianSun 现有来源
该来源默认保持关闭。它按不可修改的只读旧系统处理,不要求对方修改账号、表结构、索引、字段或数据库功能;只有现有访问方式通过受控连通性和单查询探测后,才在系统数据库的共享云数据库记录中启用。JIANSUN_ENABLED 只用于明确开启的本机覆盖模式。
启用后采用“活跃查询快照”策略:
- 页面优先读取共享系统数据库中的
upstream_query_cache;相同来源、数据范围、查询条件和公司/权限范围复用同一结果,与系统账户和开发机数量无关; - 列表和汇总默认保持 2 分钟新鲜,下拉选项等低频数据缓存 60 分钟;
- 新鲜快照直接返回;过期快照也立即返回,同时登记为紧急后台刷新,页面不等待上游;共享系统数据库中从未存在该查询时,页面只登记需求并明确提示正在准备,绝不从 HTTP 请求直接连接上游;
/etc/cron.d/caiwu-jiansun-snapshots和本地snapshot_worker每分钟调用总刷新入口,只处理最近 6 小时实际使用且已经到期的查询;- 相同查询使用独立单飞锁,不同查询最多占用 2 个上游槽位,因此大量缓存命中不会被全局串行;
- 每轮默认最多刷新 2 个活跃查询,每分钟默认最多产生 6 个实际上游查询组;列表优先于汇总,汇总优先于选项;
- 连接 5 秒未建立或单次查询超过 15 秒即失败;慢查询和失败查询进入 5 分钟冷却,连接失败同时触发该来源熔断,不连续重试;
- 某台开发机或服务器与上游不通时继续使用已有共享快照;完全没有共享数据时明确返回暂不可用,不绕过保护连续重试。
相关 API 使用 X-Caiwu-Data-Status(fresh / stale)和 X-Caiwu-Data-Synced-At 响应头报告数据状态,验收时不能只看 HTTP 200。
旧的环境变量连接方式只用于迁移、隔离排错或紧急回滚;日常由超级管理员或普通管理员在“数据库管理”的“云数据库”区域维护,并加密保存到系统数据库。系统数据库连接在同页的独立区域维护,不得与云数据库记录混用。
正常共享模式只要求生产服务器私有 runtime/.env 明确保留以下开关:
UPSTREAM_STORED_CONFIG_ENABLED=true
UPSTREAM_LOCAL_CONFIG_ENABLED=false
来源的启用状态、允许范围、地址、数据库名、账号和共享密码全部从系统数据库读取。正常共享模式不要在开发机或生产 runtime/.env 中填写 DB_JIANSUN_*、DB_OPERATIONS_REPLICA_* 或对应的 *_ENABLED。
下列参数是不含凭据的快照和保护参数,可以按来源保留在私有 runtime/.env;示例只展示 jiansun,其他来源使用自己的前缀:
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
只有明确进入本机覆盖模式时,才设置 UPSTREAM_LOCAL_CONFIG_ENABLED=true,并在私有 runtime/.env 中完整提供对应来源的 DB_<来源>_*、<来源>_ENABLED 和 <来源>_ALLOWED_SCOPES。覆盖模式不得作为团队日常开发方案。
开发机完成系统数据库配置后,系统自动设置 UPSTREAM_STORED_CONFIG_ENABLED=true 和 UPSTREAM_LOCAL_CONFIG_ENABLED=false,连接名称、地址、数据库名、账号、允许范围和共享密码均从系统数据库读取。不同开发机可以使用不同 APP_KEY,不得复制生产 APP_KEY。
本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 MySQL 标成财务系统配置数据库。开发者连接共享系统数据库时只输入密码;系统自动解锁固定地址、端口、库名、账号和 TLS 配置,只检查数据库能够连接且基础系统表存在。迁移版本、管理员权限映射、公司映射和云数据库共享密码均不得作为连接拦截条件。连接成功后立即清除临时账号 Session,并要求使用共享库正式账号重新登录;super_admin 和 admin 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。
系统数据库连接测试从应用容器发起,但新人页面只显示密码输入框。主机、端口、库名、账号和 TLS 均来自使用系统数据库密码加密的团队固定连接资料;不得把这些高级连接项重新暴露给普通开发者。密码错误必须回到原页面显示中文提示,网络或数据库暂不可用也必须保留旧连接并显示中文原因,不得出现 500。
容器内的 Laravel 初始化、缓存、测试、迁移和快照刷新必须统一以 www-data 执行。docker compose exec app php artisan ... 会默认使用 root,可能在持久化 storage 中创建网页进程不可写的目录或文件;统一改用 docker compose exec -u www-data app php artisan ...。容器入口负责修复历史属主,后台刷新进程固定使用 www-data,健康检查也以同一身份验证运行目录写权限,禁止只看登录页 HTTP 200。
旧记录只有 password、没有 shared_password 时,必须在仍持有旧 APP_KEY 的受信任环境中先执行只读检查,再经负责人批准执行迁移:
php artisan system-database:reencrypt-upstream-passwords
php artisan system-database:reencrypt-upstream-passwords --apply
命令执行前会在持久化 storage 中保存不含明文的私有备份;无法使用旧 APP_KEY 解密时会整体回滚,不得用本机空密码配置掩盖失败。
完成上述配置后,不得再把主业务连接的 DB_HOST 改回本机 MySQL 容器。系统用户、侧边栏、系统设置、云数据库登记、上游数据字典和按需查询快照都属于同一套系统数据库数据;切回本机 MySQL 会形成一套独立数据,页面可能仍可登录,但用户数量、连接名称、来源映射和业务结果都可能与其他开发机不同。标准来源映射固定为:jiansun 对应 JianSun 旧业务库,operations_replica 对应 JSReportReplica 的 rpt 只读视图。系统数据库统一保存连接配置、结构目录、活跃查询登记和共享快照,但不代理网络流量;标准模式由生产刷新节点持有共享锁并连接上游,普通开发机不承担上游网络连接。
只有隔离排错或尚未迁移的旧环境需要直接使用本机数据库参数时,才在私有 runtime/.env 设置 UPSTREAM_LOCAL_CONFIG_ENABLED=true,并完整填写 DB_JIANSUN_* 与 DB_OPERATIONS_REPLICA_*。开启后,该开发环境完全忽略共享云数据库配置。系统数据库密码轮换前必须先备份 system_database_connections 并使用新旧密码完成 shared_password 重加密;任何密码、密钥和私有配置不得进入 Git、聊天、日志或截图。
标准共享模式在系统数据库云数据库记录的 allowed_scopes 字段中填写逗号分隔的精确范围或 prefix.*;本机覆盖模式才使用 JIANSUN_ALLOWED_SCOPES。首次上线先开放完成探测的小表范围,例如 invoice-deductions.*;约 280 万行核心表对应范围必须单独验证,任何模式下都不得填写全局 *。
这套策略提供的是按代码实际使用范围生成的近实时查询快照,不等同于复制上游全库。没有业务代码调用的数据范围不会登记、不会查询、也不会同步。对于没有可靠更新时间或删除标记的旧表,不能声称实现了完整行级增量;禁止为了追求秒级实时而周期性扫描数百万行。页面实时性依靠共享快照和一分钟后台刷新,历史一致性只能通过低频、小批量、可随时停止的核对补齐。
8.4 生产服务器不能直连上游时
使用一台指定开发机作为主中继,另一台作为备用中继。它们只转发 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。核心大表不随总开关自动开放。
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 缓存当前占用。