This commit is contained in:
2026-08-28 23:01:52 +08:00
parent 969ff05815
commit da32a67d7f
107 changed files with 465 additions and 271 deletions
+1
View File
@@ -33,6 +33,7 @@ node scripts/create-pending-review-orders.mjs
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、四期范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口与排除目录 |
| Mapper 注册静态合同 | `node scripts/mapper-scan-contract.mjs`、`node --test scripts/mapper-scan-contract.test.mjs`;自动核对全部持久化模块的 `@Mapper`、服务根包注解过滤扫描和唯一扫描入口,不启动 Spring、不连接数据库;`backend-full-regression.ps1` 在 Maven 前强制执行 |
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 用显式 `--require-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 |
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
+28
View File
@@ -93,13 +93,37 @@ $pdaAgents = Read-RequiredText $pdaAgentsPath
$overseasPdaAgents = Read-RequiredText $overseasPdaAgentsPath
$backendAgents = Read-RequiredText $backendAgentsPath
$engineeringConstraints = Read-RequiredText (Join-Path $workspaceRoot 'docs/AI工程执行与协作约束.md')
$mapperScanContract = Read-RequiredText (Join-Path $backendRoot 'scripts/mapper-scan-contract.mjs')
# 当前未上线阶段的无兼容原则必须覆盖全部实现与运行状态,不能再次缩窄为文件或数据库专项规则。
Assert-Contains $rootAgents '源码结构、接口与 DTO、字段、状态机、权限、配置键、数据库结构与初始化 SQL、事件、路由、页面、Mock/Fixture、测试和脚本' '根无兼容收敛范围'
Assert-Contains $rootAgents '数据库记录、上传文件、缓存、会话、消息、搜索索引和其他运行状态均视为可丢弃' '根开发运行状态范围'
Assert-Contains $rootAgents '后端 MyBatis Mapper 新增、移动或扫描遗漏' '根 Mapper 快速入口'
Assert-Contains $rootAgents 'node scripts/mapper-scan-contract.mjs' '根 Mapper 静态门禁'
Assert-Contains $engineeringConstraints '禁止为旧代码、旧调用或开发环境存量保留旧接口/旧字段别名' '工程无兼容执行规则'
Assert-Contains $engineeringConstraints '不授权自动重置数据库、删除数据/文件、执行 SQL、部署或修改环境' '无兼容安全边界'
Assert-Contains $clientAgents '不得继续扩展 `LEGACY_MANUAL`、旧系统入仓码、C02 不可用回退旧 API' 'Client 头程无兼容边界'
Assert-Contains $backendAgents '所有生产 MyBatis Mapper 接口必须显式标记 `@Mapper`' '后端 Mapper 注册规则'
Assert-Contains $backendAgents 'Mapper 新增、移动、Bean 缺失与扫描遗漏' '后端 Mapper 快速路由'
Assert-Contains $mapperScanContract 'annotationClass = Mapper.class' 'Mapper 根包注解过滤合同'
Assert-Contains $mapperScanContract '禁止分散维护 MapperScan' 'Mapper 单一扫描入口合同'
$mapperModuleAgents = @(
@{ Module = 'audit-service'; Application = 'BaiyueAuditApplication.java' },
@{ Module = 'auth'; Application = 'BaiyueAuthApplication.java' },
@{ Module = 'event-bridge-service'; Application = 'EventBridgeApplication.java' },
@{ Module = 'file'; Application = 'FileServiceApplication.java' },
@{ Module = 'firstleg'; Application = 'BaiyueFirstlegApplication.java' },
@{ Module = 'iam'; Application = 'BaiyueIamApplication.java' },
@{ Module = 'lastleg-service'; Application = 'BaiyueLastlegApplication.java' },
@{ Module = 'order'; Application = 'BaiyueOrderApplication.java' },
@{ Module = 'warehouse'; Application = 'BaiyueWarehouseApplication.java' },
@{ Module = 'warehouse-overseas-service'; Application = 'BaiyueWarehouseOverseasApplication.java' }
)
foreach ($entry in $mapperModuleAgents) {
$moduleAgents = Read-RequiredText (Join-Path $backendRoot "$($entry.Module)/AGENTS.md")
Assert-Contains $moduleAgents $entry.Application "$($entry.Module) Mapper 启动入口"
Assert-Contains $moduleAgents 'scripts/mapper-scan-contract.mjs' "$($entry.Module) Mapper 静态门禁"
}
# 部署单元必须能从根索引进入。
Get-ChildItem -LiteralPath (Join-Path $workspaceRoot 'apps') -Directory |
@@ -439,6 +463,10 @@ $criticalPaths = @(
'apps/pda-overseas-uniapp/src/i18n/locales/en.js',
'apps/pda-overseas-uniapp/src/i18n/locales/es.js',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java',
'baiyue-firstleg/scripts/mapper-scan-contract.mjs',
'baiyue-firstleg/scripts/mapper-scan-contract.test.mjs',
'baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/MapperRegistrationArchitectureTest.java',
'baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/MapperRegistrationArchitectureTest.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/client/FeePaymentVoucherFileClient.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentService.java',
+8
View File
@@ -7,7 +7,9 @@ param(
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$pom = Join-Path $repoRoot "pom.xml"
$mapperScanContract = Join-Path $repoRoot "scripts/mapper-scan-contract.mjs"
$requiredTests = @(
"scripts/mapper-scan-contract.test.mjs",
"firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingControllerContractTest.java",
"firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java",
"firstleg/src/test/java/com/baiyue/firstleg/repository/TruckAppointmentMapperContractTest.java",
@@ -45,6 +47,10 @@ if (-not (Test-Path -LiteralPath $pom)) {
Write-Error "Missing Maven pom: $pom"
exit 2
}
if (-not (Test-Path -LiteralPath $mapperScanContract)) {
Write-Error "Missing Mapper scan contract: $mapperScanContract"
exit 2
}
$missing = @($requiredTests | Where-Object { -not (Test-Path -LiteralPath (Join-Path $repoRoot $_)) })
if ($missing.Count -gt 0) {
@@ -60,6 +66,8 @@ if ($DryRun) {
Push-Location $repoRoot
try {
& node $mapperScanContract
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& $MavenCommand @mavenArgs
$code = $LASTEXITCODE
} finally {
@@ -23,6 +23,11 @@ test('backend full regression includes file reactor and critical file/container
]) assert.match(backendScript, new RegExp(required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
})
test('backend full regression rejects Mapper registration drift before Maven', () => {
assert.match(backendScript, /scripts\/mapper-scan-contract\.mjs/)
assert.match(backendScript, /& node \$mapperScanContract[\s\S]*& \$MavenCommand @mavenArgs/)
})
test('test-environment full acceptance registers container overview module', () => {
const modulePath = resolve(scriptsDir, 'admin-test-env-modules', 'phase3-container-overview.mjs')
if (!adminAcceptance.includes('phase3-container-overview') || !existsSync(modulePath)) {
+122
View File
@@ -0,0 +1,122 @@
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
export const repositoryRoot = resolve(scriptDir, '..')
function filesBelow(root, predicate) {
if (!statSafe(root)?.isDirectory()) return []
const result = []
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (entry.name === 'target' || entry.name === '.git') continue
const path = join(root, entry.name)
if (entry.isDirectory()) result.push(...filesBelow(path, predicate))
else if (predicate(path)) result.push(path)
}
return result
}
function statSafe(path) {
try { return statSync(path) } catch { return null }
}
function packageName(source) {
return source.match(/\bpackage\s+([\w.]+)\s*;/)?.[1] || ''
}
function mapperInterfaces(javaFiles) {
return javaFiles.flatMap((path) => {
const source = readFileSync(path, 'utf8')
const names = [...source.matchAll(/\b(?:public\s+)?interface\s+(\w*Mapper)\b/g)].map((match) => match[1])
return names.map((name) => ({ path, source, packageName: packageName(source), name }))
})
}
function mapperScanAnnotation(source) {
const start = source.indexOf('@MapperScan')
if (start < 0) return ''
const end = source.indexOf('\n', start)
if (source[start + '@MapperScan'.length] !== '(') return source.slice(start, end < 0 ? undefined : end)
let depth = 0
for (let index = start + '@MapperScan'.length; index < source.length; index += 1) {
if (source[index] === '(') depth += 1
else if (source[index] === ')' && --depth === 0) return source.slice(start, index + 1)
}
return source.slice(start)
}
function moduleNames(root) {
const pom = readFileSync(join(root, 'pom.xml'), 'utf8')
return [...pom.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
}
export function auditMapperScanning(root = repositoryRoot) {
const failures = []
let mapperCount = 0
let persistenceModuleCount = 0
for (const moduleName of moduleNames(root)) {
const moduleRoot = join(root, moduleName)
const javaRoot = join(moduleRoot, 'src', 'main', 'java')
const javaFiles = filesBelow(javaRoot, (path) => path.endsWith('.java'))
const mappers = mapperInterfaces(javaFiles)
if (!mappers.length) continue
persistenceModuleCount += 1
mapperCount += mappers.length
const mapperNames = new Map()
for (const mapper of mappers) {
const previous = mapperNames.get(mapper.name)
if (previous) {
failures.push(`${moduleName}: 根包扫描下 Mapper Bean 名称重复 ${mapper.name}(${relative(root, previous)}、${relative(root, mapper.path)})`)
} else mapperNames.set(mapper.name, mapper.path)
if (!/@Mapper\b/.test(mapper.source)) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 缺少 @Mapper(${relative(root, mapper.path)})`)
}
}
const applications = javaFiles.filter((path) => /@SpringBootApplication\b/.test(readFileSync(path, 'utf8')))
if (applications.length !== 1) {
failures.push(`${moduleName}: 期望且只能有一个 @SpringBootApplication,实际 ${applications.length} 个`)
continue
}
const applicationPath = applications[0]
const applicationSource = readFileSync(applicationPath, 'utf8')
const rootPackage = packageName(applicationSource)
const scan = mapperScanAnnotation(applicationSource)
if (!scan) failures.push(`${moduleName}: 启动类缺少统一 @MapperScan`)
else {
if (!scan.includes(`"${rootPackage}"`)) failures.push(`${moduleName}: @MapperScan 必须覆盖服务根包 ${rootPackage}`)
if (!/annotationClass\s*=\s*Mapper\.class/.test(scan)) failures.push(`${moduleName}: @MapperScan 必须使用 annotationClass = Mapper.class`)
}
for (const path of javaFiles) {
if (path === applicationPath) continue
if (/@MapperScan\b/.test(readFileSync(path, 'utf8'))) {
failures.push(`${moduleName}: 禁止分散维护 MapperScan(${relative(root, path)})`)
}
}
for (const mapper of mappers) {
if (!(mapper.packageName === rootPackage || mapper.packageName.startsWith(`${rootPackage}.`))) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 不在服务扫描根包 ${rootPackage} 内`)
}
}
}
return { failures, mapperCount, persistenceModuleCount }
}
function main() {
const result = auditMapperScanning(repositoryRoot)
if (result.failures.length) {
console.error(`Mapper 扫描合同失败,共 ${result.failures.length} 项:`)
result.failures.forEach((failure) => console.error(`- ${failure}`))
process.exitCode = 1
return
}
console.log(`Mapper 扫描合同通过:${result.persistenceModuleCount} 个持久化模块、${result.mapperCount} 个 Mapper 均使用 @Mapper,并由服务根包注解过滤扫描。`)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main()
+10
View File
@@ -0,0 +1,10 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { auditMapperScanning, repositoryRoot } from './mapper-scan-contract.mjs'
test('every production MyBatis mapper is annotated and covered by one root scan', () => {
const result = auditMapperScanning(repositoryRoot)
assert.deepEqual(result.failures, [])
assert.ok(result.persistenceModuleCount >= 10)
assert.ok(result.mapperCount >= 200)
})