CaiWuXiTong/docs/开发与部署SOP.md

466 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CaiWuXiTong 开发与部署 SOP
## 1. 这套流程解决什么问题
本项目由 6 位新人使用 Codex 协作开发。团队按照已经确定的业务分工自行认领和协调任务;Codex 负责仓库检查、技术范围、分支、实现、测试和 PR,日常命令不能转交给新人。产品方向、验收结果和高风险操作仍按仓库规则确认。
开发协作固定链路如下,详细要求见 `docs/team-collaboration.md`:
```text
团队既有分工 -> Codex 检查 main、远程支线和既有实现
-> 运行 scripts/codex-task-preflight.ps1
-> 立即推送独立任务分支并创建 WIP PR
-> 开发、测试、再次碰撞检查
-> 任务 Codex 独立自审 -> 合并、验证 main 并清理分支
```
任务没有远程分支和 WIP PR 时,其他人的 Codex 无法看见,不得开始大量开发。发现相同页面、路由、Repository、表/视图、上游 `scope`、权限名、菜单键或公共组件时,相关 Codex 必须停止写入并报告重叠项,由相关开发者先完成协调。
固定链路如下:
```text
任务分支开发 -> 本机验证 -> PR -> 任务 Codex 独立自审 -> 合并并验证 main
-> 开发机复用缓存构建镜像
-> 经 SSH 隧道只上传变化的镜像层
-> 生产服务器只拉取本机 Registry 并切换镜像
-> 本机后端检查 + 公网 HTTPS 验收
-> 保留当前与上一个镜像,清理其余本项目产物
```
服务器永远不构建源码。
普通代码发布在相关验证通过、自审并合并到最新 `main` 后,由任务 Codex 按本 SOP 直接部署并验收,无需用户再次授权部署;用户明确要求暂不上线时遵循其范围。本任务所需的共享镜像接入、迁移和同步由 Codex 按 `AGENTS.md` 自主完成;其他数据库迁移、数据修复、删除和覆盖配置仍按其授权边界执行。
## 2. 每台开发机只做一次的设置
新人只负责启动 Docker Desktop、在网页中登录,以及输入一次系统数据库密码。下面涉及仓库、脚本、构建、迁移、缓存、容器、数据库验证和排错的动作均由 Codex 执行,不能把命令转交给新人。
1. 安装并启动 Docker Desktop,使用 WSL2 后端。
2. 在 Docker Desktop 的 `Settings -> Resources -> Advanced` 中:
- Docker 虚拟磁盘上限建议 60GB;
- Windows 磁盘至少保留 15GB 可用空间。
3. 首次部署仅需在本机密码窗口输入一次服务器密码,之后 Codex 自动复用本机加密保存的登录信息;不要求新人生成、配置或管理私钥。Windows 上密码使用当前用户绑定的 DPAPI 加密,保存在 `%LOCALAPPDATA%\CaiWuXiTong\deploy-credentials`;macOS 上保存在系统钥匙串(服务名 `CaiWuXiTong-deploy`)。两者都不进入仓库或日志。已有私钥的机器仍可选传 `-KeyPath`(macOS 为 `--key`)。
4. 只使用仓库里的发布脚本。不要自行执行 `docker system prune -a`,也不要重置 Docker Desktop。
Codex 已有用户提供的密码时可通过 `-Password` 的 `SecureString` 参数直接使用并保存,无需用户再输入;不要把明文写进命令参数或日志。密码变更时由 Codex 使用 `-UpdatePassword` 更新本机登录信息。部署结束保留加密凭据,下次自动使用。
说明: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 或当班开发者在仓库根目录执行:
```powershell
.\scripts\deploy-from-workstation.ps1 `
-Server 218.93.190.75 `
-BootstrapServer
```
macOS 开发机使用同一套流程的脚本:
```bash
bash scripts/deploy-from-mac.sh --bootstrap-server
```
初始化脚本会:
- 使用服务器现有 Ubuntu 安全更新源安装并启动 Docker 与 Compose;
- 服务器未配置 swap 时增加 2GB 交换空间,仅用于运行时缓冲;
- 创建 `/opt/caiwuxitong/runtime/.env`;
- 在服务器本地生成数据库密码和 Laravel `APP_KEY`,不会打印出来;
- 创建持久化数据库卷和运行文件卷;运行文件卷继续保存缓存、临时文件和可回滚的旧上传原件。
- 上传应用与 MariaDB 首次全量包,并初始化仅限本机访问的增量 Registry。
当前云平台仅提供可用的 `TCP 8442`,生产地址为 `https://caiwu.jiansuntuzhi.cn:8442`。应用容器只绑定服务器本机 `127.0.0.1:18088`,公网由轻量 Nginx 在 `8442` 提供 HTTPS;Registry 的 `5000` 端口仍禁止对公网开放。
生产域名使用公共 CA 签发的 `*.jiansuntuzhi.cn` 证书。证书和私钥只保存在服务器 Nginx 私有目录,禁止进入 Git、镜像或发布日志;更换证书后必须执行 `nginx -t`、重载并从外网验证域名和到期时间。
首次打开 `https://caiwu.jiansuntuzhi.cn:8442` 后,由负责人或指定人员在网页安装向导中设置管理员账号和密码。
## 5. 日常正式发布
前提:PR 已审核并合并,当前为干净的 `main`。
```powershell
git switch main
git pull --ff-only
.\scripts\deploy-from-workstation.ps1
```
macOS 开发机使用同一套流程的脚本:
```bash
git switch main
git pull --ff-only
bash scripts/deploy-from-mac.sh
```
脚本自动完成:
1. 检查分支和未提交改动;
2. 使用项目专用 Node 20 和本机 npm 缓存构建前端资源;
3. 将 JS、CSS、字体和 PWA 图片直接打进应用镜像,不上传七牛;
4. 首次由发布机安全写入服务器私有业务文件存储配置,后续直接复用服务器配置;
5. 使用提交 SHA 构建镜像并复用项目专属缓存;
6. 将应用镜像导出到临时文件,通过 SSH 隧道只上传 Registry 缺少的镜像层;
7. 服务器从本机 Registry 拉取新版本并启动;
8. 验证唯一私有七牛桶的写入、读取和删除,并把旧本机业务上传幂等复制到七牛;不删除本机原件;
9. 检查服务器本机后端页面;
10. 任一存储验证或页面验证失败时恢复上一个镜像;
11. 每次上传使用独立暂存目录;服务器取得发布锁和同步锁后,再核对线上 SHA 与发布机已检查的 SHA,确认未变化才切换配置和应用;
12. 删除本地临时镜像包并关闭 SSH 隧道;
13. 开发机、服务器 Docker 和服务器 Registry 均仅保留当前和上一个本项目版本;
14. 清理 7 天前的本项目构建缓存,并将总量压到最多 3GB。
### 5.0 多人发布保护
- 远端 `main` 禁止直接推送和强推,任务分支通过 PR 合并;不额外要求人工审批或新增 CI 门槛。
- 发布机先刷新远端 `main`,正式发布必须与最新主线一致,并检查本次版本包含线上提交。
- 上传期间若别人已发布,服务器在锁内拒绝本次激活,运行中的应用保持原版本。先 `git fetch`、快进到最新 `main` 并重新发布,不用回滚开关绕过并发校验。
- Windows 和 macOS 发布机都必须使用最新脚本。共享根目录的两个旧激活入口已停用并设置 Linux immutable 属性,旧发布机无法用上传覆盖它们;新脚本从每次独立暂存目录执行激活,缺少已检查线上版本参数也会被拒绝。
- 运行配置按每次发布独立暂存,核对通过后才在锁内安装;上传文件不会提前覆盖其他发布使用的配置。
- 服务器私有 `runtime/private/deployments.tsv` 记录起止 UTC 时间、发布机声明的身份、SSH 来源、前后 SHA、结果、退出码和已检查的 SHA。声明身份及提交作者不能单独证明具体操作者;记录不包含凭据或业务数据。
- 安装旧入口保护前自动备份到服务器私有 `runtime/private/release-protection/`。普通发布不解除 immutable 属性;维护这些停用入口时先核对备份和目的,再由维护人员使用 `chattr -i` 处理指定文件。应用镜像回滚不需要启用旧入口。
- 仅维护发布控制脚本时,可在发布锁内备份并安装已合并主线的控制脚本,记录独立的 `.release-controls-sha`,不重建应用容器、不修改表示应用镜像版本的 `.deployed-sha`。
### 5.1 安全软件拦截 regctl 时的降级
第 6 步默认用项目专用 `regctl`(未签名)把 BuildKit 原生 OCI 分层包推入服务器本机 Registry。少数开发机的 360 安全卫士、金山毒霸等安全软件会拦截未签名程序,表现为 `regctl-v0.11.5.exe` 启动即报“拒绝访问”。
- 脚本会自动降级:regctl 无法运行或连续上传失败时,改用 `docker save` 全量包经 `scp` 上传,在服务器上 `docker load`、推入本机 Registry,再执行同一个 `activate-incremental-release.sh`(回滚、健康检查、双版本保留和旧层回收逻辑不变)。降级只增加传输量,功能与发布结果一致。
- 也可以在受拦截的开发机上显式跳过 regctl:`.\scripts\deploy-from-workstation.ps1 -SkipRegctl`(macOS 为 `bash scripts/deploy-from-mac.sh --skip-regctl`)。
- 建议在这类开发机上把 `%LOCALAPPDATA%\CaiWuXiTong\tools` 加入安全软件信任区,以恢复只上传变化层的增量方式。
### 5.2 程序资源与业务文件的固定边界
- 程序资源:JavaScript、CSS、字体、PWA 图标和仓库图片。它们只随 Git 提交和应用镜像发布,浏览器从当前应用域名加载。
- 业务文件:用户上传的图片、票据、PDF、表格和附件。生产环境统一写入项目唯一的私有七牛桶,本地开发默认仍写入本机 `storage/app/uploads`。
- 两类资源不得混用。发布不再运行任何“静态资源同步到七牛”的脚本,七牛故障也不会阻止程序资源随镜像上线。
- 私有业务图片必须先经过应用的登录、公司和附件权限检查,再跳转到 5 分钟有效的签名地址;下载附件仍由应用鉴权。
- 七牛凭据只由 Codex 或发布负责人放在发布机用户环境、被 Git 忽略的 `runtime/qiniu-business-storage.env`,或服务器权限为 `0600` 的 `runtime/qiniu-storage.env` 中。不得提交、打印或转交新人。
- 首次切换不需要手工搬文件。发布激活命令会执行 `storage:business-files:sync --require-qiniu`,确认私有桶可读写后复制旧文件;失败即恢复旧镜像和本机文件读取。
## 6. 数据库迁移
默认禁止自动迁移。脚本发现上次部署后新增了 `database/migrations` 文件时会停止。
Codex 先核对全部待执行迁移,并准备受影响数据的备份与回滚。仅涉及本任务共享镜像建表、字段、索引和同步登记时,自主执行以下命令,无需负责人另行批准;其他迁移仍按 `AGENTS.md` 的授权边界执行:
```powershell
.\scripts\deploy-from-workstation.ps1 `
-Server 218.93.190.75 `
-AllowMigrations
```
macOS 上对应命令:
```bash
bash scripts/deploy-from-mac.sh --allow-migrations
```
应用镜像回滚不能自动撤销数据库结构变更;Codex 必须为本次迁移保留对应的数据备份和恢复路径。
## 7. 测试分支发布
正式服务器默认只允许 `main`。确需临时测试时,必须得到明确授权,并添加:
```powershell
-AllowNonMain
```
macOS 上对应参数为 `--allow-non-main`。
测试完成后仍要回到 `main` 再正式发布,不能把测试分支当长期生产版本。
## 8. 多上游财务数据库同步
新数据库和新数据集优先使用 [通用上游数据接入](通用上游数据接入.md):来源由连接记录自动登记,已启用的声明式数据集由调度自动首次同步和增量同步。下面的固定来源、逐项 `--full` 和旧快照说明适用于既有兼容路径,不应当作为新增普通开发任务的重复审批要求。
### 8.1 统一接入边界
本系统按 3 个以上上游数据库设计。生产系统数据库的启动连接只存在于服务器私有 `runtime/.env`;本地新人只填写系统数据库密码,即可自动解锁两套上游的固定主机、端口、库名、账号和密码,不需要复制生产 `APP_KEY`、填写上游环境变量或等待负责人开放范围。
本地开发探索可以直接执行代码中的固定只读查询,用于确认尚未接入的表和字段。正式业务页面不再把“页码 + 筛选条件”各自保存成一份临时快照,而是优先读取项目共享业务库 `caiwu_business_mirror` 中已经接入的表级副本。生产同步节点小批量访问上游并更新镜像,所有开发机和生产页面从同一份镜像做分页、筛选和汇总。无论哪种环境,上游 SQL 仍统一经过 `UpstreamReadOnlyQuery`,只允许参数化的 `SELECT`,防止误改旧财务库。
`caiwu_system_config` 与 `caiwu_business_mirror` 是同一台团队 MariaDB 中的两个独立逻辑库:前者保存用户、权限、设置、连接资料、数据字典和同步状态;后者只保存项目实际使用的上游业务数据。它们默认复用同一主机、端口、账号、密码和 TLS,因此新人仍只输入一次系统数据库密码,不需要理解或填写第二套连接参数。
固定调用链如下:
```text
本地探索/开发验证 -> 单来源适配器 -> 只读查询执行器 -> 对应上游
生产同步节点 -> 稳定游标分批读取 -> caiwu_business_mirror
本地与生产页面/API -> caiwu_business_mirror -> 本地分页、筛选、汇总
尚未迁移的旧页面 -> 共享查询快照(过渡兼容,不再扩展)
```
执行规则:
1. 已接入镜像的页面只能通过镜像 Repository 读取,不能直接连接上游;尚未迁移的旧页面暂时通过 `UpstreamQueryCache` 兼容;
2. 新适配器只能通过 `UpstreamReadOnlyQuery` 执行固定、参数化的 `SELECT/WITH`,不能把 PDO 交给控制器,不能执行写入、建表、存储过程或多语句;
3. 镜像表必须保留来源标识、稳定上游业务键、同步时间和必要行哈希;列表分页与筛选只在共享业务库执行,不能为每个页码复制一份上游结果;
4. 生产刷新每个来源默认最多 2 个冷查询并发、每分钟 6 个查询组、每轮刷新 2 个活跃查询;所有来源共用默认 4 个总槽位;
5. 生产环境中一个来源断线或熔断只影响自身;本地直连失败时直接返回脱敏错误,不阻塞另一个来源;
6. 跨来源报表禁止在 HTTP 请求中实时 JOIN 多个数据库。先分别同步到共享业务镜像并记录 `source_key`、上游业务键和同步时间,再从共享数据生成报表;
7. 新来源必须先完成单表、单查询探测并登记 `UpstreamSnapshotRefresher`,最后才允许开启;仅写入 `config/upstreams.php` 不代表已经接入完成;
8. 当前登记的后续来源模板保持关闭,尚未完成表到业务页面的映射和刷新器前,不得填入真实配置或开启总开关。
通用本机保护参数可在服务器私有配置中调整,默认值通常不需要修改:
```dotenv
UPSTREAM_MAX_CONCURRENCY=4
UPSTREAM_SLOT_WAIT_MS=1500
```
后台刷新可按来源执行:
```bash
php artisan upstream:snapshots:refresh <source_key>
```
尚未迁移页面仍由下面的兼容入口生成查询快照:
```bash
php artisan upstream:snapshots:refresh-all
```
旧的 `php artisan jiansun:snapshots:refresh` 继续保留兼容。已迁移的数据集使用自己的 `business-mirror:sync-*` 命令;部署脚本在同一把上游互斥锁内顺序调用,避免快照刷新和表级同步同时施压上游。
### 8.2 共享业务镜像接入闭环
项目不会复制上游所有表,只复制已经被正式业务代码使用的表或视图。每个数据集必须完成以下闭环:
1. 明确来源、对象、字段、业务键、稳定排序、增量依据和页面用途;
2. 首次全量同步使用固定时间窗、稳定游标、小批量和断点状态,任何中断都从已提交游标继续;
3. 首次全量完成前页面明确提示同步未完成,不能把第一页、局部快照或空表当成完整数据;
4. 后续同步优先按更新时间、完成时间或递增键增量读取,并保留有限重叠窗口覆盖迟到修正;
5. 上游异常时保留上一版镜像,页面继续读取旧数据并展示最近同步时间;禁止页面现场直连上游补页;
6. 验收至少对比镜像总数、第一页、第二页或末页、指定筛选、最近更新时间和上游限定计数;
7. 没有可靠增量依据的大表必须单独评估低频分段核对,不能在每分钟任务里全表扫描。
首个落地数据集是商品价格:上游 `operations_replica / rpt.Bulk_Price_Limit`,镜像表 `product_price_mirror_rows`,同步命令:
```bash
php artisan business-mirror:sync-product-prices --full
php artisan business-mirror:sync-product-prices
```
其余已在代码中使用的上游表由统一的数据集目录管理。首次全量必须逐个数据集显式执行,普通定时任务只续传已完成首次全量的数据集:
```bash
php artisan business-mirror:sync-tables <dataset-key> --full
php artisan business-mirror:sync-tables
```
完整数据集、来源对象、镜像表和分阶段切换状态见 `docs/上游业务镜像清单.md`。不得把“已登记同步目录”误报成“页面已经切换完成”;每个业务域仍需在首份全量核对通过后,把对应 Repository 改为只读镜像。
首次全量、历史回补和镜像校准由任务 Codex 自主执行:先备份受影响的系统配置、镜像表及同步状态,再在生产同步节点内使用现有上游互斥锁,按稳定游标分批执行或续传 `--full`,直至完成并核对源表/镜像和实际页面。无需负责人另行授权或代跑命令;开发机驱动不支持时直接通过 SSH 使用生产容器,不以本机不能同步为由停止任务。后续无参数命令只执行或继续受控增量,不对无关数据集批量全量。
重型发货回填、业绩明细重算、商品成本重算和店铺候选全量核对,统一从生产节点使用受限任务入口:
```bash
sh /opt/caiwuxitong/run-finance-batch.sh jushuitan:sync-shipping --from=YYYY-MM-DD --to=YYYY-MM-DD
sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:order-match:sync YYYY-MM --platform=taobao-alipay
```
入口持有现有上游同步锁,使用 `batch` Profile 的独立容器(1 核、3GiB 内存、不使用 Swap、无 Web 健康检查);正常发布不启动该 Profile。先备份再执行回补,失败时保留原有未完成状态,不重复并行启动任务。店铺候选平时按镜像同步时间增量刷新,02:45 使用 `--full` 全量核对。ClickHouse 系统日志的保留期由 `deploy/clickhouse-resource-policy.xml` 管理;业务表不受日志策略影响。
查件运单索引 `work_order_waybill_lookup` 只保存原始工单镜像的稳定键、运单号与排序键;工单 JSON 仍为事实来源。升级后先通过上述批入口执行 `business-mirror:index-work-order-waybills --max-batches=0 --pause-ms=50` 完成一次可续传初始化,完成前保留现有核验路径。其后索引与原始工单批次在同一事务中更新,全量校准同时移除失效关联。不得每次导入重新扫描工单 JSON。
销量汇总按 `store_report_paid_sales_dirty_dates` 中的受影响日期更新。业绩明细写入与汇总共用互斥锁,改金额、替换账期和删除都必须登记旧日期及新日期。镜像人工校准后,可通过批入口执行 `business-mirror:refresh-paid-sales --full` 全量重建派生汇总;普通刷新无需全表指纹扫描。科目重算每轮默认处理一个平台月份,再释放同步锁,剩余任务由下一轮继续。
账单导出保存在原有文件任务队列,每分钟由 `account-bills:exports:run` 消费一个任务,任一时刻只有一个重型导出。该只读任务使用同一受限批 Profile 和独立导出锁,不长期持有上游写同步锁;空队列不启动容器。状态、取消、下载和七天文件保留沿用现有逻辑。
生产临时排查先使用元数据和 `EXPLAIN`,业务读取须设置合理的 SQL 执行时限,并使用已有索引缩小范围。不要在网页容器内对归档大表进行无时限计数、模糊查找或循环重试;需要全表核对时先安排受控批处理窗口。
### 8.3 开发者需要尚未接入的上游数据时
系统数据库保存团队配置、数据字典和同步状态,共享业务库保存已接入业务行;二者都不是 SQL Server 网络代理。开发探索与正式页面的读取方式不同:
```text
本地开发探索 -> 通用只读入口 -> 使用共享凭据直接读取选定业务表
本地功能实现 -> 固定 Repository -> 使用共享凭据直接只读上游
正式页面 -> 镜像 Repository -> caiwu_business_mirror
生产同步节点 -> 固定批次 Repository -> 对应上游
```
开发者需要先确认表名和字段时,进入“系统设置 → 数据库管理 → 查看数据字典”。任何能直连上游的开发环境或生产刷新节点都可以低频读取两套上游的 `INFORMATION_SCHEMA`,把表、视图、字段、类型和可空信息保存到共享系统数据库 `upstream_schema_columns`。目录同步失败时保留最近一次完整结果,不会清空其他来源。需要更新目录时由 Codex 自动执行:
```bash
php artisan upstream:schema:sync
php artisan upstream:schema:sync operations_replica
```
数据字典只解决“有哪些表和字段”,不复制业务行,也不允许在系统数据库中保存任意 SQL。缺少现成 Repository、页面适配器、生产 `scope` 或共享快照时,本地开发并没有被挡住:Codex 通过 `UpstreamReadOnlyQueryFactory::forSource(<source_key>)` 对目录中选定的对象执行少量、限定字段和返回量的只读 `SELECT/WITH`,确认真实业务行和字段含义;新人不需要自己编写或执行探测 SQL。该探索查询不需要修改云数据库配置或等待生产刷新,也不得接受用户输入的原始 SQL。
每次新人接入验收必须逐个来源明确报告:来源标识、数据库和 Schema、验证的表或视图、读取字段、返回行数、耗时、`direct/fresh/stale` 状态以及失败阶段。不能只报告端口通、登录成功、`SELECT 1` 成功,也不能用“已有配置”“等待范围”代替真实业务只读查询。连接资料由系统自动读取并使用,不属于新人操作项。
确认数据后,再按下面的固定查询闭环把它实现成可合并、可测试、可部署的正式功能。Repository 是正式业务代码的交付要求,不是本地访问数据库和开始开发的前置条件。
需要新表、视图、字段或查询条件时,按下面的固定闭环执行:
1. **先声明边界**:任务和 PR 必须写明 `source_key`、业务用途、表或视图、字段、过滤条件、排序、最大返回量、公司/权限范围、期望 TTL 和没有数据时的页面表现。禁止只写“同步整张表”;
2. **实现固定查询**:Repository/适配器通过 `UpstreamReadOnlyQuery` 执行参数化的 `SELECT/WITH`;控制器和页面只调用 Repository,不接触 PDO,不把 SQL 保存到系统数据库;
3. **登记同步契约**:定义稳定且可审查的数据集键、上游业务键、同步游标、重叠窗口和镜像表。参数必须有限制,批次必须有稳定排序和行数上限;
4. **分支内完成自动测试**:使用脱敏 fixture 或测试替身验证正常、空数据和不可用状态;开发机能够直连时,通过通用只读入口或新 Repository 用少量真实只读数据验证,不得在测试、日志或截图中输出凭据和敏感业务行;
5. **本地直接验证**:开发阶段可以直接执行固定只读查询确认数据;正式页面测试使用隔离镜像库 fixture,不依赖负责人环境;
6. **生产首次同步**:代码审查和测试通过后合并到 `main`;由 Codex 完成受影响数据备份后自主创建镜像表并执行可断点的首次全量;
7. **完成共享验收**:确认同步状态成功、镜像总数和筛选计数与上游限定查询一致,第二页或末页可读;随后从第二个应用实例读取相同镜像,并证明页面没有再次访问上游;
8. **发现压力立即收口**:慢查询、返回量异常或上游告警时,先从共享记录移除该精确范围并保留证据,再缩小字段、时间窗或分页。禁止通过提高并发、扩大 `*` 或全表扫描解决。
部署脚本在统一上游互斥锁内运行兼容快照刷新和已登记的业务镜像同步。发布新增镜像数据集时,发布记录必须额外写明:镜像表迁移、批次 Repository、首次同步状态、镜像/上游对账、第二页或末页验证、第二实例复用,以及是否观察到慢查询或上游压力。未完成首次全量时,页面必须保持“同步未完成”,不能显示误导性空数据。
### 8.4 旧查询快照兼容范围
该来源按不可修改的只读旧系统处理,不要求对方修改账号、表结构、索引、字段或数据库功能。连接启用状态保存在系统数据库;`JIANSUN_ENABLED` 只用于明确开启的本机覆盖模式。
生产环境采用“活跃查询快照”策略:
1. 页面优先读取共享系统数据库中的 `upstream_query_cache`;相同来源、数据范围、查询条件和公司/权限范围复用同一结果,与系统账户和开发机数量无关;
2. 列表和汇总默认保持 2 分钟新鲜,下拉选项等低频数据缓存 60 分钟;
3. 新鲜快照直接返回;过期快照也立即返回,同时登记为紧急后台刷新,页面不等待上游;共享系统数据库中从未存在该查询时,页面只登记需求并明确提示正在准备,绝不从 HTTP 请求直接连接上游;
4. 只有生产服务器的 `/etc/cron.d/caiwu-jiansun-snapshots` 每分钟调用总刷新入口;开发机不再运行 `snapshot_worker`,避免不同分支重复访问上游;
5. 相同查询使用独立单飞锁,不同查询最多占用 2 个上游槽位,因此大量缓存命中不会被全局串行;
6. 每轮默认最多刷新 2 个活跃查询,每分钟默认最多产生 6 个实际上游查询组;列表优先于汇总,汇总优先于选项;
7. 连接 5 秒未建立或单次查询超过 15 秒即失败;慢查询和失败查询进入 5 分钟冷却,连接失败同时触发该来源熔断,不连续重试;
8. 生产服务器与上游不通时继续使用已有共享快照;完全没有共享数据时明确返回暂不可用。
相关 API 使用 `X-Caiwu-Data-Status`(本地为 `direct`,生产为 `fresh` / `stale`)和 `X-Caiwu-Data-Synced-At` 响应头报告数据状态,验收时不能只看 HTTP 200。
旧的环境变量连接方式只用于迁移、隔离排错或紧急回滚;日常由超级管理员或普通管理员在“数据库管理”的“云数据库”区域维护,并加密保存到系统数据库。系统数据库连接在同页的独立区域维护,不得与云数据库记录混用。
正常共享模式只要求生产服务器私有 `runtime/.env` 明确保留以下开关:
```dotenv
UPSTREAM_STORED_CONFIG_ENABLED=true
UPSTREAM_LOCAL_CONFIG_ENABLED=false
```
来源的启用状态、允许范围、地址、数据库名、账号和共享密码全部从系统数据库读取。正常共享模式不要在开发机或生产 `runtime/.env` 中填写 `DB_JIANSUN_*`、`DB_OPERATIONS_REPLICA_*` 或对应的 `*_ENABLED`。
下列参数是不含凭据的快照和保护参数,可以按来源保留在私有 `runtime/.env`;示例只展示 `jiansun`,其他来源使用自己的前缀:
```dotenv
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` 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。
开发者拉取新代码后,由 Codex 直接运行不带参数的 `deploy/local-init.sh`。脚本必须根据 Dockerfile、依赖锁文件、前端源码和静态资源计算镜像指纹,必要静态资源缺失或构建输入变化时自动重建;不得要求新人判断是否使用 `--build`。脚本和新版容器入口还必须自动补齐本机临时引导库的新增迁移,不能把 `Pending` 状态交给新人处理。迁移逻辑必须同时校验 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动和固定容器主机名 `mysql`;处于共享系统数据库模式时必须直接跳过,普通开发者不得执行或被提示执行共享数据库迁移。
系统数据库连接测试从应用容器发起,但新人页面只显示密码输入框。主机、端口、库名、账号和 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` 的受信任环境中先执行只读检查,再经负责人批准执行迁移:
```bash
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.2 接入共享业务镜像。禁止为了追求秒级实时而周期性扫描数百万行。
### 8.5 生产服务器不能直连上游时
使用一台指定开发机作为主中继,另一台作为备用中继。它们只转发 TCP 流量,不保存数据库凭据,也不在开发机执行同步查询。数据库凭据加密保存在系统数据库,中继机只保存已收紧 ACL 的端点和隧道配置;应用仍通过同一套快照、限流和熔断逻辑读取数据。
安全边界:
- 服务器创建专用低权限 SSH 用户,只允许远端端口转发到预先指定的 Docker 私有网桥地址和端口;禁止 Shell、TTY、Agent、本地转发和任意监听地址;
- 监听地址只能是 Docker 私有网桥,不能是 `0.0.0.0`、公网网卡或宿主机公网端口;
- 每台中继开发机生成独立 SSH 私钥,只把公钥交给具备生产权限的 Codex 或运维配置,禁止复制或共享私钥;
- 每个来源最多配置一主一备。同一来源的远端端口只会有一台成功监听,备用机每 30 秒左右低频重试;主机离线后备用机自动接管;
- 隧道断开不触发页面连续查询:已有快照继续返回 `stale`,没有快照时明确暂不可用。
由具备生产授权的 Codex 或运维在中继开发机执行下面的通用安装脚本,项目负责人不负责日常运行这些命令。尖括号内容从私有配置取得,禁止写入 Git、PR 或群聊:
```powershell
.\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;
- 禁止把程序 JS、CSS、字体、PWA 图标或仓库图片上传到七牛业务文件目录;
- 禁止把服务器 Registry 的 `5000` 端口开放到公网;
- 禁止为了省空间删除 Docker 卷;
- 禁止全局清空 Docker,因为同一台电脑可能还有其他项目;
- 禁止跳过健康检查后口头说“部署完成”。
## 10. 发布完成时应报告
- 发布分支和完整 Git SHA;
- 镜像标签;
- 服务器 `caiwu_app`、`caiwu_db` 状态;
- `https://caiwu.jiansuntuzhi.cn:8442` 的实际返回结果和证书状态;
- 是否执行数据库迁移;
- 本地临时包是否删除;
- 本地/服务器保留了几个本项目镜像;
- 本次增量新增镜像层大小和传输耗时;
- BuildKit 缓存当前占用。
- 私有业务文件存储探测是否通过、旧文件复制数量以及本机原件是否保留。