diff --git a/AGENTS.md b/AGENTS.md index 4957e6ee..7099f874 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,6 +162,7 @@ - 本地开发的标准入口只有“系统设置 → 数据库管理 → 系统数据库”。开发者只填写并验证同一套系统数据库密码,其他连接参数由项目自动设置;成功后系统用户、侧边栏、系统设置和云数据库登记必须继续使用该系统数据库。不得再把主业务连接的 `DB_HOST` 改回本机 MySQL 容器,否则会形成一套互不相干的本地数据,导致用户、配置、来源映射和业务结果与团队不一致。 - 两个现有来源的标识和数据库映射固定为:`jiansun` 对应 `JianSun` 旧业务库,`operations_replica` 对应 `JSReportReplica` 的 `rpt` 只读视图。不得把 `shujuku`、本机测试库名或历史截图中的临时名称写回共享记录,也不得交换两个来源的账号或数据库名。 - 开发者拉取包含本规则的最新代码后,必须确保 `docker-compose.local.yml` 已挂载 `./modules:/var/www/html/modules`,并至少重建一次应用容器、清除 Laravel 配置缓存。禁止只重启旧镜像后根据旧模块结果判断共享配置无效。 +- 本机临时引导库的新增迁移由 `deploy/local-init.sh` 和新版容器入口自动补齐;该逻辑只允许在 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动且主机为固定容器服务名 `mysql` 时运行。连接共享系统数据库后必须自动跳过,普通开发者不需要也不得自行执行 `php artisan migrate`;看到本机迁移 `Pending` 时应重新运行标准初始化脚本,而不是操作共享数据库。 - Codex 排查开发机连接时,必须先确认当前主业务库仍是共享系统数据库,再确认 `UPSTREAM_STORED_CONFIG_ENABLED=true`、`UPSTREAM_LOCAL_CONFIG_ENABLED=false`,随后通过 `UpstreamSourceRegistry` 验证完整连接来自 `system_database_connections`。不得通过填写本机 `DB_JIANSUN_*`、`DB_OPERATIONS_REPLICA_*`、复制他人 `.env` 或统一 `APP_KEY` 来掩盖共享配置读取错误。 - 系统数据库统一保存应用数据、云数据库配置、上游数据字典、活跃查询登记和按需产生的共享查询快照,但不是 SQL Server 网络代理。标准模式由具备连通性的生产后台刷新进程负责同步结构、获取首份数据和后续刷新;普通开发机只查看共享数据字典、登记需求并复用共享快照,不要求逐台配置白名单、VPN 或隧道。只有已确认生产刷新节点也被网络拦截时,才处理上游白名单或受限隧道。 - 验收不能只做端口探测或数据库客户端直连。发布验收由具备网络连通性的生产刷新节点分别验证两套来源的共享配置解析、`SELECT 1` 和至少一个真实只读业务查询;普通开发机清空本机上游覆盖后,必须验证第二个应用实例能够复用共享快照且不再次访问上游。所有输出不得包含地址、账号、密码或完整 DSN。数据库客户端看到的全部对象与项目页面结果并不等价,项目还受固定适配器、允许范围、公司/用户权限和共享快照约束;缺少业务适配器或刷新器时应实现对应读取链路,不得绕过缓存把页面直接接到 PDO。 diff --git a/LOCAL_DEPLOY.md b/LOCAL_DEPLOY.md index 63581ffc..0c4ccada 100644 --- a/LOCAL_DEPLOY.md +++ b/LOCAL_DEPLOY.md @@ -44,8 +44,9 @@ bash deploy/local-init.sh --build 1. 在 `runtime/.env` 不存在时生成仅供本机使用的环境配置和随机 `APP_KEY`; 2. 首次构建并启动应用和临时 MySQL; 3. 随机生成临时管理员密码,只写入被 Git 忽略的本机私有文件; -4. 检查安装状态、数据库连接、运行目录和登录页; -5. 提示下一步配置系统数据库。 +4. 自动补齐本机临时引导库的新结构,不要求开发者执行迁移命令; +5. 检查安装状态、数据库连接、运行目录和登录页; +6. 提示下一步配置系统数据库。 首次临时登录凭据保存在 `runtime/bootstrap-credentials.env`。脚本只显示文件路径,不打印密码。不要提交、截图或转发该文件。 @@ -53,6 +54,8 @@ bash deploy/local-init.sh --build 初始化中断后直接重新执行 `bash deploy/local-init.sh` 会复用已有镜像并从数据库初始化阶段继续。只有依赖、Dockerfile 或构建资源发生变化时才使用 `--build`;确认只启动现有镜像时可使用 `--skip-build`。 +拉取新代码后再次运行该脚本时,本机临时引导库会自动更新;连接团队共享系统数据库后,这项自动更新会安全跳过。普通开发者任何时候都不需要执行 `php artisan migrate`,也不得对团队共享系统数据库自行迁移。 + ## 配置系统数据库 1. 打开 `http://127.0.0.1:18088/auth/login`。 diff --git a/app/Console/Commands/UpdateLocalBootstrapDatabase.php b/app/Console/Commands/UpdateLocalBootstrapDatabase.php new file mode 100644 index 00000000..61e2234d --- /dev/null +++ b/app/Console/Commands/UpdateLocalBootstrapDatabase.php @@ -0,0 +1,51 @@ +current(); + + if ($settings['mode'] !== 'bootstrap') { + $this->components->info('当前使用共享系统数据库,本机不会执行数据库迁移。'); + + return self::SUCCESS; + } + + if ( + $settings['connection'] !== 'mysql' + || $settings['driver'] !== 'mysql' + || $settings['host'] !== 'mysql' + ) { + $this->components->error('本机引导库配置异常,已拒绝自动迁移。'); + + return self::FAILURE; + } + + $exitCode = $migrator->run(); + + if ($exitCode !== self::SUCCESS) { + return self::FAILURE; + } + + $this->components->info('本机临时引导库已自动更新。'); + + return self::SUCCESS; + } +} diff --git a/app/Utilities/LocalBootstrapMigrator.php b/app/Utilities/LocalBootstrapMigrator.php new file mode 100644 index 00000000..5d708d1c --- /dev/null +++ b/app/Utilities/LocalBootstrapMigrator.php @@ -0,0 +1,16 @@ + true, + '--no-interaction' => true, + ]); + } +} diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index d30e1076..c4b2ace2 100644 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -44,6 +44,11 @@ if [ "${1:-}" = "apache2-foreground" ]; then run_artisan_as_web package:discover --ansi if grep -Eq '^APP_INSTALLED=(true|1)$' .env; then + # 开发机的临时引导库属于可恢复的本机数据,拉取新代码后自动补齐 + # 结构;命令会在共享系统数据库模式下直接跳过,绝不由开发机迁移 + # 团队共享数据库。 + run_artisan_as_web local-bootstrap:update --no-ansi + if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then run_artisan_as_web migrate --force fi diff --git a/deploy/local-init.sh b/deploy/local-init.sh index 089a1d6f..b5554ffa 100644 --- a/deploy/local-init.sh +++ b/deploy/local-init.sh @@ -193,8 +193,12 @@ else echo "==> [4/5] 当前环境已经初始化,跳过本机引导库安装。" fi -echo "==> [5/5] 验证安装状态、数据库和管理员权限..." +echo "==> 自动检查本机临时引导库结构..." docker compose -f "$compose_file" exec -T -u www-data app php artisan optimize:clear >/dev/null +docker compose -f "$compose_file" exec -T -u www-data app \ + php artisan local-bootstrap:update --no-ansi + +echo "==> [5/5] 验证安装状态、数据库和管理员权限..." ready=0 for i in $(seq 1 30); do diff --git a/docs/开发与部署SOP.md b/docs/开发与部署SOP.md index 24187362..c4da67ee 100644 --- a/docs/开发与部署SOP.md +++ b/docs/开发与部署SOP.md @@ -272,6 +272,8 @@ JIANSUN_SLOW_QUERY_COOLDOWN=300 本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 MySQL 标成财务系统配置数据库。开发者连接共享系统数据库时只输入密码;系统自动解锁固定地址、端口、库名、账号和 TLS 配置,只检查数据库能够连接且基础系统表存在。迁移版本、管理员权限映射、公司映射和云数据库共享密码均不得作为连接拦截条件。连接成功后立即清除临时账号 Session,并要求使用共享库正式账号重新登录;`super_admin` 和 `admin` 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。 +开发者拉取新代码后,`deploy/local-init.sh` 和新版容器入口必须自动补齐本机临时引导库的新增迁移,不能把 `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。 diff --git a/tests/Feature/Installation/UpdateLocalBootstrapDatabaseTest.php b/tests/Feature/Installation/UpdateLocalBootstrapDatabaseTest.php new file mode 100644 index 00000000..b7a32fa0 --- /dev/null +++ b/tests/Feature/Installation/UpdateLocalBootstrapDatabaseTest.php @@ -0,0 +1,77 @@ + true]); + + $configuration = Mockery::mock(SystemDatabaseConfiguration::class); + $configuration->shouldReceive('current')->once()->andReturn([ + 'connection' => 'mysql', + 'driver' => 'mysql', + 'host' => 'shared-db.test', + 'mode' => 'shared', + ]); + $this->app->instance(SystemDatabaseConfiguration::class, $configuration); + + $this->artisan('local-bootstrap:update') + ->expectsOutputToContain('本机不会执行数据库迁移') + ->assertSuccessful(); + } + + public function test_uninstalled_environment_does_not_run_migrations(): void + { + config(['app.installed' => false]); + + $configuration = Mockery::mock(SystemDatabaseConfiguration::class); + $configuration->shouldNotReceive('current'); + $this->app->instance(SystemDatabaseConfiguration::class, $configuration); + + $this->artisan('local-bootstrap:update')->assertSuccessful(); + } + + public function test_nonstandard_bootstrap_connection_is_rejected(): void + { + config(['app.installed' => true]); + + $configuration = Mockery::mock(SystemDatabaseConfiguration::class); + $configuration->shouldReceive('current')->once()->andReturn([ + 'connection' => 'mysql', + 'driver' => 'mysql', + 'host' => 'unexpected-host', + 'mode' => 'bootstrap', + ]); + $this->app->instance(SystemDatabaseConfiguration::class, $configuration); + + $this->artisan('local-bootstrap:update')->assertFailed(); + } + + public function test_standard_local_bootstrap_database_is_updated_automatically(): void + { + config(['app.installed' => true]); + + $configuration = Mockery::mock(SystemDatabaseConfiguration::class); + $configuration->shouldReceive('current')->once()->andReturn([ + 'connection' => 'mysql', + 'driver' => 'mysql', + 'host' => 'mysql', + 'mode' => 'bootstrap', + ]); + $this->app->instance(SystemDatabaseConfiguration::class, $configuration); + $migrator = Mockery::mock(LocalBootstrapMigrator::class); + $migrator->shouldReceive('run')->once()->andReturn(0); + $this->app->instance(LocalBootstrapMigrator::class, $migrator); + + $this->artisan('local-bootstrap:update') + ->expectsOutputToContain('本机临时引导库已自动更新') + ->assertSuccessful(); + } +} diff --git a/tests/Unit/Deployment/WebProcessOwnershipTest.php b/tests/Unit/Deployment/WebProcessOwnershipTest.php index 5279382e..109aa5ba 100644 --- a/tests/Unit/Deployment/WebProcessOwnershipTest.php +++ b/tests/Unit/Deployment/WebProcessOwnershipTest.php @@ -15,6 +15,10 @@ class WebProcessOwnershipTest extends TestCase 'runuser -u www-data -- php artisan "$@"', $entrypoint ); + $this->assertStringContainsString( + 'run_artisan_as_web local-bootstrap:update --no-ansi', + $entrypoint + ); $this->assertStringContainsString('assert_web_writable', $entrypoint); $this->assertDoesNotMatchRegularExpression('/^\s*php artisan /m', $entrypoint); } @@ -39,6 +43,10 @@ class WebProcessOwnershipTest extends TestCase 'exec -T -u www-data app', $initializer ); + $this->assertStringContainsString( + 'php artisan local-bootstrap:update --no-ansi', + $initializer + ); $this->assertDoesNotMatchRegularExpression( '/exec -T app\s+(?:\\\\\s*)?php artisan/', $initializer