修复:完成新人双上游直连验收

This commit is contained in:
WuYongJie 2026-07-24 23:33:16 +08:00
parent 53537bb9ee
commit 59da42ec7d
4 changed files with 62 additions and 26 deletions

View File

@ -140,6 +140,7 @@
- 本地开发环境(`APP_ENV=local`)直接执行 `UpstreamQueryCache` resolver 中已经写入代码的只读查询,不等待生产刷新节点、共享快照或 `allowed_scopes`。两套连接资料仍从共享系统数据库自动解析,开发者只填写一次系统数据库密码,不再配置上游环境变量。
- 生产环境(`APP_ENV=production`)继续通过 `UpstreamQueryCache` 读取持久化在共享系统数据库 `upstream_query_cache` 中的按需查询快照;页面冷查询只登记需求,首份数据和后续刷新由 `upstream:snapshots:refresh-all` 生成。
- 两套上游的表、视图和字段结构由命令 `upstream:schema:sync` 低频读取 `INFORMATION_SCHEMA`,保存到共享系统数据库 `upstream_schema_columns`。任何能够直连的开发环境或生产刷新节点都可以生成目录;同步失败必须保留最近一次完整目录。
- 缺少现成 Repository、页面适配器、生产 `scope` 或共享快照,不代表本地开发无法读取该来源。开发者和 Codex 在 `APP_ENV=local` 中可以直接通过 `UpstreamReadOnlyQueryFactory::forSource(<source_key>)` 对数据字典中选定的对象执行少量、限定字段和返回量的只读 `SELECT/WITH`,用于确认业务行、理解字段并实现功能;不需要先修改云数据库记录或等待生产刷新。禁止把用户输入拼成 SQL、输出敏感业务行或以此替代合并前的正式 Repository/测试。
- 新来源适配器必须通过 `UpstreamReadOnlyQuery` 执行固定、参数化的只读查询,禁止把原始 PDO 连接暴露给控制器或页面。即使现有数据库账号本身有写权限,应用代码也不得执行写入、高风险语句或存储过程。
- 系统数据库只保存活跃查询登记、参数、访问范围和共享结果,不执行 SQL。禁止把任意 SQL、DSN 或凭据写入任务表让后台解释执行;生产后台进程只能运行已经合并到 `main` 并在 `UpstreamSnapshotRefresher` 中登记的固定查询代码。
- 开发者需要尚未缓存的新表或字段时,必须在任务说明和 PR 中列出来源标识、业务目的、表/视图、字段、筛选与排序、最大返回量、公司/权限范围、缓存时间和失败表现;未说明边界的“把整张表接进来”不得合并。
@ -165,6 +166,6 @@
- 本机临时引导库的新增迁移由 `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 网络代理。本地开发应用直接使用系统数据库中的连接资料访问两套上游;前提是该开发机网络本身能够到达数据库端点。
- 开发验收分别验证两套来源的共享配置解析、`SELECT 1` 和至少一个真实只读查询;生产验收还需验证共享快照。所有输出不得包含地址、账号、密码或完整 DSN。页面和 Repository 仍必须通过 `UpstreamReadOnlyQuery` 执行固定、参数化的 `SELECT`,禁止写操作和任意 SQL 输入。
- 开发验收必须在不复用负责人现有 `.env`、登录会话、容器或数据库卷的全新隔离环境中完成:只填写一次系统数据库密码,然后分别验证两套来源的共享配置解析、`SELECT 1` 和至少一个真实业务表/视图的只读查询。只在负责人已配置环境中测试不算新人接入验收。生产验收还需验证共享快照。所有输出不得包含地址、账号、密码、完整 DSN 或敏感业务行。页面和正式 Repository 仍必须通过 `UpstreamReadOnlyQuery` 执行固定、参数化的 `SELECT`,禁止写操作和任意 SQL 输入。
详细 SOP 见 `docs/开发与部署SOP.md`。

View File

@ -2,6 +2,7 @@
namespace App\Utilities;
use Dotenv\Dotenv;
use Illuminate\Support\Facades\Artisan;
use PDO;
use PDOException;
@ -403,11 +404,11 @@ class SystemDatabaseConfiguration
}
/**
* @return array<string, false>
* @return array<string, string|false>
*/
protected function freshVerificationEnvironment(): array
{
return array_fill_keys([
$keys = [
'DB_CONNECTION',
'DB_HOST',
'DB_PORT',
@ -421,7 +422,27 @@ class SystemDatabaseConfiguration
'SYSTEM_DATABASE_MODE',
'UPSTREAM_STORED_CONFIG_ENABLED',
'UPSTREAM_LOCAL_CONFIG_ENABLED',
], false);
];
$environment = array_fill_keys($keys, false);
$contents = file_get_contents($this->environmentPath);
if ($contents === false) {
throw new RuntimeException('无法读取刚保存的系统数据库配置。');
}
try {
$saved = Dotenv::parse($contents);
} catch (Throwable $exception) {
throw new RuntimeException('刚保存的系统数据库配置格式无效。', 0, $exception);
}
foreach ($keys as $key) {
if (array_key_exists($key, $saved)) {
$environment[$key] = (string) $saved[$key];
}
}
return $environment;
}
private function quoteIdentifier(string $identifier): string

View File

@ -190,7 +190,9 @@ php artisan upstream:snapshots:refresh-all
系统数据库保存团队配置、数据字典和生产快照,不是 SQL Server 网络代理。开发环境和生产环境的读取方式不同:
```text
本地开发环境 -> 固定 Repository -> 使用共享凭据直接只读上游
本地开发探索 -> 通用只读入口 -> 使用共享凭据直接读取选定业务表
本地功能实现 -> 固定 Repository -> 使用共享凭据直接只读上游
生产环境 -> 登记 scope + 参数 -> 后台固定刷新器 -> 共享查询快照
```
@ -202,14 +204,16 @@ php artisan upstream:schema:sync
php artisan upstream:schema:sync operations_replica
```
数据字典只解决“有哪些表和字段”,不复制业务行,也不允许在系统数据库中保存任意 SQL。开发者从目录选定对象后,仍按下面的固定查询闭环实现实际业务数据读取。
数据字典只解决“有哪些表和字段”,不复制业务行,也不允许在系统数据库中保存任意 SQL。缺少现成 Repository、页面适配器、生产 `scope` 或共享快照时,本地开发并没有被挡住:开发者或 Codex 可以通过 `UpstreamReadOnlyQueryFactory::forSource(<source_key>)` 对目录中选定的对象执行少量、限定字段和返回量的只读 `SELECT/WITH`,先确认真实业务行和字段含义。该探索查询不需要修改云数据库配置或等待生产刷新,但不得输出敏感业务行,也不得接受用户输入的原始 SQL。
确认数据后,再按下面的固定查询闭环把它实现成可合并、可测试、可部署的正式功能。Repository 是正式业务代码的交付要求,不是本地访问数据库和开始开发的前置条件。
需要新表、视图、字段或查询条件时,按下面的固定闭环执行:
1. **先声明边界**:任务和 PR 必须写明 `source_key`、业务用途、表或视图、字段、过滤条件、排序、最大返回量、公司/权限范围、期望 TTL 和没有数据时的页面表现。禁止只写“同步整张表”;
2. **实现固定查询**:Repository/适配器通过 `UpstreamReadOnlyQuery` 执行参数化的 `SELECT/WITH`;控制器和页面只调用 Repository,不接触 PDO,不把 SQL 保存到系统数据库;
3. **登记读取契约**:为列表、汇总和选项定义稳定且可审查的 `scope`,通过 `UpstreamQueryCache` 读取,并在对应 `UpstreamSnapshotRefresher` 中登记同名范围。参数必须有限制,列表必须有稳定排序和行数上限;
4. **分支内完成自动测试**:使用脱敏 fixture 或测试替身验证正常、空数据和不可用状态;开发机能够直连时再用少量真实只读数据验证,不得在测试、日志或截图中输出凭据和敏感业务行;
4. **分支内完成自动测试**:使用脱敏 fixture 或测试替身验证正常、空数据和不可用状态;开发机能够直连时,通过通用只读入口或新 Repository 用少量真实只读数据验证,不得在测试、日志或截图中输出凭据和敏感业务行;
5. **本地直接验证**:本地页面/API 直接执行固定只读查询,不需要配置 `allowed_scopes` 或等待首份快照;
6. **生产登记范围**:代码审查和测试通过后合并到 `main`;生产刷新器仍登记精确 `allowed_scopes` 并生成共享快照;
7. **完成双实例验收**:确认首份快照有同步时间,页面响应头为 `fresh` 或允许的 `stale`;随后从第二个应用实例用相同访问范围读取,并证明 resolver 没有再次访问上游;

View File

@ -88,28 +88,38 @@ class SystemDatabasePersistenceTest extends TestCase
}
}
public function test_fresh_verification_does_not_inherit_stale_database_environment(): void
public function test_fresh_verification_uses_values_from_the_just_saved_environment(): void
{
$configuration = new class extends SystemDatabaseConfiguration {
public function verificationEnvironment(): array
{
return $this->freshVerificationEnvironment();
}
};
$environmentPath = storage_path('framework/testing-fresh-verification.env');
file_put_contents($environmentPath, implode("\n", [
'DB_HOST="new-shared-host.test"',
'DB_DATABASE="new_shared_database"',
'DB_PASSWORD="new-shared-password"',
'SYSTEM_DATABASE_MODE="shared"',
'UPSTREAM_STORED_CONFIG_ENABLED="true"',
'UPSTREAM_LOCAL_CONFIG_ENABLED="false"',
'',
]));
$environment = $configuration->verificationEnvironment();
try {
$configuration = new class($environmentPath) extends SystemDatabaseConfiguration {
public function verificationEnvironment(): array
{
return $this->freshVerificationEnvironment();
}
};
foreach ([
'DB_HOST',
'DB_DATABASE',
'DB_PASSWORD',
'DB_PREFIX',
'SYSTEM_DATABASE_MODE',
'UPSTREAM_STORED_CONFIG_ENABLED',
'UPSTREAM_LOCAL_CONFIG_ENABLED',
] as $key) {
$this->assertArrayHasKey($key, $environment);
$this->assertFalse($environment[$key]);
$environment = $configuration->verificationEnvironment();
$this->assertSame('new-shared-host.test', $environment['DB_HOST']);
$this->assertSame('new_shared_database', $environment['DB_DATABASE']);
$this->assertSame('new-shared-password', $environment['DB_PASSWORD']);
$this->assertSame('shared', $environment['SYSTEM_DATABASE_MODE']);
$this->assertSame('true', $environment['UPSTREAM_STORED_CONFIG_ENABLED']);
$this->assertSame('false', $environment['UPSTREAM_LOCAL_CONFIG_ENABLED']);
$this->assertFalse($environment['DB_PREFIX']);
} finally {
@unlink($environmentPath);
}
}
}