321 lines
15 KiB
PowerShell
321 lines
15 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$pagesPath = Join-Path $root 'pages.json'
|
|
$mappingPath = Join-Path $root 'docs/接口与页面映射总表.md'
|
|
|
|
if (-not (Test-Path -LiteralPath $mappingPath -PathType Leaf)) {
|
|
throw '缺少页面与接口关系的唯一总表:docs/接口与页面映射总表.md'
|
|
}
|
|
|
|
$pagesDocument = Get-Content -Raw -Encoding UTF8 -LiteralPath $pagesPath | ConvertFrom-Json
|
|
$registeredRoutes = @($pagesDocument.pages | ForEach-Object { $_.path })
|
|
$mappingLines = @(Get-Content -Encoding UTF8 -LiteralPath $mappingPath)
|
|
$mappingText = $mappingLines -join "`n"
|
|
|
|
# 第一步:只解析以正式页面编号开头的表格行,避免把正文中的示例路由误算成页面映射。
|
|
$routeRows = @(
|
|
foreach ($line in $mappingLines) {
|
|
if ($line -match '^\|\s*(?<code>[AGTFRNM]\d{2})\s*\|\s*[^|]+\|\s*`(?<route>pages/[^`]+)`\s*\|') {
|
|
$columns = @($line.Trim('|').Split('|') | ForEach-Object { $_.Trim() })
|
|
[pscustomobject]@{
|
|
Code = $Matches.code
|
|
Route = $Matches.route
|
|
Line = $line
|
|
Columns = $columns
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
if ($routeRows.Count -ne $registeredRoutes.Count) {
|
|
throw "页面映射行数与 pages.json 不一致:映射=$($routeRows.Count),注册=$($registeredRoutes.Count)"
|
|
}
|
|
|
|
# 第二步:每个活动页面必须完整填写业务目标、入口、流程结果、状态和接口业务域;只列编号和路由不算完成迁移。
|
|
foreach ($row in $routeRows) {
|
|
if ($row.Columns.Count -ne 9) {
|
|
throw "页面映射列数错误:$($row.Code) 应为 9 列,实际为 $($row.Columns.Count)"
|
|
}
|
|
foreach ($columnIndex in 1..7) {
|
|
if ([string]::IsNullOrWhiteSpace($row.Columns[$columnIndex])) {
|
|
throw "页面映射存在空业务列:$($row.Code) 第 $($columnIndex + 1) 列"
|
|
}
|
|
}
|
|
}
|
|
|
|
# 第三步:页面编号和路由都必须唯一;重复行会制造两个互相冲突的当前合同。
|
|
$duplicateCodes = @($routeRows | Group-Object Code | Where-Object Count -gt 1)
|
|
if ($duplicateCodes.Count -gt 0) {
|
|
throw "页面映射存在重复编号:$($duplicateCodes.Name -join ', ')"
|
|
}
|
|
|
|
$duplicateRoutes = @($routeRows | Group-Object Route | Where-Object Count -gt 1)
|
|
if ($duplicateRoutes.Count -gt 0) {
|
|
throw "页面映射存在重复路由:$($duplicateRoutes.Name -join ', ')"
|
|
}
|
|
|
|
# 页面编号必须由实际文件名语义得出,不能把正确路径和另一个页面编号错误拼接。
|
|
foreach ($row in $routeRows) {
|
|
if ($row.Route -notmatch '/(?<routeCode>[a-z]\d{2})-[^/]+$') {
|
|
throw "页面映射路由缺少文件名语义编号:$($row.Route)"
|
|
}
|
|
if ($Matches.routeCode.ToUpperInvariant() -ne $row.Code) {
|
|
throw "页面映射编号与路由不一致:编号=$($row.Code),路由=$($row.Route)"
|
|
}
|
|
}
|
|
|
|
# 第四步:总表顺序和路由集合必须与 pages.json 精确一致,新增或删除页面时必须同轮更新唯一总表。
|
|
$mappedRoutes = @($routeRows | ForEach-Object Route)
|
|
$missingRoutes = @($registeredRoutes | Where-Object { $_ -notin $mappedRoutes })
|
|
$staleRoutes = @($mappedRoutes | Where-Object { $_ -notin $registeredRoutes })
|
|
if ($missingRoutes.Count -gt 0 -or $staleRoutes.Count -gt 0) {
|
|
throw "页面映射与 pages.json 不相等:缺失=[$($missingRoutes -join ', ')];陈旧=[$($staleRoutes -join ', ')]"
|
|
}
|
|
|
|
for ($index = 0; $index -lt $registeredRoutes.Count; $index += 1) {
|
|
if ($mappedRoutes[$index] -ne $registeredRoutes[$index]) {
|
|
throw "页面映射顺序错误:第 $($index + 1) 行应为 $($registeredRoutes[$index]),实际为 $($mappedRoutes[$index])"
|
|
}
|
|
}
|
|
|
|
# 第五步:A04、G 系列第一轮、F 系列当前写边界和 T01 允许登记已有专项证据;
|
|
# 其余页面不能提前冒充完成 OpenAPI 审查。F 系列每页必须同时写出已核对事实和仍未启用的边界。
|
|
$authSeriesReviewPatterns = @{
|
|
A01 = '密码登录线上 wire.*默认 mock.*`validToken` 未由登录接口消费.*`API-AUTH-TAC-001`.*见 2\.10'
|
|
A04 = '`APP_REGISTER`、4 位码、`RAppLoginVo → AppLoginVo\.access_token` 客户端链已落地.*真实 challenge.*红灯.*见 2\.10'
|
|
A05 = '`APP_FORGOT_PASSWORD` 客户端链已落地.*真实后端、Android 可访问替代与 MuMu 仍红灯.*见 2\.10'
|
|
}
|
|
$gSeriesWorkspacePatterns = @{
|
|
G01 = 'Task26 工作区红灯已建立.*`/mine`.*尚未接远端.*Task29 unread-count 红灯.*M01.*见 2\.13/5\.6'
|
|
G03 = 'Task35 已建立 atomic bootstrap、无 PII operation-status、统一 accessPreset、APP 可信地区与词法 ID 的后端/客户端双红灯.*见 2\.8/5\.12'
|
|
G05 = 'Task26 选定 `/overview` 为唯一 owner.*对象级授权和错误语义未关闭.*见 2\.8/5\.3'
|
|
G11 = 'Task38 已建立唯一 merge PUT、settingsVersion/If-Match、409 three-way、待审串行化与 unknown 红灯.*见 2\.16/5\.15'
|
|
G12 = 'Task39 已建立唯一 GET/PUT、poemSetVersion/If-Match、完整候选集合、unknown 与 CORS owner 红灯.*页面尚未接远端.*见 2\.17/5\.16'
|
|
}
|
|
$gSeriesJoinPatterns = @{
|
|
G06 = 'Task36 搜索.*见 2\.14/5\.13.*Task37 六 operation 邀请红灯.*见 2\.15/5\.14'
|
|
G08 = 'Task36 后端红灯.*见 2\.14/5\.13.*Task37 明确邀请不得进入 G08'
|
|
G09 = 'mine 四状态、拒绝原因和撤回 CAS.*消息中心承诺待接线时删除'
|
|
G10 = 'Task36 固定 APPROVE/REJECT、PENDING CAS.*手机号节点待接线时原子删除'
|
|
}
|
|
$fSeriesReviewPatterns = @{
|
|
F01 = 'Task40 已建立唯一 cursor GET、无 PII 投影、逐页鉴权与非泄露 404 红灯.*后端绿前保持 fixture.*见 2\.18/5\.17'
|
|
F02 = '线上写请求要求 `feedContent`.*minLength=1.*`feedType/mediaOssIds/sortOrder/status`.*`WRITE_UNAVAILABLE`'
|
|
F03 = 'Task40 已拆分详情与一级评论 cursor owner.*评论写仍只做未提交预览.*见 2\.18/5\.17'
|
|
F04 = '任务 7 已核对线上列表及写接口.*唯一只读 owner'
|
|
F05 = '线上未证明收藏合同.*`articleTitle/articleContent`.*minLength=1'
|
|
F06 = '线上 POST/PUT 已存在.*`categoryId/coverOssId` 为 int64.*真实调用未'
|
|
F07 = '线上创建要求 `albumName`.*不伪增列表或计数'
|
|
F08 = '任务 7 已核对.*复合身份.*跨谱'
|
|
F09 = '旧 `/files/upload` 已下线.*resumable init/chunk/complete.*complete 返回.*`ossId` 为 string.*照片创建要求 int64.*后端消除类型冲突前不得转换或提交'
|
|
F10 = '任务 7 未发现.*保持关闭'
|
|
}
|
|
$rSeriesReviewPatterns = @{
|
|
R01 = '任务 8 已核对.*lineage/persons/page.*词法字符串'
|
|
R02 = '任务 8 已核对.*AppLineagePersonBody.*`name` 必填.*真实写入未启用'
|
|
R03 = '任务 8 已核对.*relative-records.*不是.*ceremony gifts.*方向语义'
|
|
R04 = '任务 8 已核对.*`relativeId`.*`relativeName` 必填.*本地预览'
|
|
R05 = '任务 8 已核对.*ceremonies.*`ceremonyTitle/ceremonyType` 必填.*无正式枚举'
|
|
R06 = '任务 8 已核对.*invitations.*成员选项.*参与者'
|
|
R07 = '任务 8 已核对.*`ceremonyId`.*真实写入未启用'
|
|
R08 = '任务 8 已核对.*growth-records.*`lineagePersonId`.*`recordType` 无枚举'
|
|
R09 = '任务 8 已核对.*没有独立人生事件接口.*硬关闭'
|
|
R10 = '任务 8 已核对.*memos.*`memoTitle` 必填.*`completed` 无枚举'
|
|
R11 = '任务 8 已核对.*merit-records.*`donorName/meritTitle` 必填.*无枚举'
|
|
}
|
|
$nmSeriesReviewPatterns = @{
|
|
N01 = 'Task29 读取红灯已建立.*完整活动集合.*纯文本.*计数同域.*见 2\.13/5\.6.*Task30 前不得本地伪写'
|
|
N02 = 'Task29 选定 list-owned snapshot.*删除目标 CTA.*Task30.*私有字符串 ID.*见 2\.13/5\.6/5\.7'
|
|
M01 = 'Task28 profile GET 红灯.*身份失败不锁菜单.*见 2\.13/5\.5.*Task29 unread-count 红灯.*fixture 伪数.*见 2\.13/5\.6'
|
|
M02 = 'Task28 读取红灯仍是前置.*Task31 已建立 PUT merge.*If-Match.*typed response.*409 红灯.*头像不混入.*见 2\.13/5\.5/5\.8'
|
|
M03 = 'Task28 已建立 profile GET 红灯.*普通读取失败不得阻断密码入口.*设备状态仍无合同.*见 2\.13/5\.5'
|
|
M04 = 'Task33 已建立 raw writeOnly、ALL 会话撤销、typed 错误与 session marker 红灯.*四条密码 wire 必须同批迁移.*见 2\.13/5\.10'
|
|
M05 = 'Task34 已建立专用 SaToken 发码、全活动六位码、ALL 会话撤销、outbox 与 credential marker 红灯.*依赖 M04 raw wire.*见 2\.13/5\.11'
|
|
M06 = 'Task27 选定列表唯一 owner 并建立红灯.*专用 required、纯文本、仅发布、顺序和认证语义待后端关闭.*见 2\.13/5\.4'
|
|
M07 = 'Task25 已接 `POST /genealogy/app/feedback` 严格客户端.*remote 实测与 MuMu 待认证门禁关闭.*见 2\.13'
|
|
M08 = 'Task37 六 operation 邀请红灯.*见 2\.15/5\.14.*通过前保持不可用且不展示假码'
|
|
M09 = '套餐/订单端点存在但支付闭环未定义.*删除查询参数演示订单.*见 2\.13'
|
|
M10 = 'Task9 已完成本机清理基线.*Task32 已建立当前凭证族、幂等 200、logoutCoordinator、required RVoid 与部署复用红灯.*见 2\.13/5\.9'
|
|
}
|
|
$prematureRows = @(
|
|
$routeRows | Where-Object {
|
|
if ($authSeriesReviewPatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $authSeriesReviewPatterns[$_.Code] }
|
|
if ($_.Code -eq 'T01') { return $_.Line -notmatch '专项已核对:现有递归 `LineagePersonTreeView` 不满足' }
|
|
if ($gSeriesWorkspacePatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $gSeriesWorkspacePatterns[$_.Code] }
|
|
if ($gSeriesJoinPatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $gSeriesJoinPatterns[$_.Code] }
|
|
if ($fSeriesReviewPatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $fSeriesReviewPatterns[$_.Code] }
|
|
if ($rSeriesReviewPatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $rSeriesReviewPatterns[$_.Code] }
|
|
if ($nmSeriesReviewPatterns.ContainsKey($_.Code)) { return $_.Line -notmatch $nmSeriesReviewPatterns[$_.Code] }
|
|
return $_.Line -notmatch '待对应业务阶段 OpenAPI 审查'
|
|
}
|
|
)
|
|
if ($prematureRows.Count -gt 0) {
|
|
throw "存在没有准确表达接口审查状态的活动页面:$($prematureRows.Code -join ', ')"
|
|
}
|
|
|
|
foreach ($heading in @(
|
|
'## 一、权威边界',
|
|
'## 二、全局业务与交互合同',
|
|
'## 三、52 个活动页面映射',
|
|
'## 四、封存与已移除页面',
|
|
'## 五、后续接口审查填充规则'
|
|
)) {
|
|
if (-not $mappingText.Contains($heading)) {
|
|
throw "接口与页面映射总表缺少章节:$heading"
|
|
}
|
|
}
|
|
|
|
# 第六步:锁定旧资料中仍然有效的关键业务事实,避免未来把表格保留却悄悄清空真正的产品合同。
|
|
foreach ($requiredFact in @(
|
|
'15—64 Unicode code point',
|
|
'无组成规则',
|
|
'SmsCodeSecret',
|
|
'恰好 6 位 ASCII 数字',
|
|
'/genealogy/app/auth/phone/sms/code',
|
|
'PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED',
|
|
'API-PHONE-001',
|
|
'STEP_UP_UNAVAILABLE',
|
|
'AppGenealogyBootstrapBody',
|
|
'GenealogyBootstrapOperationKey',
|
|
'GenealogyAccessPreset',
|
|
'GenealogyRegionCode',
|
|
'FAILED_NO_COMMIT',
|
|
'G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED',
|
|
'G03-BOOTSTRAP-CLIENT-RELEASE BLOCKED',
|
|
'/genealogy/app/region/search',
|
|
'GET 必须纯读',
|
|
'receipt→mine cache→context→G05',
|
|
'API-G03-001',
|
|
'API-G03-005',
|
|
'AppGenealogySettingsUpdateBody',
|
|
'GenealogySettingsVersion',
|
|
'G11-SETTINGS-OPENAPI-CONTRACT BLOCKED',
|
|
'API-SETTINGS-001',
|
|
'API-SETTINGS-006',
|
|
'ACTIVE_PENDING_APPLICATIONS',
|
|
'ATOMIC_SINGLE_WINNER',
|
|
'NON_DISCLOSING_GENEALOGY_NOT_AVAILABLE',
|
|
'OMIT_ONLY_AFTER_CONFIRMED_ACCESS_LOSS',
|
|
'canonical 设置实际变化',
|
|
'内部 LF',
|
|
'控制字符',
|
|
'exact local',
|
|
'旁路详情',
|
|
'nullable',
|
|
'真实 JSON 数组',
|
|
'允许非语义 tracing header',
|
|
'禁止 ETag',
|
|
'traceparent/tracestate/x-request-id/x-correlation-id',
|
|
'内联 Header Object',
|
|
'非 null 的 string schema',
|
|
'readOnly/writeOnly',
|
|
'allOf',
|
|
'根层',
|
|
'PUBLIC_APPLY→MEMBER_ONLY',
|
|
'AppGenerationPoemSetUpdateBody',
|
|
'GenerationPoemSetSnapshot',
|
|
'GenerationPoemSetVersion',
|
|
'G12-GENERATION-POEM-OPENAPI-CONTRACT BLOCKED',
|
|
'API-POEM-001',
|
|
'API-POEM-006',
|
|
'APP_GATEWAY_PREFLIGHT',
|
|
'VALIDATE_FINAL_ACTIVE_CAPACITY',
|
|
'FRESH_GET_THREE_WAY_NO_AUTO_PUT',
|
|
'FAMILY-FEED-READ-OPENAPI-CONTRACT BLOCKED',
|
|
'FAMILY-FEED-READ-OPENAPI-ADVERSARIAL-CONTRACT PASS MUTANTS=58',
|
|
'API-FEED-READ-001',
|
|
'API-FEED-READ-006',
|
|
'appListFamilyFeeds',
|
|
'appGetFamilyFeed',
|
|
'appListFamilyFeedRootComments',
|
|
'AppFamilyFeedReadItem',
|
|
'AppFamilyFeedRootCommentReadItem',
|
|
'UPPER_BOUND_KEYSET_LATEST_VISIBLE',
|
|
'FAMILY_FEED_NOT_AVAILABLE',
|
|
'hasMedia',
|
|
'A01、A04、A05 已接入同一个 `TacVerification`',
|
|
'`APP_SMS_LOGIN`',
|
|
'`APP_REGISTER`',
|
|
'`APP_FORGOT_PASSWORD`',
|
|
'开发/联调 wire 已按线上',
|
|
'`validToken` 未进入或被服务端消费',
|
|
'不能把本地滑动成功冒充服务端验证',
|
|
'聚焦密码框',
|
|
'审核中记录只展示进度和“撤回申请”',
|
|
'被拒绝记录展示原因和“修改后重新提交”',
|
|
'仅成员可见',
|
|
'公开可申请',
|
|
'LineageGraphWindow',
|
|
'EMPTY/POPULATED',
|
|
'entryPersonIds',
|
|
'genealogyRootPersonIds',
|
|
'redactedGenealogyRootCount',
|
|
'rootVisibility',
|
|
'pathCompleteness',
|
|
'ancestorPathSegments',
|
|
'anchorPersonId',
|
|
'schemaVersion',
|
|
'returnedNodeCount',
|
|
'partnerRelationship',
|
|
'relationshipKind',
|
|
'parentRole',
|
|
'LINEAGE_QUERY_INVALID',
|
|
'LINEAGE_FOCUS_NOT_AVAILABLE',
|
|
'RELATIONSHIP_PATCH_EMPTY',
|
|
'sceneVersion',
|
|
'/genealogy/app/v2/',
|
|
'API-T01-001',
|
|
'单个原生页面实例',
|
|
'注册成功响应为 `RAppLoginVo`',
|
|
'`AppLoginVo.access_token`',
|
|
'aria-describedby',
|
|
'邀请码直接加入且不生成审核记录',
|
|
'appApi',
|
|
'500',
|
|
'20',
|
|
'100ms',
|
|
'44dp',
|
|
'导航栈语义统一',
|
|
'跨页面领域数据持久化',
|
|
'全局文字层级和无障碍第二轮',
|
|
'A02 账号登录',
|
|
'A06 登录状态',
|
|
'G02',
|
|
'G04',
|
|
'G07',
|
|
'T02'
|
|
)) {
|
|
if (-not $mappingText.Contains($requiredFact)) {
|
|
throw "接口与页面映射总表缺少关键业务事实:$requiredFact"
|
|
}
|
|
}
|
|
|
|
if ($mappingText -match '汤正华堂侄') {
|
|
throw '当前产品文档不得使用具体姓名示例,应改为“某某某堂侄”等通用占位表达'
|
|
}
|
|
|
|
$localRouteBoundary = [regex]::Match(
|
|
$mappingText,
|
|
'(?s)### 2\.6 当前本地数据与路由参数边界.*?(?=### 2\.7)'
|
|
).Value
|
|
$localRouteTable = [regex]::Match($localRouteBoundary, '(?s)\| 页面 \|.*?\n\n').Value
|
|
foreach ($retiredRParameter in @('`count`', '`saveResult`', '`giftId`', '`ritualId`', '`personName`')) {
|
|
if ($localRouteTable.Contains($retiredRParameter)) {
|
|
throw "R 系列本地路由边界仍登记已退役参数:$retiredRParameter"
|
|
}
|
|
}
|
|
foreach ($currentRContract in @(
|
|
'| R01 | `genealogyId`、`state` |',
|
|
'| R04 | `genealogyId`、`mode`、`relativeId`、`state` |',
|
|
'| R07 | `genealogyId`、`mode`、`ceremonyId`、`state` |',
|
|
'| R08、R09 | `genealogyId`、`personId`、`state` |',
|
|
'| R10、R11 | `genealogyId`、`state` |'
|
|
)) {
|
|
if (-not $localRouteTable.Contains($currentRContract)) {
|
|
throw "R 系列本地路由边界缺少当前复合身份合同:$currentRContract"
|
|
}
|
|
}
|
|
|
|
Write-Output "INTERFACE-PAGE-MAPPING-CONTRACT PASS ROUTES=$($routeRows.Count)"
|