接口开始5%
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$currentDocuments = @(
|
||||
'docs/项目当前总览.md',
|
||||
'docs/家谱项目全量治理设计.md',
|
||||
'docs/家谱项目全量治理实施计划.md',
|
||||
'docs/视觉资产与构建基线.md',
|
||||
'docs/接口与页面映射总表.md'
|
||||
)
|
||||
|
||||
$combined = ''
|
||||
$documentContents = @{}
|
||||
foreach ($relativePath in $currentDocuments) {
|
||||
$absolutePath = Join-Path $root $relativePath
|
||||
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
|
||||
throw "缺少当前权威文档:$relativePath"
|
||||
}
|
||||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $absolutePath
|
||||
$documentContents[$relativePath] = $content
|
||||
$combined += "`n" + $content
|
||||
}
|
||||
|
||||
foreach ($token in @('TO' + 'DO', 'TB' + 'D', 'FIX' + 'ME', 'PLACE' + 'HOLDER')) {
|
||||
if ($combined -match "(?i)\b$([regex]::Escape($token))\b") {
|
||||
throw "当前权威文档仍含未完成占位标记:$token"
|
||||
}
|
||||
}
|
||||
|
||||
# 当前进度只由项目总览表述;其余权威文档不得把已完成的阶段 0 或已收口的书面设计写成进行中。
|
||||
$designDocument = $documentContents['docs/家谱项目全量治理设计.md']
|
||||
$planDocument = $documentContents['docs/家谱项目全量治理实施计划.md']
|
||||
$mappingDocument = $documentContents['docs/接口与页面映射总表.md']
|
||||
$overviewDocument = $documentContents['docs/项目当前总览.md']
|
||||
if ($designDocument -match '阶段 0 正在执行') {
|
||||
throw '治理设计仍把已完成的阶段 0 写成进行中'
|
||||
}
|
||||
if ($mappingDocument -match '当前阶段:阶段 0——迁移现行页面与业务事实') {
|
||||
throw '接口与页面映射总表仍把阶段 0 迁移写成当前任务'
|
||||
}
|
||||
if ($mappingDocument -notmatch '阶段 0 已完成;导航栈与 T01 专项设计已完成三人终审,业务代码尚未实施') {
|
||||
throw '接口与页面映射总表缺少阶段 0 完成、书面设计收口及业务代码未实施的准确边界'
|
||||
}
|
||||
if ($overviewDocument -notmatch '下一步从导航失败测试开始') {
|
||||
throw '项目总览没有写明当前精确实施入口'
|
||||
}
|
||||
if ($overviewDocument -notmatch 'API-T01-001') {
|
||||
throw '项目总览没有登记 T01 后端接口门禁'
|
||||
}
|
||||
if ($overviewDocument -match '当前 MuMu[^\r\n]*在线') {
|
||||
throw '项目总览不得把易漂移的 MuMu 在线状态写成长期当前事实'
|
||||
}
|
||||
if ($overviewDocument -notmatch '阶段 0 验收时的 MuMu 证据') {
|
||||
throw '项目总览缺少带时间边界的 MuMu 验收证据'
|
||||
}
|
||||
|
||||
# 导航设计已经三人终审,文档合同必须保护其所有权、根语义、公开方法和阶段边界,不能只检查“有一份计划”。
|
||||
foreach ($owner in @('utils/navigation-routes.js', 'utils/navigation.js')) {
|
||||
if ($designDocument -notmatch [regex]::Escape($owner) -or $planDocument -notmatch [regex]::Escape($owner)) {
|
||||
throw "导航设计或实施计划缺少唯一所有者:$owner"
|
||||
}
|
||||
}
|
||||
foreach ($semanticMethod in @('openPage', 'replaceStep', 'goBack', 'returnTo', 'finishPage', 'goRoot')) {
|
||||
if ($designDocument -notmatch "(?m)^$([regex]::Escape($semanticMethod))\(") {
|
||||
throw "导航设计缺少公开语义方法:$semanticMethod"
|
||||
}
|
||||
}
|
||||
if ($planDocument -notmatch 'ROOT_ROUTE_KEYS = Object\.freeze\(\["A01", "G01", "F01", "M01"\]\)') {
|
||||
throw '实施计划没有把四个根语义精确锁定为 A01/G01/F01/M01'
|
||||
}
|
||||
if ($planDocument -match 'ROOT_ROUTE_KEYS[^\r\n]*N01') {
|
||||
throw 'N01 不是根页,不得进入 ROOT_ROUTE_KEYS'
|
||||
}
|
||||
if ($designDocument -notmatch 'G01、F01、M01.*自定义 `AppTabbar`' -or $designDocument -notmatch 'A01 是认证根页') {
|
||||
throw '治理设计没有区分三个业务根页与 A01 认证根页'
|
||||
}
|
||||
if ($designDocument -notmatch '字段键精确为 `operation、entityId(可选)、refresh`' -or $designDocument -notmatch '不允许加入 `treeVersion、genealogyId、focusId、payload`') {
|
||||
throw '一次性导航结果没有保持 operation/entityId/refresh 的唯一形状'
|
||||
}
|
||||
if ($designDocument -notmatch 'T03 原生页面实例只保留一个' -or $designDocument -notmatch '轨迹初始化为恰好 `\[initialPersonId\]`') {
|
||||
throw '治理设计缺少 T03 单实例与初始成员轨迹合同'
|
||||
}
|
||||
if ($planDocument -notmatch '导航阶段允许只迁移 T01 的现有导航调用' -or $planDocument -notmatch '导航门禁通过前,不得实施 T01 新图合同') {
|
||||
throw '导航与 T01 的阶段隔离边界不准确'
|
||||
}
|
||||
if ($planDocument -notmatch 'T03_CONTEXT_CONFLICT') {
|
||||
throw '实施计划缺少 T03 跨家谱上下文冲突规则'
|
||||
}
|
||||
if ($mappingDocument -match '将在阶段 1.*确定' -or $mappingDocument -match '阶段 1 依赖') {
|
||||
throw '接口映射总表仍把已收口导航或未来业务错误绑定为阶段 1 待定项'
|
||||
}
|
||||
|
||||
# A04 已有精确 OpenAPI 证据:注册成功必须建立会话并进入 G01,不能退回并行的“注册后再登录”终点。
|
||||
if ($mappingDocument -notmatch '成功建立登录态并进入 G01' -or $mappingDocument -notmatch '注册成功响应为 `LoginResult`') {
|
||||
throw 'A04 文档没有同时锁定 LoginResult 与建立登录态后进入 G01'
|
||||
}
|
||||
if ($designDocument -notmatch '正式终点不是“注册后再登录”' -or $planDocument -notmatch '不能先回 A01 再让用户重复登录') {
|
||||
throw '设计或计划没有明确排除注册后再次登录的并行终点'
|
||||
}
|
||||
|
||||
# T01 后端合同、固定 MuMu 配置和人工验收规模已经收口;旧字段只能出现在“必须拒绝”的文字中,不能再出现在目标 JSON 形状。
|
||||
foreach ($canonicalField in @('schemaVersion', 'treeVersion', 'state', 'entryPersonIds', 'genealogyRootPersonIds', 'redactedGenealogyRootCount', 'rootVisibility', 'pathCompleteness', 'ancestorPathSegments', 'returnedNodeCount', 'partnerRelationship', 'relationshipKind', 'parentRole', 'LINEAGE_QUERY_INVALID', 'LINEAGE_FOCUS_NOT_AVAILABLE', 'RELATIONSHIP_PATCH_EMPTY', 'projectLineageGraph', 'sceneVersion')) {
|
||||
if ($designDocument -notmatch [regex]::Escape($canonicalField) -and $planDocument -notmatch [regex]::Escape($canonicalField)) {
|
||||
throw "T01 设计与计划缺少规范字段或阶段:$canonicalField"
|
||||
}
|
||||
}
|
||||
if ($planDocument -match '"partnerIds"\s*:' -or $planDocument -match '"rootPersonIds"\s*:' -or $planDocument -match '"rootReason"\s*:' -or $planDocument -match '"schema"\s*:' -or $planDocument -match '"tree"\s*:') {
|
||||
throw 'T01 目标 JSON 示例仍在定义已经废弃的旧字段'
|
||||
}
|
||||
foreach ($emptyContract in @('"state": "EMPTY"', '"focusPersonId": null', '"entryPersonIds": []', '"generationRange": null', '合法的 `state=EMPTY` 空谱', '`state=POPULATED`')) {
|
||||
if (-not $planDocument.Contains($emptyContract)) {
|
||||
throw "T01 计划缺少 EMPTY/POPULATED 判别合同:$emptyContract"
|
||||
}
|
||||
}
|
||||
if ($planDocument -notmatch 'POPULATED 分支要求 nodes 非空、`focusPersonId` 引用其中一个 VISIBLE 节点') {
|
||||
throw 'T01 POPULATED 仍允许把 REDACTED 节点作为焦点'
|
||||
}
|
||||
if ($designDocument -match '无主根时显示合同异常' -or $designDocument -notmatch '只有 `state=POPULATED` 缺少有效焦点或窗口入口时才显示合同异常') {
|
||||
throw 'T01 仍把合法 EMPTY 误判为无主根合同异常'
|
||||
}
|
||||
foreach ($entryInvariant in @('没有 primary 入边的节点集合', '每个入口在当前窗口恰有零条 primary 入边', '每个非入口恰有一条 primary 入边', '`entryPersonIds` 引用不存在人物', '列表内人物 `entryReason=null`', '列表外人物 `entryReason!=null`', '入口仍有 primary 入边')) {
|
||||
if (-not $planDocument.Contains($entryInvariant) -and -not $designDocument.Contains($entryInvariant)) {
|
||||
throw "T01 窗口入口与 primary 森林不变量缺失:$entryInvariant"
|
||||
}
|
||||
}
|
||||
if ($planDocument -notmatch 'VisibleLineagePerson.*精确包含' -or $planDocument -notmatch 'RedactedLineagePerson.*只允许' -or $planDocument -notmatch '不得用于 FOCUS、locator、搜索或写接口') {
|
||||
throw 'T01 人物模型没有按 visibility 严格区分可见字段与匿名字段'
|
||||
}
|
||||
if ($planDocument -notmatch 'REDACTED 节点只显示通用隐私说明' -or $planDocument -notmatch 'focusPersonId.*targetPersonId.*稳定 VISIBLE 人物 ID' -or $planDocument -notmatch 'focusable=false、targetPersonId=null') {
|
||||
throw 'T01 仍可能把 REDACTED opaque ID 用作焦点或聚合跳转目标'
|
||||
}
|
||||
if ($planDocument -notmatch 'redacted:\{treeVersion\}:\{token\}' -or $planDocument -notmatch '不得以 `redacted:` 开头的稳定人物 ID') {
|
||||
throw 'T01 没有提供可执行的稳定人物 ID 与匿名 ID 边界'
|
||||
}
|
||||
if ($planDocument -notmatch 'protectedPathPersonIds/protectedPathFamilyUnitIds' -or $planDocument -notmatch '任意时刻每个家庭的真实子女上限始终为 5') {
|
||||
throw 'T01 投影没有同时保护焦点祖先路径和固定五人渲染上限'
|
||||
}
|
||||
if ($planDocument -notmatch 'relationshipKind.*discriminator.*oneOf' -or $planDocument -notmatch 'PARTNER 分支只允许 `relationType/status`') {
|
||||
throw 'T01 关系修改没有形成按不可变 relationshipKind 区分的唯一请求合同'
|
||||
}
|
||||
if ($planDocument -notmatch '至少提交一个可修改字段' -or $planDocument -notmatch '未提交的可修改字段保持原值' -or $planDocument -notmatch '422 RELATIONSHIP_PATCH_EMPTY') {
|
||||
throw 'T01 关系 PATCH 没有拒绝空更新或没有定义省略字段语义'
|
||||
}
|
||||
foreach ($enumContract in @('EMPTY/POPULATED', 'MALE/FEMALE/UNKNOWN', 'VISIBLE/REDACTED', 'GENEALOGY_ROOT/WINDOW_CUT/DISCONNECTED_COMPONENT', 'ANCHOR/PARTNER', 'MARRIAGE/PARTNERSHIP/UNKNOWN', 'ACTIVE/ENDED/UNKNOWN')) {
|
||||
if ($planDocument -notmatch [regex]::Escape($enumContract)) {
|
||||
throw "T01 OpenAPI 计划缺少完整枚举:$enumContract"
|
||||
}
|
||||
}
|
||||
if ($planDocument -notmatch '规范化最坏复杂度明确为 `O\(N log N \+ E\)`' -or $planDocument -notmatch '投影只接受已经规范化且校验通过的图' -or $planDocument -notmatch '投影阶段保持 `O\(N\+E\)`') {
|
||||
throw 'T01 规范化排序与线性投影的复杂度边界不清晰'
|
||||
}
|
||||
if (-not $planDocument.Contains('Scene 根对象精确为 `{ sceneVersion, treeVersion, focusPersonId, bounds, items }`') -or $planDocument -notmatch '相同 `sceneVersion` 对应不同序列化 payload.*失败' -or $planDocument -notmatch 'scene\.sceneVersion') {
|
||||
throw 'T01 Scene 根模型、版本所有权或版本碰撞规则不完整'
|
||||
}
|
||||
if ($planDocument -notmatch '瞬时 `selectedId` 不属于 Scene payload' -or $planDocument -notmatch '只改变 `selectedId` 时 Scene payload 与 `scene\.sceneVersion` 不变') {
|
||||
throw 'T01 瞬时选中态仍可能制造同 sceneVersion 的不同 Scene payload'
|
||||
}
|
||||
foreach ($overviewInvariant in @('overview` 只接受必填查询参数 `treeVersion`', 'genealogyPersonCount=0、genealogyRootPersonIds=[]、redactedGenealogyRootCount=0、generationRange=null、buckets=[]', 'genealogyRootPersonIds.length + redactedGenealogyRootCount >= 1', '三类计数总和等于 `genealogyPersonCount`', '隐私根只计入 `redactedGenealogyRootCount`', 'getLineageOverview(genealogyId, treeVersion)')) {
|
||||
if (-not $planDocument.Contains($overviewInvariant) -and -not $designDocument.Contains($overviewInvariant)) {
|
||||
throw "T01 全谱概览的版本、空谱、计数或隐私根不变量缺失:$overviewInvariant"
|
||||
}
|
||||
}
|
||||
foreach ($locatorInvariant in @('根可见性与路径完整性独立校验', 'pathCompleteness=REDACTED_GAPS', 'ancestorPathSegments', '可见根+REDACTED_GAPS', '隐私根+REDACTED_GAPS', '可见根路径不从 `rootPersonId` 开始', 'REDACTED 段含人物 ID', 'validateLineageLocator')) {
|
||||
if (-not $planDocument.Contains($locatorInvariant) -and -not $designDocument.Contains($locatorInvariant)) {
|
||||
throw "T01 locator 的隐私根或路径完整性分支缺失:$locatorInvariant"
|
||||
}
|
||||
}
|
||||
foreach ($runtimeSemantic in @('tests/lineage-overview-locator-runtime-smoke.js', 'validateLineageOverview', 'bucket 三类计数与 `genealogyPersonCount` 不等', '隐私根 POPULATED', '响应 `treeVersion` 与请求不一致')) {
|
||||
if (-not $planDocument.Contains($runtimeSemantic)) {
|
||||
throw "T01 overview 跨字段语义缺少运行时负例门禁:$runtimeSemantic"
|
||||
}
|
||||
}
|
||||
if ($designDocument -match '焦点、版本和展开状态' -or $planDocument -match '共享焦点、版本、展开状态') {
|
||||
throw 'T01 文档仍残留已删除的页内聚合展开状态'
|
||||
}
|
||||
if ($planDocument -notmatch '/genealogy/app/v2/genealogies/' -or $planDocument -match '若旧 `/lineage/tree`') {
|
||||
throw 'T01 后端路径没有固定为明确 v2 合同'
|
||||
}
|
||||
if ($planDocument -notmatch 'emulator-5554 / 720×1280 / 320dpi' -or $designDocument -notmatch '10、50、100、300 代') {
|
||||
throw 'T01 文档没有锁定当前 MuMu 配置和 10/50/100/300 代人工定位矩阵'
|
||||
}
|
||||
|
||||
$retiredEntrypoints = @(
|
||||
'design-pipeline/manifests/a01.json',
|
||||
'design-pipeline/manifests/a01-scroll-skins-v3.json',
|
||||
'design-pipeline/scripts/build-a01.mjs',
|
||||
'tests/a01-code-pipeline-contract.ps1',
|
||||
'docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd',
|
||||
'build:a01',
|
||||
'validate:auth-runtime-assets',
|
||||
'design-pipeline/manifests/g01-background-candidates.json',
|
||||
'design-pipeline/manifests/module-page-backgrounds.json',
|
||||
'design-pipeline/scripts/build-module-page-backgrounds.mjs',
|
||||
'design-pipeline/scripts/build_module_page_backgrounds.py',
|
||||
'design-pipeline/scripts/build-g01-empty-frame.mjs',
|
||||
'design-pipeline/tests/test_build_g01_backgrounds.py',
|
||||
'build:g01-background-candidates',
|
||||
'build:module-page-backgrounds',
|
||||
'build:g01-empty-frame'
|
||||
)
|
||||
foreach ($entrypoint in $retiredEntrypoints) {
|
||||
if ($combined.Contains($entrypoint)) {
|
||||
throw "当前权威文档仍引用已退役入口:$entrypoint"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($owner in @(
|
||||
'design-pipeline/manifests/runtime-assets.json',
|
||||
'design-pipeline/manifests/application-runtime-assets.json',
|
||||
'design-pipeline/manifests/shared-scroll-skins-v3.json',
|
||||
'design-pipeline/manifests/page-backgrounds-v3.json',
|
||||
'design-pipeline/manifests/g01-state-frame-v3.json',
|
||||
'tests/runtime-assets-contract.ps1',
|
||||
'tests/retired-asset-removal-contract.ps1',
|
||||
'tests/mumu-visual-acceptance-boundary-contract.ps1',
|
||||
'npm.cmd run validate:runtime-assets',
|
||||
'npm.cmd run build:page-backgrounds',
|
||||
'npm.cmd run build:g01-state-frame',
|
||||
'docs/接口与页面映射总表.md'
|
||||
)) {
|
||||
if (-not $combined.Contains($owner)) {
|
||||
throw "当前权威文档缺少现行所有者:$owner"
|
||||
}
|
||||
}
|
||||
|
||||
# 当前文档写出的正式 owner 与验证入口必须真实存在;文档不得指向已删除脚本制造“可恢复”的假象。
|
||||
foreach ($relativePath in @(
|
||||
'components/GenealogyPageBackground.vue',
|
||||
'components/ModulePageBackground.vue',
|
||||
'design-pipeline/manifests/application-runtime-assets.json',
|
||||
'design-pipeline/manifests/page-backgrounds-v3.json',
|
||||
'design-pipeline/manifests/g01-state-frame-v3.json',
|
||||
'design-pipeline/scripts/build-raster-assets.mjs',
|
||||
'design-pipeline/scripts/build_raster_assets.py',
|
||||
'tests/runtime-assets-contract.ps1',
|
||||
'tests/retired-asset-removal-contract.ps1',
|
||||
'tests/global-heritage-visual-system-contract.ps1'
|
||||
)) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $root $relativePath) -PathType Leaf)) {
|
||||
throw "当前正式 owner 或验证入口不存在:$relativePath"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($referenceOnly in @(
|
||||
'docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png',
|
||||
'docs/design/assets/a01-vnext/A01-shared-skin-family-option-2-selected.png'
|
||||
)) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $root $referenceOnly) -PathType Leaf)) {
|
||||
throw "缺少用户批准的视觉锚点:$referenceOnly"
|
||||
}
|
||||
if (-not $combined.Contains($referenceOnly)) {
|
||||
throw "当前文档没有登记 reference-only 视觉锚点:$referenceOnly"
|
||||
}
|
||||
}
|
||||
|
||||
$pipelinePackage = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'design-pipeline/package.json') | ConvertFrom-Json
|
||||
if ($pipelinePackage.scripts.'build:page-backgrounds' -ne 'node scripts/build-raster-assets.mjs design-pipeline/manifests/page-backgrounds-v3.json') {
|
||||
throw 'design-pipeline 缺少长页面背景 schema v3 重建命令'
|
||||
}
|
||||
if ($pipelinePackage.scripts.'build:g01-state-frame' -ne 'node scripts/build-raster-assets.mjs design-pipeline/manifests/g01-state-frame-v3.json') {
|
||||
throw 'design-pipeline 缺少 G01 状态边框 schema v3 重建命令'
|
||||
}
|
||||
if ($pipelinePackage.dependencies -and $pipelinePackage.dependencies.sharp) {
|
||||
throw '通用 Pillow 构建器生效后不得重新引入 Sharp 单用途依赖'
|
||||
}
|
||||
|
||||
# 阶段 0 已结束,文档体系只允许五个长期入口;临时清单、旧规格、旧计划、交接和设计记录不得重新出现。
|
||||
$temporaryChecklist = Join-Path $root 'docs/项目基线清理清单.md'
|
||||
if (Test-Path -LiteralPath $temporaryChecklist -PathType Leaf) {
|
||||
throw '阶段 0 已完成,临时清理清单必须删除'
|
||||
}
|
||||
|
||||
$allowedMarkdown = @($currentDocuments)
|
||||
$actualMarkdown = @(
|
||||
Get-ChildItem -LiteralPath (Join-Path $root 'docs') -Recurse -File -Filter '*.md' |
|
||||
ForEach-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
)
|
||||
$unexpectedMarkdown = @($actualMarkdown | Where-Object { $_ -notin $allowedMarkdown })
|
||||
if ($unexpectedMarkdown.Count -gt 0) {
|
||||
throw "仍存在当前体系之外的旧 Markdown:$($unexpectedMarkdown -join ', ')"
|
||||
}
|
||||
|
||||
foreach ($retiredRootDocument in @('design-qa.md', '记录.md')) {
|
||||
if (Test-Path -LiteralPath (Join-Path $root $retiredRootDocument) -PathType Leaf) {
|
||||
throw "仍存在已退役的根目录文档:$retiredRootDocument"
|
||||
}
|
||||
}
|
||||
|
||||
# 旧 H5/静态设计图不能再次成为长期视觉证据;目录在 fresh checkout 中可以不存在。
|
||||
$screenDirectory = Join-Path $root 'docs/design/screens'
|
||||
if (Test-Path -LiteralPath $screenDirectory -PathType Container) {
|
||||
$retiredScreens = @(Get-ChildItem -LiteralPath $screenDirectory -Recurse -File)
|
||||
if ($retiredScreens.Count -gt 0) {
|
||||
throw "仍存在已退役的长期截图:$($retiredScreens.Count) 张"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($retiredTest in @(
|
||||
'tests/a01-design-record-audit.ps1',
|
||||
'tests/g06-screenshot-consistency.ps1',
|
||||
'tests/handoff-documentation-contract.ps1',
|
||||
'tests/repository-handoff-size-contract.ps1'
|
||||
)) {
|
||||
if (Test-Path -LiteralPath (Join-Path $root $retiredTest) -PathType Leaf) {
|
||||
throw "仍存在只保护旧文档或截图的测试:$retiredTest"
|
||||
}
|
||||
}
|
||||
|
||||
$sddDirectory = Join-Path $root '.superpowers/sdd'
|
||||
if (Test-Path -LiteralPath $sddDirectory -PathType Container) {
|
||||
$sddFiles = @(Get-ChildItem -LiteralPath $sddDirectory -Recurse -File)
|
||||
if ($sddFiles.Count -gt 0) {
|
||||
throw "仍存在已完成任务的内部 SDD 过程文件:$($sddFiles.Count) 个"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output 'CURRENT-DOCUMENTATION-CONTRACT PASS'
|
||||
Reference in New Issue
Block a user