完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
+307
View File
@@ -0,0 +1,307 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$jsonPath = Join-Path $root 'APP.openapi.json'
$yamlPath = Join-Path $root 'APP.openapi.yaml'
$document = Get-Content -Raw -Encoding UTF8 -LiteralPath $jsonPath | ConvertFrom-Json
$yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath $yamlPath
$helper = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'utils/generation-poem.js')
function Get-YamlBlock {
param(
[string]$Header,
[string]$NextHeaderPattern
)
$lines = @($yaml -split "`r?`n")
$start = [Array]::IndexOf($lines, $Header)
if ($start -lt 0) { throw "YAML block missing: $Header" }
$end = $lines.Count
for ($index = $start + 1; $index -lt $lines.Count; $index += 1) {
if ($lines[$index] -match $NextHeaderPattern) {
$end = $index
break
}
}
return ($lines[$start..($end - 1)] -join "`n")
}
function Get-YamlNestedBlock {
param(
[string]$Content,
[string]$Header,
[string]$NextHeaderPattern
)
$pattern = '(?ms)^' + [regex]::Escape($Header) + "`n(?<body>.*?)(?=$NextHeaderPattern|\z)"
$match = [regex]::Match($Content, $pattern)
if (-not $match.Success) { throw "YAML nested block missing: $Header" }
return $match.Value
}
foreach ($path in @(
'/genealogy/app/genealogies',
'/genealogy/app/genealogies/{genealogyId}',
'/genealogy/app/genealogies/{genealogyId}/join-applies',
'/genealogy/app/genealogies/{genealogyId}/join-applies/{applyId}/audit',
'/genealogy/app/genealogies/{genealogyId}/generation-poems',
'/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/preview',
'/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/save',
'/genealogy/app/genealogies/{genealogyId}/generation-poems/management'
)) {
if (-not $document.paths.PSObject.Properties[$path]) { throw "G-series OpenAPI path missing: $path" }
if (-not $yaml.Contains(" $path`:")) { throw "G-series YAML path missing: $path" }
}
# 创建、设置与访问预设从本轮起只由 g03-bootstrap-openapi-contract.ps1 管理.
# 旧测试不得继续锁定 GenealogyCreateBody 或 visibility/joinMode,否则会把后端的新单一合同误判为回归.
$joinApply = $document.components.schemas.GenealogyJoinApplyBody
foreach ($field in @('applicantName', 'phone', 'relationDesc', 'applyReason', 'inviterUserId')) {
if (-not $joinApply.properties.PSObject.Properties[$field]) {
throw "GenealogyJoinApplyBody field missing: $field"
}
}
$audit = $document.components.schemas.GenealogyJoinAuditBody
if ('status' -notin @($audit.required)) { throw 'GenealogyJoinAuditBody.status must remain required' }
foreach ($field in @('status', 'auditRemark')) {
if (-not $audit.properties.PSObject.Properties[$field]) {
throw "GenealogyJoinAuditBody field missing: $field"
}
}
$batch = $document.components.schemas.GenerationPoemBatchBody
$batchProperties = @($batch.properties.PSObject.Properties.Name | Sort-Object)
if (($batchProperties -join ',') -ne 'disableMissing,poemText') {
throw "GenerationPoemBatchBody fields drifted: $($batchProperties -join ',')"
}
$batchRequired = @($batch.required | Sort-Object)
if (($batchRequired -join ',') -ne 'poemText') {
throw "GenerationPoemBatchBody required fields drifted: $($batchRequired -join ',')"
}
if ($batch.properties.poemText.type -ne 'string') { throw 'GenerationPoemBatchBody.poemText must remain a string' }
if ($batch.properties.disableMissing.type -ne 'boolean') { throw 'GenerationPoemBatchBody.disableMissing must remain a boolean' }
if ($batch.properties.poemText.maxLength -ne 26000) { throw 'GenerationPoemBatchBody.poemText maxLength must be 26000' }
$poemDescription = [string]$batch.properties.poemText.description
foreach ($fact in @(
'\u5355\u4e2a\u5b57\u8f88\u6700\u591a50\u4e2a\u5b57\u7b26',
'\u4e00\u6b21\u6700\u591a500\u4e2a\u4e16\u4ee3',
'\u7a7a\u683c\u3001\u9017\u53f7\u3001\u5206\u53f7\u3001\u987f\u53f7\u3001\u659c\u6760\u6216\u7ad6\u7ebf'
)) {
if ($poemDescription -notmatch $fact) { throw "GenerationPoemBatchBody description missing: $fact" }
}
if ([string]$batch.properties.disableMissing.description -notmatch '\u4e0d\u4f1a\u5220\u9664\u5386\u53f2\u8bb0\u5f55') {
throw 'GenerationPoemBatchBody.disableMissing no longer guarantees history retention'
}
foreach ($path in @(
'/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/preview',
'/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/save'
)) {
$bodyRef = $document.paths.$path.post.requestBody.content.'application/json'.schema.'$ref'
if ($bodyRef -ne '#/components/schemas/GenerationPoemBatchBody') {
throw "Batch endpoint request body drifted: $path -> $bodyRef"
}
}
$poemView = $document.components.schemas.GenerationPoemView
$poemViewFields = @{
poemId = @{ Type = 'integer'; Format = 'int64' }
generationNo = @{ Type = 'integer'; Format = 'int64' }
generationText = @{ Type = 'string'; Format = $null }
status = @{ Type = 'string'; Format = $null }
}
foreach ($field in $poemViewFields.Keys) {
$property = $poemView.properties.PSObject.Properties[$field].Value
if (-not $property) { throw "GenerationPoemView field missing: $field" }
if ($property.type -ne $poemViewFields[$field].Type -or $property.format -ne $poemViewFields[$field].Format) {
throw "GenerationPoemView.$field type/format drifted"
}
}
if ([string]$poemView.properties.status.description -notmatch '0\u6b63\u5e38\uff0c1\u505c\u7528') {
throw 'GenerationPoemView status 0/1 contract drifted'
}
$normalList = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get
if ([string]$normalList.description -notmatch '\u53ef\u67e5\u770b\u5bb6\u8c31' -or [string]$normalList.description -notmatch '\u4ec5\u8fd4\u56de\u6b63\u5e38\u72b6\u6001') {
throw 'Normal generation-poem list capability or active-only projection drifted'
}
$managementList = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems/management'.get
if ([string]$managementList.description -notmatch '\u5185\u5bb9\u7f16\u8f91\u8005' -or [string]$managementList.description -notmatch '\u6b63\u5e38\u548c\u505c\u7528') {
throw 'Management generation-poem list capability or status projection drifted'
}
foreach ($operation in @($normalList, $managementList)) {
if ($operation.responses.'200'.'$ref' -ne '#/components/responses/GenerationPoemListResult') {
throw 'Generation-poem list operation no longer returns GenerationPoemListResult'
}
}
if ($document.components.responses.GenerationPoemListResult.content.'application/json'.schema.'$ref' -ne '#/components/schemas/RGenerationPoemList') {
throw 'GenerationPoemListResult wrapper drifted'
}
$poemListData = $document.components.schemas.RGenerationPoemList.properties.data
if ($poemListData.type -ne 'array' -or $poemListData.items.'$ref' -ne '#/components/schemas/GenerationPoemView') {
throw 'RGenerationPoemList.data no longer exposes GenerationPoemView items'
}
$previewPath = '/genealogy/app/genealogies/{genealogyId}/generation-poems/batch/preview'
if ($document.paths.$previewPath.post.responses.'200'.'$ref' -ne '#/components/responses/GenerationPoemBatchPreviewResult') {
throw 'Generation-poem preview response no longer uses GenerationPoemBatchPreviewResult'
}
if ($document.components.responses.GenerationPoemBatchPreviewResult.content.'application/json'.schema.'$ref' -ne '#/components/schemas/RGenerationPoemBatchPreview') {
throw 'GenerationPoemBatchPreviewResult wrapper drifted'
}
if ($document.components.schemas.RGenerationPoemBatchPreview.properties.data.'$ref' -ne '#/components/schemas/GenerationPoemBatchPreviewView') {
throw 'RGenerationPoemBatchPreview.data no longer exposes the preview view'
}
$previewView = $document.components.schemas.GenerationPoemBatchPreviewView
if ($previewView.properties.items.type -ne 'array' -or $previewView.properties.items.items.'$ref' -ne '#/components/schemas/GenerationPoemBatchItemView') {
throw 'GenerationPoemBatchPreviewView.items contract drifted'
}
$previewItem = $document.components.schemas.GenerationPoemBatchItemView
$previewItemExpected = @{
poemId = @{ Type = 'integer'; Format = 'int64' }
generationNo = @{ Type = 'integer'; Format = 'int64' }
oldGenerationText = @{ Type = 'string'; Format = $null }
newGenerationText = @{ Type = 'string'; Format = $null }
oldStatus = @{ Type = 'string'; Format = $null }
newStatus = @{ Type = 'string'; Format = $null }
action = @{ Type = 'string'; Format = $null }
warning = @{ Type = 'string'; Format = $null }
}
$previewItemFields = @($previewItem.properties.PSObject.Properties.Name | Sort-Object)
if (($previewItemFields -join ',') -ne (@($previewItemExpected.Keys | Sort-Object) -join ',')) {
throw "GenerationPoemBatchItemView fields drifted: $($previewItemFields -join ',')"
}
foreach ($field in $previewItemExpected.Keys) {
$property = $previewItem.properties.PSObject.Properties[$field].Value
if ($property.type -ne $previewItemExpected[$field].Type -or $property.format -ne $previewItemExpected[$field].Format) {
throw "GenerationPoemBatchItemView.$field type/format drifted"
}
}
if ([string]$previewItem.properties.action.description -notmatch 'create.*update.*keep.*disable') {
throw 'GenerationPoemBatchItemView.action dictionary drifted'
}
$yamlBatch = Get-YamlBlock ' GenerationPoemBatchBody:' '^ [A-Za-z0-9_]+:$'
if ($yamlBatch -notmatch '(?m)^ required:\n - poemText\n properties:$') {
throw 'YAML GenerationPoemBatchBody required fields drifted'
}
$yamlBatchFields = @([regex]::Matches($yamlBatch, '(?m)^ (?<field>[A-Za-z][A-Za-z0-9]*):$') | ForEach-Object { $_.Groups['field'].Value } | Sort-Object)
if (($yamlBatchFields -join ',') -ne 'disableMissing,poemText') {
throw "YAML GenerationPoemBatchBody fields drifted: $($yamlBatchFields -join ',')"
}
$yamlPoemText = Get-YamlNestedBlock $yamlBatch ' poemText:' '^ [A-Za-z0-9_]+:$'
foreach ($pattern in @('(?m)^ type: string$', '(?m)^ maxLength: 26000$', '\u5355\u4e2a\u5b57\u8f88\u6700\u591a50\u4e2a\u5b57\u7b26', '\u4e00\u6b21\u6700\u591a500\u4e2a\u4e16\u4ee3')) {
if ($yamlPoemText -notmatch $pattern) { throw "YAML GenerationPoemBatchBody.poemText drifted: $pattern" }
}
$yamlDisableMissing = Get-YamlNestedBlock $yamlBatch ' disableMissing:' '^ [A-Za-z0-9_]+:$'
foreach ($pattern in @('(?m)^ type: boolean$', '\u4e0d\u4f1a\u5220\u9664\u5386\u53f2\u8bb0\u5f55')) {
if ($yamlDisableMissing -notmatch $pattern) { throw "YAML GenerationPoemBatchBody.disableMissing drifted: $pattern" }
}
$yamlPoemView = Get-YamlBlock ' GenerationPoemView:' '^ [A-Za-z0-9_]+:$'
$yamlPoemViewExpected = @{
poemId = @{ Type = 'integer'; Format = 'int64' }
generationNo = @{ Type = 'integer'; Format = 'int64' }
generationText = @{ Type = 'string'; Format = $null }
status = @{ Type = 'string'; Format = $null }
}
foreach ($field in $yamlPoemViewExpected.Keys) {
$fieldBlock = Get-YamlNestedBlock $yamlPoemView " $field`:" '^ [A-Za-z0-9_]+:$'
if ($fieldBlock -notmatch "(?m)^ type: $($yamlPoemViewExpected[$field].Type)$") {
throw "YAML GenerationPoemView.$field type drifted"
}
$format = $yamlPoemViewExpected[$field].Format
if (($format -and $fieldBlock -notmatch "(?m)^ format: $format$") -or (-not $format -and $fieldBlock -match '(?m)^ format:')) {
throw "YAML GenerationPoemView.$field format drifted"
}
}
$yamlStatus = Get-YamlNestedBlock $yamlPoemView ' status:' '^ [A-Za-z0-9_]+:$'
if ($yamlStatus -notmatch 'description:.*0\u6b63\u5e38\uff0c1\u505c\u7528' -or
$yamlStatus -notmatch '(?m)^ type: string$') {
throw 'YAML GenerationPoemView.status contract drifted'
}
$yamlPreviewPath = Get-YamlBlock ' /genealogy/app/genealogies/{genealogyId}/generation-poems/batch/preview:' '^ /.*:$'
if ($yamlPreviewPath -notmatch [regex]::Escape("`$ref: '#/components/responses/GenerationPoemBatchPreviewResult'")) {
throw 'YAML generation-poem preview response drifted'
}
$yamlPreviewResponse = Get-YamlBlock ' GenerationPoemBatchPreviewResult:' '^ [A-Za-z0-9_]+:$'
if ($yamlPreviewResponse -notmatch [regex]::Escape("`$ref: '#/components/schemas/RGenerationPoemBatchPreview'")) {
throw 'YAML GenerationPoemBatchPreviewResult wrapper drifted'
}
$yamlPreviewWrapper = Get-YamlBlock ' RGenerationPoemBatchPreview:' '^ [A-Za-z0-9_]+:$'
if ($yamlPreviewWrapper -notmatch [regex]::Escape("`$ref: '#/components/schemas/GenerationPoemBatchPreviewView'")) {
throw 'YAML RGenerationPoemBatchPreview.data drifted'
}
$yamlPreviewView = Get-YamlBlock ' GenerationPoemBatchPreviewView:' '^ [A-Za-z0-9_]+:$'
$yamlPreviewItems = Get-YamlNestedBlock $yamlPreviewView ' items:' '^ [A-Za-z0-9_]+:$'
if ($yamlPreviewItems -notmatch '(?m)^ type: array$' -or $yamlPreviewItems -notmatch [regex]::Escape("`$ref: '#/components/schemas/GenerationPoemBatchItemView'")) {
throw 'YAML GenerationPoemBatchPreviewView.items drifted'
}
$yamlPreviewItem = Get-YamlBlock ' GenerationPoemBatchItemView:' '^ [A-Za-z0-9_]+:$'
$yamlPreviewItemFields = @([regex]::Matches($yamlPreviewItem, '(?m)^ (?<field>[A-Za-z][A-Za-z0-9]*):$') | ForEach-Object { $_.Groups['field'].Value } | Sort-Object)
if (($yamlPreviewItemFields -join ',') -ne (@($previewItemExpected.Keys | Sort-Object) -join ',')) {
throw "YAML GenerationPoemBatchItemView fields drifted: $($yamlPreviewItemFields -join ',')"
}
foreach ($field in $previewItemExpected.Keys) {
$fieldBlock = Get-YamlNestedBlock $yamlPreviewItem " $field`:" '^ [A-Za-z0-9_]+:$'
if ($fieldBlock -notmatch "(?m)^ type: $($previewItemExpected[$field].Type)$") {
throw "YAML GenerationPoemBatchItemView.$field type drifted"
}
$format = $previewItemExpected[$field].Format
if (($format -and $fieldBlock -notmatch "(?m)^ format: $format$") -or (-not $format -and $fieldBlock -match '(?m)^ format:')) {
throw "YAML GenerationPoemBatchItemView.$field format drifted"
}
}
$yamlPreviewAction = Get-YamlNestedBlock $yamlPreviewItem ' action:' '^ [A-Za-z0-9_]+:$'
if ($yamlPreviewAction -notmatch 'create.*update.*keep.*disable') {
throw 'YAML GenerationPoemBatchItemView.action dictionary drifted'
}
$yamlNormalList = Get-YamlBlock ' /genealogy/app/genealogies/{genealogyId}/generation-poems:' '^ /.*:$'
$yamlNormalGet = Get-YamlNestedBlock $yamlNormalList ' get:' '^ [a-z]+:$'
if ($yamlNormalGet -notmatch '\u53ef\u67e5\u770b\u5bb6\u8c31' -or $yamlNormalGet -notmatch '\u4ec5\u8fd4\u56de\u6b63\u5e38\u72b6\u6001' -or
$yamlNormalGet -notmatch [regex]::Escape("`$ref: '#/components/responses/GenerationPoemListResult'")) {
throw 'YAML normal generation-poem capability or projection drifted'
}
$yamlManagementList = Get-YamlBlock ' /genealogy/app/genealogies/{genealogyId}/generation-poems/management:' '^ /.*:$'
$yamlManagementGet = Get-YamlNestedBlock $yamlManagementList ' get:' '^ [a-z]+:$'
if ($yamlManagementGet -notmatch '\u5185\u5bb9\u7f16\u8f91\u8005' -or $yamlManagementGet -notmatch '\u6b63\u5e38\u548c\u505c\u7528' -or
$yamlManagementGet -notmatch [regex]::Escape("`$ref: '#/components/responses/GenerationPoemListResult'")) {
throw 'YAML management generation-poem capability or projection drifted'
}
$yamlPoemListResponse = Get-YamlBlock ' GenerationPoemListResult:' '^ [A-Za-z0-9_]+:$'
if ($yamlPoemListResponse -notmatch [regex]::Escape("`$ref: '#/components/schemas/RGenerationPoemList'")) {
throw 'YAML GenerationPoemListResult wrapper drifted'
}
$yamlPoemListWrapper = Get-YamlBlock ' RGenerationPoemList:' '^ [A-Za-z0-9_]+:$'
$yamlPoemListData = Get-YamlNestedBlock $yamlPoemListWrapper ' data:' '^ [A-Za-z0-9_]+:$'
if ($yamlPoemListData -notmatch '(?m)^ type: array$' -or
$yamlPoemListData -notmatch [regex]::Escape("`$ref: '#/components/schemas/GenerationPoemView'")) {
throw 'YAML RGenerationPoemList.data drifted'
}
$yamlPreviewPost = Get-YamlNestedBlock $yamlPreviewPath ' post:' '^ [a-z]+:$'
if ($yamlPreviewPost -notmatch [regex]::Escape("`$ref: '#/components/schemas/GenerationPoemBatchBody'")) {
throw 'YAML generation-poem preview request body drifted'
}
$yamlSavePath = Get-YamlBlock ' /genealogy/app/genealogies/{genealogyId}/generation-poems/batch/save:' '^ /.*:$'
$yamlSavePost = Get-YamlNestedBlock $yamlSavePath ' post:' '^ [a-z]+:$'
if ($yamlSavePost -notmatch [regex]::Escape("`$ref: '#/components/schemas/GenerationPoemBatchBody'")) {
throw 'YAML generation-poem save request body drifted'
}
foreach ($constant in @(
'MAX_GENERATION_COUNT = 500',
'MAX_GENERATION_TEXT_LENGTH = 50',
'MAX_GENERATION_POEM_INPUT_LENGTH = 26000',
"ACTIVE: '0'",
"DISABLED: '1'"
)) {
if (-not $helper.Contains($constant)) { throw "Generation poem helper is not synchronized with OpenAPI: $constant" }
}
foreach ($schemaName in @('GenealogyJoinApplyBody', 'GenealogyJoinAuditBody', 'GenerationPoemBatchBody', 'GenerationPoemView')) {
if (-not $yaml.Contains(" $schemaName`:")) { throw "G-series YAML schema missing: $schemaName" }
}
Write-Output 'G-SERIES-OPENAPI-CONTRACT PASS'