This commit is contained in:
2026-08-25 14:46:06 +08:00
parent 5abc7543ae
commit f325d57fc1
106 changed files with 3682 additions and 450 deletions
+30 -4
View File
@@ -62,13 +62,13 @@ const parseCompose = (yaml) => {
}))
}
test('root POM freezes 22 modules and service-map freezes 15 executable services', async () => {
test('root POM freezes 23 modules and service-map freezes 16 executable services', async () => {
const modules = parseRootModules(await read('pom.xml'))
const services = parseServiceMap(await read('deploy/service-map.yml'))
assert.equal(modules.length, 22)
assert.equal(new Set(modules).size, 22)
assert.equal(services.size, 15)
assert.equal(modules.length, 23)
assert.equal(new Set(modules).size, 23)
assert.equal(services.size, 16)
const bootModules = []
for (const module of modules) {
@@ -155,6 +155,32 @@ test('Maven verify publishes JaCoCo reports without enforcing a threshold', asyn
assert.match(gitlab, /^ - "\*\*\/target\/jacoco\.exec"$/m)
})
test('SpotBugs remains an opt-in non-blocking report without quality thresholds', async () => {
const pom = await read('pom.xml')
const gitlab = await read('.gitlab-ci.yml')
const profile = pom.match(/<profile>\s*<id>static-analysis-report<\/id>[\s\S]*?<\/profile>/)?.[0] ?? ''
const spotbugs = profile.match(/<plugin>\s*<groupId>com\.github\.spotbugs<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? ''
const reportJob = gitlab.match(/^静态分析报告:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const verifyJob = gitlab.match(/^校验与测试:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? ''
assert.match(pom, /<spotbugs-maven-plugin\.version>4\.9\.8\.5<\/spotbugs-maven-plugin\.version>/)
assert.match(spotbugs, /<artifactId>spotbugs-maven-plugin<\/artifactId>/)
assert.match(spotbugs, /<phase>verify<\/phase>/)
assert.match(spotbugs, /<goal>spotbugs<\/goal>/)
assert.match(spotbugs, /<xmlOutput>true<\/xmlOutput>/)
assert.match(spotbugs, /<htmlOutput>true<\/htmlOutput>/)
assert.match(spotbugs, /<failOnError>false<\/failOnError>/)
assert.doesNotMatch(spotbugs, /<goal>check<\/goal>|<rules>|<threshold>/)
assert.doesNotMatch(pom.replace(profile, ''), /<artifactId>spotbugs-maven-plugin<\/artifactId>/)
assert.match(reportJob, /^ allow_failure: true$/m)
assert.match(reportJob, /-Pstatic-analysis-report/)
assert.match(reportJob, /^ - "\*\*\/target\/spotbugs\*\.xml"$/m)
assert.match(reportJob, /^ - "\*\*\/target\/site\/spotbugs\.html"$/m)
assert.doesNotMatch(verifyJob, /static-analysis-report|spotbugs/i)
assert.doesNotMatch(packageJob, /^ - 静态分析报告$/m)
})
test('regional Compose and Nacos manifests cover the service-map topology', async () => {
const services = parseServiceMap(await read('deploy/service-map.yml'))
const composeByRegion = new Map([
@@ -8,16 +8,18 @@ const read = async (path) => (await readFile(new URL(`../${path}`, import.meta.u
test('Jenkins pipelines recognize all overseas deployable modules', async () => {
const controller = await read('Jenkinsfile')
const service = await read('Jenkinsfile.service')
for (const module of ['gateway-overseas', 'lastleg-service', 'warehouse-overseas-service']) {
for (const module of ['gateway-overseas', 'lastleg-service', 'warehouse-overseas-service', 'overseas-security-service']) {
assert.match(controller, new RegExp(`'${module}'`))
assert.match(service, new RegExp(`'${module}'`))
}
assert.match(controller, /'gateway-overseas': 'SERVICE_GATEWAY_OVERSEAS'/)
assert.match(controller, /'lastleg-service': 'SERVICE_LASTLEG'/)
assert.match(controller, /'warehouse-overseas-service': 'SERVICE_WAREHOUSE_OVERSEAS'/)
assert.match(controller, /'overseas-security-service': 'SERVICE_OVERSEAS_SECURITY'/)
assert.match(service, /gateway-overseas\/target\/gateway-overseas-1\.0\.0-SNAPSHOT\.jar/)
assert.match(service, /lastleg-service\/target\/lastleg-service-1\.0\.0-SNAPSHOT\.jar/)
assert.match(service, /warehouse-overseas-service\/target\/warehouse-overseas-service-1\.0\.0-SNAPSHOT\.jar/)
assert.match(service, /overseas-security-service\/target\/overseas-security-service-1\.0\.0-SNAPSHOT\.jar/)
assert.match(controller, /EVENT_BRIDGE_REGION/)
assert.match(service, /EVENT_BRIDGE_REGION_OVERRIDE=\$\{bridgeRegion\}/)
assert.match(controller, /choices: \['BOTH', 'CN', 'OVERSEAS'\]/)
@@ -30,10 +32,12 @@ test('service map freezes unique internal ports and moves event bridge away from
assert.match(map, /^ gateway-overseas:[\s\S]*?port: 9300$/m)
assert.match(map, /^ lastleg-service:[\s\S]*?port: 9303$/m)
assert.match(map, /^ warehouse-overseas-service:[\s\S]*?port: 9305$/m)
assert.match(map, /^ overseas-security-service:[\s\S]*?port: 9307$/m)
assert.match(map, /^ event-bridge-service:[\s\S]*?port: 9312$/m)
assert.match(map, /^ gateway-overseas:[\s\S]*?regions: \[OVERSEAS\]$/m)
assert.match(map, /^ lastleg-service:[\s\S]*?regions: \[OVERSEAS\]$/m)
assert.match(map, /^ warehouse-overseas-service:[\s\S]*?regions: \[OVERSEAS\]$/m)
assert.match(map, /^ overseas-security-service:[\s\S]*?regions: \[OVERSEAS\]$/m)
assert.match(map, /^ event-bridge-service:[\s\S]*?regions: \[CN, OVERSEAS\]$/m)
})
@@ -46,6 +50,7 @@ test('overseas compose is isolated from CN defaults and disables implicit schema
assert.match(compose, /^ gateway-overseas:\s*$/m)
assert.match(compose, /^ lastleg-service:\s*$/m)
assert.match(compose, /^ warehouse-overseas-service:\s*$/m)
assert.match(compose, /^ overseas-security-service:\s*$/m)
assert.match(compose, /^ event-bridge-service:\s*$/m)
assert.match(compose, /EVENT_BRIDGE_OVERSEAS_PORT:-9313}:9312/)
assert.match(compose, /INTERNAL_SERVICE_TOKEN: \$\{INTERNAL_SERVICE_TOKEN\}/)
@@ -53,6 +58,8 @@ test('overseas compose is isolated from CN defaults and disables implicit schema
assert.match(compose, /EVENT_BRIDGE_DB_URL: \$\{EVENT_BRIDGE_OVERSEAS_DB_URL\}/)
assert.match(compose, /EVENT_BRIDGE_DB_USERNAME: \$\{EVENT_BRIDGE_OVERSEAS_DB_USERNAME\}/)
assert.match(compose, /EVENT_BRIDGE_DB_PASSWORD: \$\{EVENT_BRIDGE_OVERSEAS_DB_PASSWORD\}/)
assert.match(compose, /OVERSEAS_SECURITY_DB_URL: \$\{OVERSEAS_SECURITY_DB_URL\}/)
assert.match(compose, /OVERSEAS_SECURITY_EVENT_CONSUMER_URL:/)
assert.match(compose, /CN_EVENT_BRIDGE_URL: \$\{CN_EVENT_BRIDGE_TARGET_URL:-http:\/\/event-bridge-cn:9312\}/)
assert.match(compose, /FIRSTLEG_EVENT_BRIDGE_URL: \$\{CN_EVENT_BRIDGE_TARGET_URL:-http:\/\/event-bridge-cn:9312\}/)
assert.doesNotMatch(compose, /CN_EVENT_BRIDGE_URL: \$\{CN_EVENT_BRIDGE_URL:/)
@@ -6,7 +6,7 @@ boolean applyChanges = false
String templateName = 'baiyue-firstleg-dev-print'
List<String> serviceModules = [
'gateway', 'gateway-overseas', 'auth', 'iam', 'firstleg', 'finance', 'order', 'warehouse', 'file', 'print',
'integration-service', 'lastleg-service', 'warehouse-overseas-service', 'event-bridge-service', 'audit-service'
'integration-service', 'lastleg-service', 'warehouse-overseas-service', 'overseas-security-service', 'event-bridge-service', 'audit-service'
]
Jenkins jenkins = Jenkins.get()
+230
View File
@@ -0,0 +1,230 @@
export const CONTEXTS = Object.freeze(['REQUEST', 'BACKGROUND', 'INFRASTRUCTURE', 'EXTERNAL'])
export const ACCESS_MODES = Object.freeze(['READ', 'WRITE', 'READ_WRITE'])
export const IDEMPOTENCY = Object.freeze([
'SAFE_READ', 'IDEMPOTENCY_KEY', 'BUSINESS_KEY', 'EVENT_ID', 'STATE_CONVERGENT', 'NONE', 'MIXED'
])
const feign = (path, source, target, contexts, access, idempotency, note) => ({
id: `FEIGN:${path}`,
transport: 'FEIGN', path, source, target, contexts, access, idempotency, note
})
const http = (path, source, target, contexts, access, idempotency, note) => ({
id: `HTTP:${path}`,
transport: 'HTTP', path, source, target, contexts, access, idempotency, note
})
export const SYNCHRONOUS_CLIENTS = Object.freeze([
feign('auth/src/main/java/com/baiyue/auth/client/IamAuthorizationClient.java',
'auth-service', 'iam-service', ['REQUEST'], 'READ', 'SAFE_READ', 'POST 读取授权快照,不写 IAM'),
feign('auth/src/main/java/com/baiyue/auth/client/IamOnboardingClient.java',
'auth-service', 'iam-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', '注册/补偿以 authUserId 收敛,不自动重试'),
feign('file/src/main/java/com/baiyue/file/client/FinanceExportProviderClient.java',
'file-service', 'finance-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '导出 Worker 分批读取'),
feign('file/src/main/java/com/baiyue/file/client/FirstlegExportProviderClient.java',
'file-service', 'firstleg-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '导出 Worker 分批读取'),
feign('file/src/main/java/com/baiyue/file/client/IamExportProviderClient.java',
'file-service', 'iam-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '导出 Worker 分批读取'),
feign('file/src/main/java/com/baiyue/file/client/IamQualificationClient.java',
'file-service', 'iam-service', ['REQUEST'], 'WRITE', 'STATE_CONVERGENT', '资质登记/替换按客户与资质类型收敛,无通用幂等头'),
feign('file/src/main/java/com/baiyue/file/client/OrderExportProviderClient.java',
'file-service', 'order-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '导出 Worker 分批读取'),
feign('file/src/main/java/com/baiyue/file/client/WarehouseExportProviderClient.java',
'file-service', 'warehouse-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '导出 Worker 分批读取'),
feign('file/src/main/java/com/baiyue/file/client/WeightOcrClient.java',
'file-service', 'integration-service', ['REQUEST'], 'READ', 'NONE', '上传线程内调用 OCR;第三方调用不可假定免费幂等'),
feign('finance/src/main/java/com/baiyue/finance/client/FinanceFileClient.java',
'finance-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '元数据 GET 可重复,凭证上传无幂等键'),
feign('finance/src/main/java/com/baiyue/finance/client/FirstlegCostSnapshotClient.java',
'finance-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取整柜成本分摊快照'),
feign('finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java',
'finance-service', 'firstleg-service', ['REQUEST', 'BACKGROUND'], 'READ_WRITE', 'MIXED',
'事件消费回读快照;平台作废命令携带 Idempotency-Key 与请求哈希'),
feign('finance/src/main/java/com/baiyue/finance/client/IamCustomerClient.java',
'finance-service', 'iam-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取客户主档快照'),
feign('finance/src/main/java/com/baiyue/finance/client/WarehouseSupplierClient.java',
'finance-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取并校验供应商快照'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/CarrierScheduleIntegrationClient.java',
'firstleg-service', 'integration-service', ['BACKGROUND'], 'READ', 'SAFE_READ', '船节点定时任务查询,不属于用户请求'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/EventBridgeClient.java',
'firstleg-service', 'event-bridge-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '装柜/船节点 Outbox 按 eventId 投递'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/FeePaymentVoucherFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '校验付款凭证元数据'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClient.java',
'firstleg-service', 'finance-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '费用 Outbox 按事件幂等键投递'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/IamCustomerSnapshotClient.java',
'firstleg-service', 'iam-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取客户名称与状态快照'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/LoadingDocumentFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据/下载可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/LoadingEvidenceFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取装柜证据元数据/内容'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '图片上传/作废非安全重放,查询可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java',
'firstleg-service', 'order-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取订单货物/来源快照'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java',
'firstleg-service', 'order-service', ['REQUEST'], 'WRITE', 'IDEMPOTENCY_KEY', 'PDA 结算拆单携带 idempotencyKey/operationId'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '价目文件上传无幂等键,读取可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookWarehouseClient.java',
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库/渠道主数据'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java',
'firstleg-service', 'print-service', ['REQUEST'], 'READ', 'SAFE_READ', '无状态 PDF 渲染,POST 为计算型读取'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/TruckAppointmentFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '附件上传无幂等键,元数据读取可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseMasterDataClient.java',
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库主数据'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java',
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ_WRITE', 'MIXED',
'查询可重复;预占/选择释放/转移/结算使用业务幂等键,普通确认/释放按状态收敛'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseSupplierClient.java',
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取供应商快照'),
feign('firstleg/src/main/java/com/baiyue/firstleg/customs/client/CustomsFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '报关文件上传无幂等键,读取可重复'),
feign('iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java',
'iam-service', 'auth-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '账号查询可重复,写操作按账号/命令业务键收敛'),
feign('iam/src/main/java/com/baiyue/iam/client/CustomerDimensionRuleClient.java',
'iam-service', 'firstleg-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', '初始化/补偿使用 initializationKey'),
feign('iam/src/main/java/com/baiyue/iam/client/OverseasStorageFeeClient.java',
'iam-service', 'warehouse-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', '初始化/补偿使用 initializationKey'),
feign('iam/src/main/java/com/baiyue/iam/client/SecurityEventBridgeClient.java',
'iam-service', 'event-bridge-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'IAM 安全 Outbox 按 eventId 投递'),
feign('lastleg-service/src/main/java/com/baiyue/lastleg/client/EventBridgeClient.java',
'lastleg-service', 'event-bridge-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '卸柜完成 Outbox 按 eventId 投递'),
feign('lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java',
'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '报价创建时读取国内合作仓快照'),
feign('order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java',
'order-service', 'firstleg-service', ['REQUEST', 'BACKGROUND'], 'READ_WRITE', 'BUSINESS_KEY',
'列表查询/缺失修复在线执行;审核建单由 Outbox 投递,按 orderId/inboundCode 收敛'),
feign('order/src/main/java/com/baiyue/order/client/FirstlegPricingClient.java',
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', 'POST 校验计价渠道,不写履约状态'),
feign('order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java',
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', 'POST 查询统一订单投影'),
feign('order/src/main/java/com/baiyue/order/client/ProductImageClient.java',
'order-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取商品图片元数据'),
feign('order/src/main/java/com/baiyue/order/client/TenantEligibilityClient.java',
'order-service', 'iam-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取客户下单资格'),
feign('order/src/main/java/com/baiyue/order/client/WarehouseClient.java',
'order-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库主数据'),
http('common-audit/src/main/java/com/baiyue/common/audit/HttpAuditSink.java',
'common-audit', 'audit-service', ['INFRASTRUCTURE'], 'WRITE', 'NONE', '同步审计投递失败当前仅记录告警'),
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFirstlegEventClient.java',
'event-bridge-service', 'firstleg-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'Inbox 租约投递,目标按 eventId 幂等'),
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpLastlegEventClient.java',
'event-bridge-service', 'lastleg-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'Inbox 租约投递,目标按 eventId 幂等'),
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpSecurityProjectionEventClient.java',
'event-bridge-service', 'overseas-security-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'IAM 安全事件投影按 eventId/版本单调幂等'),
http('gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpOverseasSecurityValidationClient.java',
'gateway-overseas', 'overseas-security-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST 实时校验,失败关闭'),
http('gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
'gateway-overseas', 'auth-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST PDA 会话校验,失败关闭'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java',
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java',
'lastleg-service', 'warehouse-overseas-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '查询可重复;写操作按 business/reservation key 或状态收敛'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java',
'lastleg-service', 'warehouse-overseas-service', ['REQUEST'], 'READ_WRITE', 'IDEMPOTENCY_KEY', '托盘变更/补偿使用 operationKey 与请求哈希'),
http('lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java',
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'),
http('integration-service/src/main/java/com/baiyue/integration/provider/EvergreenTrackingClient.java',
'integration-service', 'external-evergreen', ['EXTERNAL'], 'READ', 'SAFE_READ', '船司轨迹查询;遵守外部限流'),
http('integration-service/src/main/java/com/baiyue/integration/provider/HedeTrackingClient.java',
'integration-service', 'external-hede', ['EXTERNAL'], 'READ', 'SAFE_READ', '船司轨迹查询;遵守外部限流'),
http('integration-service/src/main/java/com/baiyue/integration/provider/MatsonTrackingClient.java',
'integration-service', 'external-matson', ['EXTERNAL'], 'READ', 'SAFE_READ', '船司轨迹查询;遵守外部限流'),
http('integration-service/src/main/java/com/baiyue/integration/provider/SmlTrackingClient.java',
'integration-service', 'external-sml', ['EXTERNAL'], 'READ', 'SAFE_READ', '船司轨迹查询;遵守外部限流'),
http('integration-service/src/main/java/com/baiyue/integration/provider/ZimTrackingClient.java',
'integration-service', 'external-zim', ['EXTERNAL'], 'READ', 'SAFE_READ', '令牌获取与轨迹查询不改变平台业务事实'),
http('integration-service/src/main/java/com/baiyue/integration/provider/ocr/BaiduMeterClient.java',
'integration-service', 'external-baidu-ocr', ['EXTERNAL'], 'READ', 'NONE', 'OCR 可能计费,不由 HTTP 层自动重试')
])
export const USER_ENTRIES = Object.freeze([
{ name: '认证 API', service: 'auth-service' },
{ name: 'IAM API', service: 'iam-service' },
{ name: '订单 API', service: 'order-service' },
{ name: '头程 API/PDA', service: 'firstleg-service' },
{ name: '财务 API', service: 'finance-service' },
{ name: '文件 API', service: 'file-service' },
{ name: '海外尾程 API/PDA', service: 'lastleg-service' },
{ name: '国内仓储 API', service: 'warehouse-service' },
{ name: '海外仓储 API', service: 'warehouse-overseas-service' }
])
export function graphEdges(context) {
return [...new Set(SYNCHRONOUS_CLIENTS
.filter(client => context === 'ALL' || client.contexts.includes(context))
.map(client => `${client.source}->${client.target}`))].sort()
}
export function stronglyConnectedComponents(edgeNames) {
const adjacency = new Map()
for (const edge of edgeNames) {
const [from, to] = edge.split('->')
if (!adjacency.has(from)) adjacency.set(from, new Set())
if (!adjacency.has(to)) adjacency.set(to, new Set())
adjacency.get(from).add(to)
}
let index = 0
const indexes = new Map()
const lowLinks = new Map()
const stack = []
const onStack = new Set()
const components = []
function connect(node) {
indexes.set(node, index)
lowLinks.set(node, index++)
stack.push(node)
onStack.add(node)
for (const target of adjacency.get(node)) {
if (!indexes.has(target)) {
connect(target)
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(target)))
} else if (onStack.has(target)) lowLinks.set(node, Math.min(lowLinks.get(node), indexes.get(target)))
}
if (lowLinks.get(node) !== indexes.get(node)) return
const component = []
let current
do {
current = stack.pop()
onStack.delete(current)
component.push(current)
} while (current !== node)
components.push(component.sort())
}
for (const node of adjacency.keys()) if (!indexes.has(node)) connect(node)
return components.filter(component => component.length > 1)
.sort((left, right) => left.join().localeCompare(right.join()))
}
export function longestSimplePaths(entryService, edgeNames = graphEdges('REQUEST')) {
const adjacency = new Map()
for (const edge of edgeNames) {
const [from, to] = edge.split('->')
if (!adjacency.has(from)) adjacency.set(from, new Set())
adjacency.get(from).add(to)
}
let longest = []
function visit(node, path) {
const targets = [...(adjacency.get(node) ?? [])].sort()
let extended = false
for (const target of targets) {
if (path.includes(target)) continue
extended = true
visit(target, [...path, target])
}
if (extended) return
if (longest.length === 0 || path.length > longest[0].length) longest = [path]
else if (path.length === longest[0].length) longest.push(path)
}
visit(entryService, [entryService])
return longest.sort((left, right) => left.join('->').localeCompare(right.join('->')))
}
+169 -112
View File
@@ -3,72 +3,62 @@ import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
ACCESS_MODES, CONTEXTS, IDEMPOTENCY, SYNCHRONOUS_CLIENTS, USER_ENTRIES,
graphEdges, longestSimplePaths, stronglyConnectedComponents
} from './synchronous-dependency-baseline.mjs'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const serviceModules = [
'auth', 'iam', 'order', 'firstleg', 'finance', 'file', 'warehouse',
'integration-service', 'print', 'event-bridge-service', 'lastleg-service',
'warehouse-overseas-service'
'warehouse-overseas-service', 'overseas-security-service'
]
const canonicalSource = new Map(serviceModules.map(name => [name, `${name}${name.endsWith('-service') ? '' : '-service'}`]))
const expectedFeignEdges = new Map(Object.entries({
'auth-service->iam-service': 2,
'file-service->finance-service': 1,
'file-service->firstleg-service': 1,
'file-service->iam-service': 2,
'file-service->integration-service': 1,
'file-service->order-service': 1,
'file-service->warehouse-service': 1,
'finance-service->file-service': 1,
'finance-service->firstleg-service': 2,
'finance-service->iam-service': 1,
'finance-service->warehouse-service': 1,
'firstleg-service->event-bridge-service': 1,
'firstleg-service->file-service': 7,
'firstleg-service->finance-service': 1,
'firstleg-service->iam-service': 1,
'firstleg-service->integration-service': 1,
'firstleg-service->order-service': 2,
'firstleg-service->print-service': 1,
'firstleg-service->warehouse-service': 4,
'iam-service->auth-service': 1,
'iam-service->event-bridge-service': 1,
'iam-service->firstleg-service': 1,
'iam-service->warehouse-service': 1,
'lastleg-service->event-bridge-service': 1,
'lastleg-service->warehouse-service': 1,
'order-service->file-service': 1,
'order-service->firstleg-service': 3,
'order-service->iam-service': 1,
'order-service->warehouse-service': 1
}))
const expectedRawHttpImplementations = [
'common-audit/src/main/java/com/baiyue/common/audit/HttpAuditSink.java',
'event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFirstlegEventClient.java',
'event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpLastlegEventClient.java',
'gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
'lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java',
'lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java',
'lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java',
'lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java',
'integration-service/src/main/java/com/baiyue/integration/provider/EvergreenTrackingClient.java',
'integration-service/src/main/java/com/baiyue/integration/provider/HedeTrackingClient.java',
'integration-service/src/main/java/com/baiyue/integration/provider/MatsonTrackingClient.java',
'integration-service/src/main/java/com/baiyue/integration/provider/SmlTrackingClient.java',
'integration-service/src/main/java/com/baiyue/integration/provider/ZimTrackingClient.java',
'integration-service/src/main/java/com/baiyue/integration/provider/ocr/BaiduMeterClient.java'
const expectedOnlineEdges = [
'auth-service->iam-service',
'file-service->iam-service',
'file-service->integration-service',
'finance-service->file-service',
'finance-service->firstleg-service',
'finance-service->iam-service',
'finance-service->warehouse-service',
'firstleg-service->file-service',
'firstleg-service->iam-service',
'firstleg-service->order-service',
'firstleg-service->print-service',
'firstleg-service->warehouse-service',
'iam-service->auth-service',
'iam-service->firstleg-service',
'iam-service->warehouse-service',
'lastleg-service->file-service',
'lastleg-service->warehouse-overseas-service',
'lastleg-service->warehouse-service',
'order-service->file-service',
'order-service->firstleg-service',
'order-service->iam-service',
'order-service->warehouse-service'
]
const reviewedRawInternalEdges = [
const expectedBackgroundEdges = [
'event-bridge-service->firstleg-service',
'event-bridge-service->lastleg-service',
'gateway-overseas->auth-service',
'lastleg-service->file-service',
'lastleg-service->warehouse-overseas-service'
'event-bridge-service->overseas-security-service',
'file-service->finance-service',
'file-service->firstleg-service',
'file-service->iam-service',
'file-service->order-service',
'file-service->warehouse-service',
'finance-service->firstleg-service',
'firstleg-service->event-bridge-service',
'firstleg-service->finance-service',
'firstleg-service->integration-service',
'iam-service->event-bridge-service',
'lastleg-service->event-bridge-service',
'order-service->firstleg-service'
]
const rawHttpPattern = /\b(?:RestClient|WebClient|HttpClient)\b/
@@ -87,7 +77,7 @@ async function javaFiles(directory) {
}
async function discoverFeignEdges() {
const edges = new Map()
const clients = []
for (const module of serviceModules) {
const sourceRoot = path.join(root, module, 'src', 'main', 'java')
for (const file of await javaFiles(sourceRoot)) {
@@ -96,66 +86,24 @@ async function discoverFeignEdges() {
const annotation = source.match(/@FeignClient\(([\s\S]*?)\)\s*(?:public\s+)?interface/)
const target = annotation?.[1].match(/name\s*=\s*"([^"]+)"/)?.[1]
assert.ok(target, `cannot resolve Feign target from ${path.relative(root, file)}`)
const edge = `${canonicalSource.get(module)}->${target}`
edges.set(edge, (edges.get(edge) ?? 0) + 1)
clients.push({
path: path.relative(root, file).replaceAll('\\', '/'),
source: canonicalSource.get(module), target
})
}
}
return edges
return clients.sort((left, right) => left.path.localeCompare(right.path))
}
function sortedEntries(map) {
return [...map.entries()].sort(([left], [right]) => left.localeCompare(right))
}
function stronglyConnectedComponents(edgeNames) {
const adjacency = new Map()
for (const edge of edgeNames) {
const [from, to] = edge.split('->')
if (!adjacency.has(from)) adjacency.set(from, new Set())
if (!adjacency.has(to)) adjacency.set(to, new Set())
adjacency.get(from).add(to)
}
let index = 0
const indexes = new Map()
const lowLinks = new Map()
const stack = []
const onStack = new Set()
const components = []
function connect(node) {
indexes.set(node, index)
lowLinks.set(node, index++)
stack.push(node)
onStack.add(node)
for (const target of adjacency.get(node)) {
if (!indexes.has(target)) {
connect(target)
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(target)))
} else if (onStack.has(target)) {
lowLinks.set(node, Math.min(lowLinks.get(node), indexes.get(target)))
}
}
if (lowLinks.get(node) !== indexes.get(node)) return
const component = []
let current
do {
current = stack.pop()
onStack.delete(current)
component.push(current)
} while (current !== node)
components.push(component.sort())
}
for (const node of adjacency.keys()) if (!indexes.has(node)) connect(node)
return components.filter(component => component.length > 1)
.sort((left, right) => left.join().localeCompare(right.join()))
}
test('all Feign clients remain covered by the reviewed synchronous edge baseline', async () => {
assert.deepEqual(sortedEntries(await discoverFeignEdges()), sortedEntries(expectedFeignEdges))
test('all 44 Feign clients have reviewed context, access and idempotency metadata', async () => {
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN')
.map(({ path, source, target }) => ({ path, source, target }))
.sort((left, right) => left.path.localeCompare(right.path))
assert.equal(reviewed.length, 44)
assert.deepEqual(await discoverFeignEdges(), reviewed)
})
test('raw HTTP transports cannot be added without an explicit graph review', async () => {
test('all 16 raw HTTP transports have reviewed context, access and idempotency metadata', async () => {
const discovered = []
for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) {
for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) {
@@ -165,18 +113,127 @@ test('raw HTTP transports cannot be added without an explicit graph review', asy
discovered.push(path.relative(root, file).replaceAll('\\', '/'))
}
}
assert.deepEqual(discovered.sort(), [...expectedRawHttpImplementations].sort())
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP')
.map(client => client.path).sort()
assert.equal(reviewed.length, 16)
assert.deepEqual(discovered.sort(), reviewed)
})
test('the current synchronous service graph keeps its reviewed strongly connected component visible', async () => {
const feignEdges = (await discoverFeignEdges()).keys()
const components = stronglyConnectedComponents([...feignEdges, ...reviewedRawInternalEdges])
assert.deepEqual(components, [[
test('every reviewed synchronous client has a complete and exclusive executable classification', () => {
assert.equal(new Set(SYNCHRONOUS_CLIENTS.map(client => client.id)).size, SYNCHRONOUS_CLIENTS.length)
for (const client of SYNCHRONOUS_CLIENTS) {
assert.ok(client.path && client.source && client.target && client.note, `${client.id} metadata is incomplete`)
assert.ok(client.contexts.length > 0, `${client.id} has no execution context`)
assert.equal(new Set(client.contexts).size, client.contexts.length, `${client.id} repeats a context`)
for (const context of client.contexts) assert.ok(CONTEXTS.includes(context), `${client.id} has invalid context ${context}`)
assert.ok(ACCESS_MODES.includes(client.access), `${client.id} has invalid access ${client.access}`)
assert.ok(IDEMPOTENCY.includes(client.idempotency), `${client.id} has invalid idempotency ${client.idempotency}`)
}
})
test('all four Lastleg RestClient gateways use explicit bounded transports', async () => {
const clients = new Map([
['lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java', 'getUnloadPhotoFile'],
['lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java', 'getRelabelPhotoFile'],
['lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java', 'getWarehouseResource'],
['lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java', 'getPalletPhysical']
])
for (const [file, propertyAccessor] of clients) {
const source = await readFile(path.join(root, file), 'utf8')
assert.match(source, /restClientFactory\.build\(restClientBuilder, baseUrl,/)
assert.match(source, new RegExp(`restClientProperties\\.${propertyAccessor}\\(\\)`))
assert.doesNotMatch(source, /RetryTemplate|Retryer|@Retryable|\.retry\(|fallbackFactory|fallback\s*=/i)
}
const factory = await readFile(path.join(root,
'lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientFactory.java'), 'utf8')
assert.match(factory, /connectTimeout\(timeouts\.getConnectTimeout\(\)\)/)
assert.match(factory, /setReadTimeout\(timeouts\.getReadTimeout\(\)\)/)
assert.doesNotMatch(factory, /RetryTemplate|Retryer|@Retryable|\.retry\(|fallbackFactory|fallback\s*=/i)
for (const configFile of [
'lastleg-service/src/main/resources/application.yml',
'config/nacos/overseas/lastleg-service.yml'
]) {
const source = await readFile(path.join(root, configFile), 'utf8')
for (const key of ['unload-photo-file', 'relabel-photo-file', 'warehouse-resource', 'pallet-physical']) {
assert.match(source, new RegExp(
`${key}:\\s*\\n\\s+connect-timeout:\\s*\\$\\{[^}]+:[1-9][0-9]*(?:ms|s)\\}`
+ `\\s*\\n\\s+read-timeout:\\s*\\$\\{[^}]+:[1-9][0-9]*(?:ms|s)\\}`))
}
}
})
test('REQUEST, BACKGROUND and full graphs remain separate executable baselines', () => {
assert.deepEqual(graphEdges('REQUEST'), expectedOnlineEdges)
assert.deepEqual(graphEdges('BACKGROUND'), expectedBackgroundEdges)
assert.deepEqual(graphEdges('INFRASTRUCTURE'), [
'common-audit->audit-service',
'gateway-overseas->auth-service',
'gateway-overseas->overseas-security-service'
])
assert.deepEqual(stronglyConnectedComponents(graphEdges('REQUEST')), [[
'auth-service', 'file-service', 'firstleg-service', 'iam-service', 'order-service'
]])
assert.deepEqual(stronglyConnectedComponents(graphEdges('BACKGROUND')), [[
'event-bridge-service', 'finance-service', 'firstleg-service', 'lastleg-service'
]])
assert.deepEqual(stronglyConnectedComponents(graphEdges('ALL')), [[
'auth-service', 'event-bridge-service', 'file-service', 'finance-service',
'firstleg-service', 'iam-service', 'lastleg-service', 'order-service'
]])
})
test('overseas security edges never become online business dependencies', () => {
assert.ok(!graphEdges('REQUEST').some(edge => edge.includes('overseas-security-service')))
assert.ok(graphEdges('INFRASTRUCTURE').includes('gateway-overseas->overseas-security-service'))
assert.ok(graphEdges('BACKGROUND').includes('event-bridge-service->overseas-security-service'))
})
test('known user entries expose every longest simple REQUEST path over the two-hop budget', () => {
const summary = Object.fromEntries(USER_ENTRIES.map(entry => {
const paths = longestSimplePaths(entry.service)
return [entry.service, { hops: paths[0].length - 1, paths: paths.map(path => path.join('->')) }]
}))
assert.deepEqual(summary, {
'auth-service': { hops: 5, paths: [
'auth-service->iam-service->firstleg-service->order-service->file-service->integration-service'
] },
'iam-service': { hops: 4, paths: [
'iam-service->firstleg-service->order-service->file-service->integration-service'
] },
'order-service': { hops: 4, paths: [
'order-service->file-service->iam-service->firstleg-service->print-service',
'order-service->file-service->iam-service->firstleg-service->warehouse-service',
'order-service->firstleg-service->file-service->iam-service->auth-service',
'order-service->firstleg-service->file-service->iam-service->warehouse-service',
'order-service->iam-service->firstleg-service->file-service->integration-service'
] },
'firstleg-service': { hops: 4, paths: [
'firstleg-service->order-service->file-service->iam-service->auth-service',
'firstleg-service->order-service->file-service->iam-service->warehouse-service'
] },
'finance-service': { hops: 5, paths: [
'finance-service->file-service->iam-service->firstleg-service->order-service->warehouse-service',
'finance-service->firstleg-service->order-service->file-service->iam-service->auth-service',
'finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service',
'finance-service->iam-service->firstleg-service->order-service->file-service->integration-service'
] },
'file-service': { hops: 4, paths: [
'file-service->iam-service->firstleg-service->order-service->warehouse-service'
] },
'lastleg-service': { hops: 5, paths: [
'lastleg-service->file-service->iam-service->firstleg-service->order-service->warehouse-service'
] },
'warehouse-service': { hops: 0, paths: ['warehouse-service'] },
'warehouse-overseas-service': { hops: 0, paths: ['warehouse-overseas-service'] }
})
for (const entry of USER_ENTRIES) {
const result = summary[entry.service]
if (result.hops > 2) assert.ok(result.paths.every(path => path.split('->').length > 3))
}
})
test('shared Feign timeout defaults and long-running export overrides do not drift silently', async () => {
for (const profile of ['local', 'cn', 'overseas']) {
const source = await readFile(path.join(root, 'config', 'nacos', profile, 'baiyue-common.yml'), 'utf8')