This commit is contained in:
2026-08-11 18:06:26 +08:00
parent a02dd3a422
commit 192964419f
101 changed files with 8892 additions and 1952 deletions
+16 -1
View File
@@ -44,9 +44,24 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
## 现有相关脚本与边界
### 数据库中文注释审计
`database-comment-audit.ps1` 离线扫描各模块 `src/main/resources/db/*.sql` 中的 `CREATE TABLE` 定义,不连接数据库、不执行 SQL,也不修改任何文件。
```powershell
.\scripts\database-comment-audit.ps1
.\scripts\database-comment-audit.ps1 -Modules finance,order
.\scripts\database-comment-audit.ps1 -Modules finance -SummaryOnly
.\scripts\database-comment-audit.ps1 -Format Json
.\scripts\database-comment-audit.ps1 -FailOnIssues
.\scripts\database-comment-audit.ps1 -SelfTest
```
默认报告模式即使存在存量缺口也返回 `0`;CI 使用 `-FailOnIssues`,缺失或纯英文表/字段注释返回 `1`,模块或路径配置错误返回 `2`。规范和注释口径见 `../docs/数据库注释维护规范.md`。
### 本地 MySQL 全量验收
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0` 和 7803 行。
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `7F61C3BAB0D4D49525A30C1E5030DEA4B798F38F6A0287901E6D1D676967DFFA` 和 13712 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
```powershell
.\local-db-full-acceptance.ps1 -DryRun
+230
View File
@@ -0,0 +1,230 @@
[CmdletBinding()]
param(
[string]$RepositoryRoot,
[string[]]$Modules,
[ValidateSet("Text", "Json")]
[string]$Format = "Text",
[switch]$SummaryOnly,
[switch]$FailOnIssues,
[switch]$SelfTest
)
$ErrorActionPreference = "Stop"
function Get-SqlComment {
param([string]$Text)
$match = [regex]::Match($Text, "(?is)\bCOMMENT\s*(?:=\s*)?'(?<value>(?:''|[^'])*)'")
if (-not $match.Success) {
return $null
}
return $match.Groups["value"].Value.Replace("''", "'").Trim()
}
function Get-CreateTableDefinitions {
param(
[string]$Sql,
[string]$Source
)
$pattern = '(?is)\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?<name>`?[A-Za-z_][A-Za-z0-9_$]*`?)\s*\((?<body>.*?)\)\s*(?<tail>ENGINE\s*=.*?);'
$mysqlColumnTypes = @(
"BIGINT", "INT", "INTEGER", "MEDIUMINT", "SMALLINT", "TINYINT",
"DECIMAL", "NUMERIC", "FIXED", "FLOAT", "DOUBLE", "REAL", "BIT", "BOOL", "BOOLEAN",
"CHAR", "VARCHAR", "NCHAR", "NVARCHAR", "BINARY", "VARBINARY",
"TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT",
"TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB", "ENUM", "SET",
"DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", "JSON",
"GEOMETRY", "POINT", "LINESTRING", "POLYGON", "MULTIPOINT",
"MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"
)
$matches = [regex]::Matches($Sql, $pattern)
$definitions = @()
foreach ($match in $matches) {
$columns = @()
foreach ($line in ($match.Groups["body"].Value -split "`r?`n")) {
$columnMatch = [regex]::Match($line, '^\s*(?<name>`?[A-Za-z_][A-Za-z0-9_$]*`?)\s+(?<definition>[A-Za-z].*?)\s*,?\s*$')
if (-not $columnMatch.Success) {
continue
}
$definition = $columnMatch.Groups["definition"].Value
$typeMatch = [regex]::Match($definition, '^(?<type>[A-Za-z]+)\b')
if (-not $typeMatch.Success -or $typeMatch.Groups["type"].Value.ToUpperInvariant() -notin $mysqlColumnTypes) {
continue
}
$columnName = $columnMatch.Groups["name"].Value.Trim('`')
$columns += [pscustomobject]@{
Name = $columnName
Comment = Get-SqlComment -Text $line
}
}
$definitions += [pscustomobject]@{
Source = $Source
Table = $match.Groups["name"].Value.Trim('`')
Comment = Get-SqlComment -Text $match.Groups["tail"].Value
Columns = $columns
}
}
return $definitions
}
function Test-ChineseComment {
param([AllowNull()][string]$Comment)
return -not [string]::IsNullOrWhiteSpace($Comment) -and $Comment -match '[\p{IsCJKUnifiedIdeographs}]'
}
function Get-AuditResult {
param([object[]]$Definitions)
$issues = @()
$tableCount = 0
$commentedTableCount = 0
$columnCount = 0
$commentedColumnCount = 0
foreach ($definition in $Definitions) {
$tableCount++
if (-not [string]::IsNullOrWhiteSpace($definition.Comment)) {
$commentedTableCount++
}
if ([string]::IsNullOrWhiteSpace($definition.Comment)) {
$issues += [pscustomobject]@{ Code = "MISSING_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null }
} elseif (-not (Test-ChineseComment -Comment $definition.Comment)) {
$issues += [pscustomobject]@{ Code = "NON_CHINESE_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null }
}
foreach ($column in $definition.Columns) {
$columnCount++
if (-not [string]::IsNullOrWhiteSpace($column.Comment)) {
$commentedColumnCount++
}
if ([string]::IsNullOrWhiteSpace($column.Comment)) {
$issues += [pscustomobject]@{ Code = "MISSING_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name }
} elseif (-not (Test-ChineseComment -Comment $column.Comment)) {
$issues += [pscustomobject]@{ Code = "NON_CHINESE_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name }
}
}
}
$tableCoverage = if ($tableCount -eq 0) { 100 } else { [math]::Round(100 * $commentedTableCount / $tableCount, 2) }
$columnCoverage = if ($columnCount -eq 0) { 100 } else { [math]::Round(100 * $commentedColumnCount / $columnCount, 2) }
return [pscustomobject]@{
Summary = [pscustomobject]@{
TableDefinitions = $tableCount
CommentedTables = $commentedTableCount
TableCoveragePercent = $tableCoverage
Columns = $columnCount
CommentedColumns = $commentedColumnCount
ColumnCoveragePercent = $columnCoverage
Issues = $issues.Count
}
Issues = $issues
}
}
function Invoke-SelfTest {
$validSql = @"
CREATE TABLE IF NOT EXISTS sample_valid (
id BIGINT NOT NULL COMMENT '主键ID',
status VARCHAR(16) NOT NULL COMMENT '状态:ACTIVE启用、DISABLED停用',
amount DECIMAL(20,6) NOT NULL COMMENT '含税金额;币种由currency_code指定',
and_flag TINYINT(1) NOT NULL COMMENT '附加条件标记:0否、1是',
CONSTRAINT chk_sample_status CHECK (
status IN ('ACTIVE', 'DISABLED')
AND amount >= 0
OR (status = 'DISABLED' AND amount = 0)
)
) ENGINE=InnoDB COMMENT='自测业务表';
"@
$invalidSql = @"
CREATE TABLE sample_invalid (
id BIGINT NOT NULL,
payload JSON NULL COMMENT 'JSON payload'
) ENGINE=InnoDB COMMENT='Sample table';
"@
$valid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $validSql -Source "valid.sql")
if ($valid.Summary.TableDefinitions -ne 1 -or $valid.Summary.Columns -ne 4 -or $valid.Summary.Issues -ne 0) {
throw "Self-test failed: valid SQL was not parsed as expected."
}
$invalid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $invalidSql -Source "invalid.sql")
$codes = @($invalid.Issues | ForEach-Object { $_.Code })
foreach ($expected in @("NON_CHINESE_TABLE_COMMENT", "MISSING_COLUMN_COMMENT", "NON_CHINESE_COLUMN_COMMENT")) {
if ($expected -notin $codes) {
throw "Self-test failed: missing expected issue $expected."
}
}
Write-Output "SELF-TEST PASS: parser, coverage and Chinese-comment checks are working."
}
if ($SelfTest) {
Invoke-SelfTest
exit 0
}
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
$RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
} else {
$RepositoryRoot = (Resolve-Path $RepositoryRoot).Path
}
$dbDirectories = @(Get-ChildItem -LiteralPath $RepositoryRoot -Directory | ForEach-Object {
$candidate = Join-Path $_.FullName "src/main/resources/db"
if (Test-Path -LiteralPath $candidate -PathType Container) {
[pscustomobject]@{ Module = $_.Name; Path = $candidate }
}
})
if ($Modules -and $Modules.Count -gt 0) {
$requested = @($Modules | ForEach-Object { $_.ToLowerInvariant() })
$dbDirectories = @($dbDirectories | Where-Object { $_.Module.ToLowerInvariant() -in $requested })
$found = @($dbDirectories | ForEach-Object { $_.Module.ToLowerInvariant() })
$missingModules = @($requested | Where-Object { $_ -notin $found })
if ($missingModules.Count -gt 0) {
Write-Error ("Unknown modules or missing db directories: " + ($missingModules -join ", "))
exit 2
}
}
$definitions = @()
foreach ($dbDirectory in ($dbDirectories | Sort-Object Module)) {
foreach ($file in (Get-ChildItem -LiteralPath $dbDirectory.Path -Filter "*.sql" -File | Sort-Object FullName)) {
$relativePath = $file.FullName.Substring($RepositoryRoot.Length + 1).Replace('\', '/')
$sql = Get-Content -Raw -LiteralPath $file.FullName
$definitions += @(Get-CreateTableDefinitions -Sql $sql -Source $relativePath)
}
}
$result = Get-AuditResult -Definitions $definitions
if ($Format -eq "Json") {
$jsonValue = if ($SummaryOnly) { $result.Summary } else { $result }
Write-Output ($jsonValue | ConvertTo-Json -Depth 6)
} else {
Write-Output "Database comment audit"
Write-Output "Repository: $RepositoryRoot"
Write-Output "Database/API access: none"
Write-Output ("Table definitions: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.TableDefinitions, $result.Summary.CommentedTables, $result.Summary.TableCoveragePercent)
Write-Output ("Columns: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.Columns, $result.Summary.CommentedColumns, $result.Summary.ColumnCoveragePercent)
Write-Output ("Issues: {0}" -f $result.Summary.Issues)
if (-not $SummaryOnly) {
foreach ($issue in $result.Issues) {
$target = if ([string]::IsNullOrWhiteSpace($issue.Column)) { $issue.Table } else { "$($issue.Table).$($issue.Column)" }
Write-Output ("[{0}] {1} ({2})" -f $issue.Code, $target, $issue.Source)
}
}
}
if ($FailOnIssues -and $result.Summary.Issues -gt 0) {
exit 1
}
exit 0
+30 -2
View File
@@ -11,8 +11,8 @@ $ErrorActionPreference = 'Stop'
$Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$Workspace = (Resolve-Path (Join-Path $Repo '..')).Path
$Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance'
$ExpectedSha256 = 'E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0'
$ExpectedLines = 7803
$ExpectedSha256 = '03234DF522426213FA64768FDF35182DC315F2E358E5FD993AA24A2E99936D63'
$ExpectedLines = 13775
if ($TargetHost -notin @('127.0.0.1', 'localhost')) {
[Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.')
@@ -32,6 +32,25 @@ if ($actual -ne $ExpectedSha256 -or $lines -ne $ExpectedLines) {
[Console]::Error.WriteLine("Unified SQL snapshot mismatch: $actual/$lines")
exit 2
}
$sqlText = [System.IO.File]::ReadAllText($InputPath)
$commentGateTokens = @(
'CREATE PROCEDURE queue_column_comment',
'CREATE PROCEDURE apply_one_table_comments',
'CALL apply_queued_comments();',
'字段注释批量升级改变了结构签名',
'存在缺失中文注释的业务表',
'存在缺失中文注释的业务字段'
)
foreach ($token in $commentGateTokens) {
if (-not $sqlText.Contains($token)) {
[Console]::Error.WriteLine("Unified SQL comment acceptance marker missing: $token")
exit 2
}
}
if ([regex]::Matches($sqlText, "SELECT 'PASS' AS migration_status").Count -ne 1) {
[Console]::Error.WriteLine('Unified SQL must contain exactly one final PASS statement')
exit 2
}
$sequence = @(
'empty: base.sql', 'replay: base.sql + input.sql + input.sql',
@@ -40,11 +59,20 @@ $sequence = @(
'overseas-missing-objects: fixture-overseas-missing-objects.sql',
'interrupted-recovery: fixture-interrupted.sql', 'final-structure: verify-final.sql')
Write-Output "SQL SHA-256: $actual / lines: $lines"
Write-Output 'Comment gate: queued validation, per-table ALTER, signature verification, nine-schema coverage, unique PASS'
Write-Output "Target: $TargetHost`:$Port"
Write-Output 'Fixture order:'
$sequence | ForEach-Object { Write-Output " $_" }
if ($DryRun) { Write-Output 'DRY-RUN PASS (mysql not started; no database writes)'; exit 0 }
$acceptanceInput = Join-Path $Acceptance 'input.sql'
Copy-Item -LiteralPath $InputPath -Destination $acceptanceInput -Force
$stagedHash = ((Get-FileHash -Algorithm SHA256 -LiteralPath $acceptanceInput).Hash).Trim().ToUpperInvariant()
if ($stagedHash -ne $actual) {
[Console]::Error.WriteLine("Failed to stage the validated unified SQL snapshot: $stagedHash")
exit 2
}
$run = 0
function Invoke-Stage([string]$Name, [string]$File) {
$script = Join-Path $Acceptance $File