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/OceanBookingFileClient.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', 'BACKGROUND'], 'READ', 'SAFE_READ', '在线测量/箱号/排柜读取;Order 审核 Outbox 创建 Firstleg 入库及箱号时也会后台回读订单货物'), 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/PendingReviewOrderClient.java', 'firstleg-service', 'order-service', ['REQUEST'], 'READ', 'SAFE_READ', '智能排柜分页读取待审核订单投影'), 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/WarehousePartnerClient.java', 'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取启用往来单位快照'), 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', ['BACKGROUND'], 'WRITE', 'BUSINESS_KEY', 'IAM 持久化任务租约投递;初始化/补偿复用稳定 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/client/DomesticWarehouseDirectoryClient.java', 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取国内共享仓库权威目录'), feign('lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java', 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '报价创建时读取国内合作仓快照'), feign('warehouse-overseas-service/src/main/java/com/baiyue/warehouse/client/DomesticWarehouseDirectoryClient.java', 'warehouse-overseas-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '海外库位、托盘和资源锁写入前读取国内共享收货仓权威目录'), feign('order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java', 'order-service', 'firstleg-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/MerchantPricingCatalogClient.java', 'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', '批量下单读取客户可用物流渠道'), feign('order/src/main/java/com/baiyue/order/client/MerchantWarehouseCatalogClient.java', 'order-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '批量下单分页读取启用仓库'), feign('order/src/main/java/com/baiyue/order/client/OrderBatchFileClient.java', 'order-service', 'file-service', ['REQUEST'], 'WRITE', 'NONE', '批量导入文件上传无通用幂等键'), 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/HttpFinanceEventClient.java', 'event-bridge-service', 'finance-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '跨区费用 Inbox 按 eventId 投递 Finance'), 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', 'lastleg-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'IAM 安全事件投影按 eventId/版本单调幂等'), http('gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpOverseasSecurityValidationClient.java', 'gateway-overseas', 'lastleg-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/customervisit/gateway/HttpCustomerVisitFileGateway.java', 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '回访附件上传无幂等键,归属校验可重复'), 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('->'))) }