优化
This commit is contained in:
@@ -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`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user