10. 难点总表 · 哪些是真的,哪些是没做的
这一章是整本手册里最重要的一章。
前面九章讲的是"这个项目做了什么"。 这一章讲的是"这个项目没做什么"——以及怎么体面地讲没做的东西。
面试时把没做的说成做了,是当场翻车的最快方式: 面试官只要追问一句"这个 0.8 和 0.2 你怎么定的", 你就会开始编,而编的东西经不住第二个追问。
一、十大真实难点(按含金量排序)
这十条是从前九章里提炼的最值得单独拿出来讲的困难。 每一条都是"真实发生过、源码里能看到痕迹"的问题。
难点 1 · 让 LLM 碰不到钱
归属:亮点 2 · 线路成本 AI 因子
| 业务问题 | 业务要求"成本算得更准",但没人能写清要考虑哪些因素 |
| 错误做法 | 把整条路线信息丢给 LLM,让它直接返回一个 finalCost |
| 正确做法 | Java 先用高德算出确定的 baseCost;LLM 只返回 9 个乘性修正因子,每个被钳制在 [0.95, 1.10],乘积再钳制在 [0.85, 1.25];最终金额由 Java 计算 |
| 为什么难 | LLM 会非常自信地给出看起来专业的数字,且无法验证 |
| 怎么防 | 三层防线:Prompt 约束("不要输出 finalCost")→ 单因子钳制(越界/为 null → 1.0)→ 乘积钳制 → DTO 里根本没有 finalCost 字段(用类型系统做防御) |
| 加分点 | null 因子单独打回而不是整体打回,保住了其他 8 个因子的信息量;reason 字段入库可审计 |
追问预案:
- "为什么是 9 个因子?" → 业务访谈 + 整体浮动范围可控(9×10% = ±25%,业务能接受)
- "0.95~1.10 这个区间怎么定的?" → 诚实回答:和业务一起定的保守值,理想情况应该用历史真实结算成本做回归校准
- "AI 挂了怎么办?" → 因子全 1.0 → multiplier = 1.0 → finalCost = baseCost,
退化成最朴素的算法,建线路依然成功;但当前代码没有 try-catch,这是要改的
难点 2 · check-then-act 的并发问题
归属:亮点 3 · 服务范围
// ScopeServiceImpl#saveOrUpdate
var entity = mongoTemplate.findOne(query, ServiceScopeEntity.class);
if (null == entity) {
entity = new ServiceScopeEntity(); // 并发时两个线程都进这里
...
}
mongoTemplate.save(entity); // → 两条重复文档为什么难:它不会报错,只会产生数据不一致。而且:
@Indexed的bid和type都不是 unique;- 后续的
findOne会因为返回多行抛IncorrectResultSizeDataAccessException; - 管理后台"重复点保存"这种场景必然触发。
解法:@CompoundIndex(unique = true) + 改用 MongoDB 原生 upsert(Update.setOnInsert)。
为什么这条值钱:它是教科书级的并发问题,而且每个做过 MongoDB 的后端都能对上号。
难点 3 · Double 算钱
归属:亮点 1 · 运费
private Double firstWeight;
private Double continuousWeight;
double expense = carriageTemplate.getFirstWeight()
+ ((weight - 1) * carriageTemplate.getContinuousWeight());而且这个类已经 import java.math.BigDecimal 了,却一次都没用。
为什么难:单笔差额只有 1e-16,但累加到财务报表就是真实资损, 对账时会出现一批"差 1 分钱"的脏数据,排查极其痛苦。
解法:金额一律 BigDecimal + 明确舍入模式。 注意 BigDecimal.valueOf(double) vs new BigDecimal(double) 的区别(高频追问)。
难点 4 · 补偿机制本身制造了消息丢失
归属:亮点 5 · MQ
// MQServiceImpl 三处 FailMsgEntity.builder()
FailMsgEntity.builder()
.exchange(exchange)
.msgId(correlationData.getId())
.msg(jsonMsg)
.reason(result.getReason()).build();
// ↑ 没有 .routingKey(routingKey)// FailMsgJob 重发用的是 null
this.mqService.sendMsg(failMsgEntity.getExchange(),
failMsgEntity.getRoutingKey(), // ← NULL
failMsgEntity.getMsg());
this.failMsgService.removeById(failMsgEntity.getId()); // ★ 还删了记录为什么难:五层兜底设计得很完整(confirm / retry / 落库 / 补偿 / 死信), 但第 4 层因为漏了一个字段而完全失效,而且失效方式是静默的。
教训(这句一定要讲):
可靠性设计的每一层都要独立验证,不能假设上一层是对的。 测试会通过、演示会成功,只有在真正丢消息时才暴露。
难点 5 · mutate().header() 的返回值被丢弃
归属:亮点 6 · 网关
exchange.getRequest().mutate().header("userInfo", json); // ← 返回值被丢弃
exchange.getRequest().mutate().header("token", token);
// 正确:
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("userInfo", json).header("token", token).build();
exchange = exchange.mutate().request(mutated).build();为什么难:它让整条身份透传链路静默失效。 更糟的是 X-Request-From 是在 YAML 里配的(真生效了), userInfo 是在代码里加的(没生效)——只验前者的话这个 Bug 完全不可见。
叠加问题:Feign 侧还有两处 (@ConditionalOnProperty(cc.feign.user) 全仓库未配置 → 拦截器从未注册; 且头部值写成 "cc-chaincloud-gateway" 而校验的是 "chain-cloud-gateway")。
教训:
隐式约定的传递链路,中间任何一环断了都不会报错,只会让下游"看起来正常地"拒绝你。 所以信任边界上的传递必须有端到端集成测试。
难点 6 · 一个字段决定补偿任务成败
归属:亮点 4 · 调度
private Long selectCourierByAI(List<Long> courierIds, ...) {
if (courierIds.size() == 1) return courierIds.get(0);
// TODO AI智能选择快递员
return courierIds.get(1); // ← 永远取第二个
}为什么难:它能通过测试、能上线、不会抛异常, 但会造成"候选列表里的第一个快递员永远接不到单"—— 如果候选 SQL 是 ORDER BY id,那就是ID 最小的(通常最空闲的)快递员被永久跳过。
这是最贵的一类 bug:不报错,只是慢慢地把资源倾斜掉。
难点 7 · Seata 回滚不了 MQ
归属:亮点 7 · 分布式事务
@GlobalTransactional
public boolean pickup(TaskPickupVO vo) {
...
orderFeign.orderPickup(dto); // Seata 管
this.mqFeign.sendMsg(...); // ★ Seata 管不了
return true;
}为什么难: 第 2 步失败 → Seata 回滚两个数据库 → 但第 3 步的 MQ 消息已经发出去了,不会撤回。 下游收到"已揽收"消息,去查订单却是"待取件"。
这就是"幽灵消息",全项目没有任何一处用 outbox 或 @TransactionalEventListener(AFTER_COMMIT) 来避免它。
难点 8 · finally { unlock() } 覆盖了真正的业务异常
归属:亮点 7 / 8 · trade 服务 4 处
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
return true;
}
throw new CCException(TradingEnum.NATIVE_QUERY_FAIL); // 清晰的业务异常
} finally {
lock.unlock(); // ← tryLock 返回 false 时,这把锁不是当前线程的
} // → IllegalMonitorStateException 覆盖掉上面的异常为什么难: 原本清晰的业务异常被底层框架异常覆盖, 排查时看到的堆栈是 IllegalMonitorStateException 而不是业务错误。
对比:driver 服务的 intoStorage 写对了(finally 在 if 里面)。 同一项目里一处对一处错,说明不是不会,是没统一。
难点 9 · 显式实现状态机的"已完成"分支
归属:亮点 7 · driver 服务
if (lock.tryLock()) {
try {
TransportTaskDTO task = this.transportTaskFeign.findById(transportTaskId);
if (ObjectUtil.equalsAny(task.getStatus(),
TransportTaskStatusEnum.CANCELLED,
TransportTaskStatusEnum.COMPLETED)) {
return; // ← ★ 必须是 return,不是 throw
}
// 真正的业务
} finally { lock.unlock(); }
}为什么难:
if (已结束) {
throw new CCException("任务已结束"); // ❌ 会触发 Seata 全局回滚
}一个"重复提交,应该被忽略"的请求, 会变成"把整个全局事务回滚"——包括其它服务已经成功的操作。
这一条最能体现"理解分布式事务语义", 因为它区分了"看过代码"和"理解语义"两个层级。
难点 10 · 大文档 + 高频更新的内在矛盾
归属:亮点 9 · 轨迹
// 规划折线:一次写入,3MB
track.setPlanGeoJsonLine(...);
// 实时位置:每 30 秒覆盖一次
Update update = Update.update("lastPoint", new GeoJsonPoint(lng, lat))...;为什么难: lastPoint 的每次上报都要读写这个 3MB 的文档。 $set 虽然是字段级更新,但 MongoDB 仍需读取整页。 几百个车辆 × 每 30 秒 = 巨大的 IO。
解法:拆成两个集合(cc_track 轻量高频 + cc_track_plan 重只写一次)
- 写入前 Douglas-Peucker 抽稀(点数降 90%+,直接解决源码里那行 TODO)。
二、实现状态审计表(说错一个就翻车)
核对方式:逐文件读
.java源码 + 全仓库 grep 交叉验证。 README 里写了不等于代码里有。
2.1 AI 能力:10 个 Agent,落地 2 个
| Agent ID | 功能 | Java 侧状态 | 证据 | 面试该怎么说 |
|---|---|---|---|---|
| 1001 | 登录风控 | ✅ 已实现 | RiskControlServiceImpl#assessLoginRisk | 可以讲,但要说"目前只落了 2 个" |
| 1002 | Token 刷新风控 | ❌ return null | RiskControlServiceImpl:74 | "设计了契约,未实现" |
| 1003 | 自然语言运费计算 | ❌ 无 Controller | DTO ComputeCarriageResponse 存在但未用 | "参与设计" |
| 1004 | 物品识别 | ❌ 无 Controller | DTO ParcelItemCheckResponse 存在 | "参与设计" |
| 1005 | 机构类型分类 | ❌ 无 Controller | DTO OrganTypeResponse 存在 | 重点讲 resolveOrganType 的关键字匹配缺陷 |
| 1006 | 路线成本因子 | ✅ 已实现 | CostFactorServiceImpl#identifyCostFactor | 最值得讲的案例 |
| 1007 | 服务范围推荐 | ❌ Controller 返回 null | ScopeController:172 // TODO 待实现 | 讲设计(先粗后精召回) |
| 1008 | 快递员智能调度 | ❌ return courierIds.get(1) | OrderMQListener:104 | 必须承认没实现 |
| 1009 | 运输订单优先级 | ❌ Listener 空 + Feign 无此方法 | TransportOrderDispatchMQListener:55 | 讲 ZSet 方案设计 |
| 1010 | 运输任务预测 | ❌ Job 空 | DispatchJob:30 | 讲特征工程设计 |
AIGCFeign 只有 2 个方法(assessLoginRisk / identifyCostFactor), 没有任何调度相关的方法 —— 这意味着 Agent 1008/1009/1010 在 Java 侧连调用入口都没有。
2.2 README 承诺 vs 代码现实
| 能力 | README 的说法 | 代码现实 | 风险等级 |
|---|---|---|---|
| 运单智能合并 ZSet | "XGBoost 预测合并概率,Redis ZSet 实现优先级队列(AI 80% + 等待时间 20%)" | 全仓库零个 ZSet 调用(opsForZSet/zadd/reverseRangeWithScores 只在注释里) | 🔴 高危 |
| Caffeine + Redis 多级缓存 | "已落地" | pom.xml 里有 Caffeine 依赖,但全仓库零个 @Cacheable/@CacheEvict,只有 TransportInfoApplication 一个 @EnableCaching | 🔴 高危 |
| 双 Token 三验证 | "JWT 双 Token 三验证" | 双 token 是真的;"三验证"这个词只在文档里,代码里没有这个概念 | 🟡 中 |
| Seata 分布式事务 | "分布式事务:Seata 1.7.1" | OMS 里零个 @GlobalTransactional;真正在用的是 web-customer / web-courier / driver / web-manager | 🟡 中 |
| Redis 分布式锁 | "Redisson 实现" | dispatch 里只有 Bean 零调用;真正在用的是 driver(4 处)和 trade(8 处) | 🟡 中 |
| 轨迹抽稀 | 未在 README 承诺 | 代码里只有一行 //TODO ...可能会出现内存溢出的情况 | 🟢 低(正好是亮点) |
| 11 个 AI 业务场景 | "覆盖 11 个 AI 业务场景" | Agent 列表是 10 个,实际落地 2 个 | 🔴 高危 |
2.3 全仓库"声明了但没被使用"的常量
交换机(17 个里 6 个完全没人用):
cc.exchange.topic.transport ← 无生产者无消费者
cc.exchange.topic.driverJob ← 有队列定义,无监听器
cc.exchange.topic.driver ← 无
cc.exchange.topic.user ← 无
cc.exchange.topic.truck ← 无
cc.exchange.topic.system ← 无
cc.exchange.topic.aigc ← 无(README 提到"训练样本")队列(21 个里 1 个死的):
cc.queue.oms.courier.pickup.update.order ← 无监听器
cc.queue.track.transportOrder.created ← ★ 无生产者!track 在等一条没人发的消息路由键(18 个里 5 个死的): CREATE / UPDATE / DELETE / COMPLETE / COURIER_UPDATE_ORDER / COURIER_DISPATCH
锁前缀(2 个里 1 个死的): LOCKS.DISPATCH_LOCK_PREFIX —— 声明了,零使用。
其它死代码:
| 类 / 常量 | 说明 |
|---|---|
CarriageExceptionEnum.NOT_FOUND | 定义了,全仓库从未 throw |
aigc 域 8 个 Response DTO | 契约设计好了,逻辑没实现 |
dispatch/ml/model/*(5 个类) | 特征工程设计稿,零引用 |
dispatch/dto/DispatchMsgDTO | 零引用 |
dispatch/job/DispatchContext | 从未被 new |
EconomicRegionEnum.codeOf(String) | 手写 switch,零调用 |
TradingConstant.WECHAT_REFUND_ABNORMAL | 值还是错的("TRADE_CLOSED") |
TradingCacheConstant.REDIS_LEASETIME | 定义了,零使用 |
TradingCacheConstant.PAY_CHANNEL_LIST | 定义了,零使用 |
TransportLineUtils.convert(PathValue) | findLowestPath 内联逻辑的重复实现,零调用 |
resources/templates/aigc-cost-factor.txt | 和 Java 文本块内容完全相同,但没有任何地方加载 |
CostConfigurationDTO.cost 的注释 | 说默认"0.8/1.2/1.5",代码是"0.6/1.0/1.5" |
NoAuthorization 注解 | 定义了、被识别了、零使用 |
2.4 契约失配
| 契约 | 问题 |
|---|---|
cc_queue 里 OMS_TRADE_UPDATE_STATUS | 名字 cc.queue.oms.trade.update.Status —— 大写 S,与其它命名规范不一致 |
DispatchMsgDTO 的生产方式 | work 服务用 MapUtil.builder().put("transportOrderId", ...) 拼 Map 发 JSON,不是 DispatchMsgDTO —— 字段名靠约定耦合 |
MQFeign / AreaFeign 等 | 没有 fallbackFactory —— 目标服务挂了就直接抛异常,没有降级 |
TransportInfoMsg.organId | 没有任何生产者赋值它 |
RefundRecordFeign | fallbackFactory 错配 |
AuthUserInfo | 靠 @Alias("sub"/"orgid"/"stationid") 兼容两种 JWT 结构 |
三、怎么体面地讲"没做的东西"
这是本手册最实用的一节。
3.1 三种错误说法
| ❌ 错误说法 | 为什么错 |
|---|---|
| "我实现了 ZSet 优先级队列,AI 80% + 等待 20%" | 一问"0.8 怎么定的"就崩 |
| "我们用了 Caffeine + Redis 多级缓存" | 一问"缓存 key 怎么设计的、怎么失效"就崩 |
| "双 Token 三验证" | 这是个不存在的概念,问的人会以为是三因子认证 |
3.2 四种正确说法
说法 A:区分"设计"和"实现"
"这个模块我们做了完整的方案设计但没落地。 我可以详细讲方案——为什么用 ZSet 而不是堆(要范围查询 + 天然去重)、 为什么加权要加等待时间(防饿死)、 为什么装不下要跳过而不是中断(避免大单阻塞小单)。 但我不能说实现了,因为落地时我判断优先级排在了后面, 它依赖的
work#orderToTransportOrder前置链路当时还是 TODO。"
说法 B:讲清楚"为什么没做"
"多级缓存我们没有做。当时的判断是: 运费模板虽然读多写少,但它的规模不大(不到 20 条), MySQL 的索引查询本身就在 1ms 以内, 在没有压测数据证明它是瓶颈之前,引入两级缓存是在为不存在的问题增加复杂度。 这是我的判断,现在回看是对的——但如果订单量上来十倍, 这个判断就要重新做了。"
说法 C:讲清楚"如果要做会怎么做"
"如果让我现在做多级缓存,我的方案是: L1 Caffeine 缓存运费模板对象,TTL 1 分钟; L2 Redis 缓存,key 用
carriage:template:{同城?}:{寄件省}:{收件省}; 失效不用@CacheEvict按 key 删(因为一个地址组合对应多条查询路径), 而是在 key 后缀挂一个配置版本号,改配置时版本号 +1, 老 key 自然过期。"
说法 D:主动暴露缺陷,换取信任
"这块代码有个问题我自己 review 出来了: 落库失败消息的时候没存
routingKey, 导致补偿任务重发时路由键是 null,消息发不出去, 而且因为发送方法是乐观返回true,补偿任务还会把记录删掉—— 等于补偿机制本身制造了消息丢失。 修复是一行代码。 这件事真正教我的是:可靠性设计的每一层都要独立验证,不能假设上一层是对的。"
3.3 一段可以直接背的开场
如果面试官问"你这个项目有什么不足",可以这么说:
"我梳理过三类不足:
第一类是设计完备但没实现的——AI 的 10 个 Agent 落地了 2 个, 运力的 ZSet 优先级队列只有设计和类定义。 这类我能详细讲方案,但我不会说"实现了"。
第二类是实现有缺陷的——我 review 出来至少 12 个问题, 包括网关身份透传的
mutate().header()用法错误、 MQ 失败落库漏存routingKey、 快递员调度永远返回候选列表第二个。 这些我都能说出根因和修复方案。第三类是设计本身有问题的——比如 freight 用 Double 算钱、 运力路径搜索用
-[*..10]->全路径枚举而不是 Dijkstra、 运费计算在下单主链路上没有任何缓存。 这类我是事后才意识到的。我觉得能说清楚这些比多讲一个技术亮点更有价值, 因为它说明我是在维护这个系统,而不是在写完就忘了。"
四、面试前 5 分钟自查清单
关于项目描述:
- 能用 90 秒讲清业务(一次快递的旅程)而不提任何技术名词
- 能说清为什么是 22 个微服务(按数据归属划分,不是按表数量)
- 能说清三个"另类"服务为什么不用 MySQL(图 / 空间 / 文档)
关于亮点:
- 每个亮点都能用"业务问题 → 为什么难 → 怎么解 → 代价"四段讲完
- 每个亮点都能说出至少一个真实的缺陷
- 每个亮点都能说出"如果重做我会怎么改"
关于诚实边界:
- 能背出"AI 落地 2 个 / 设计 10 个"
- 绝不说"我实现了 ZSet 优先级队列"
- 绝不说"我们用了 Caffeine + Redis 多级缓存"
- 绝不说"双 Token 三验证"
- 能解释"为什么这些没做"(而不是"没时间")
关于源码证据:
- 随便抽一个亮点,能说出文件名 + 行号
- 随便抽一个缺陷,能说出具体是哪几行
- 知道哪些常量/类是死代码
五、面试官可能的"陷阱式追问"
陷阱 1: "你们这个 ZSet 优先级队列的权重是怎么定的?" → 这是在测试你是不是编的。 正确回答:"这部分只做了设计没落地, 权重需要用历史数据做离线回放来调,我给不出一个真实的权重值。"
陷阱 2: "你们的多级缓存 key 是怎么设计的?失效策略是什么?" → 同样是在测试。正确回答:"我们没做。如果做的话, 我会用配置版本号做 key 后缀而不是按 key 删,因为一个地址组合对应多条查询路径。"
陷阱 3: "你说用了 Seata,那 Seata 的 TC 挂了会怎样?" → 这是在考 Seata 的真实理解。正确回答: "全局事务会挂起等待 TC 恢复,数据一致性不会丢,但业务会阻塞。 所以 Seata TC 必须高可用,生产上通常是三节点部署。 而且我们这个项目里 OMS 本身没用 Seata,用在 BFF 层, 这是我判断边界时的取舍——因为中间态用户可见。"
陷阱 4: "JWT 里的 payload 是 Base64 编码的,不是加密的,那不是可以随便看?" → 正确回答:"是的,payload 是签名不是加密, 任何人都能解出来看。所以我们 C 端的 token 里只放 userId, 不放手机号、不放密码、不放任何敏感信息。 这也是为什么我们不用 session 的原因之一—— session 内容在服务端,加密可控;而 JWT 的可见性无法通过算法控制。"
陷阱 5: "你这个预算是多少?日均多少单?" → 不要说编的数字。 正确回答:"这是一个学习项目,没有真实的生产流量。 我能说的是设计的量级——比如交易对账用了 XXL-Job 分片广播, 说明设计时考虑了百万级交易单的处理能力。"
六、附录:核心源码索引(按能力分类)
鉴权与安全
| 能力 | 文件 |
|---|---|
| 网关过滤器 | chain-cloud-gateway/.../filter/TokenGatewayFilter.java |
| 四端 Token 策略 | chain-cloud-gateway/.../filter/*TokenGatewayFilterFactory.java |
| JWT 工具 | chain-cloud-common/.../util/JwtUtils.java |
| 身份透传 | chain-cloud-common/.../interceptor/{User,Token}Interceptor.java |
| 双 Token | chain-cloud-ms-web-customer/.../service/impl/TokenServiceImpl.java |
| 登录记录 | chain-cloud-ms-auth-service/.../entity/LoginRecordEntity.java |
消息与一致性
| 能力 | 文件 |
|---|---|
| 消息发送/重试/补偿 | chain-cloud-mq/.../{service/impl/MQServiceImpl, job/FailMsgJob}.java |
| 死信 | chain-cloud-mq/.../config/ErrorMessageConfig.java |
| 常量体系 | chain-cloud-common/.../constant/Constants.java:33-340 |
| 分布式事务 | ms-web-customer/.../OmsServiceImpl:396、ms-driver-service/.../DriverJobServiceImpl:178 |
| 分布式锁 | ms-driver-service/.../DriverJobServiceImpl、ms-trade-service/.../*ServiceImpl |
计算与算法
| 能力 | 文件 |
|---|---|
| 运费计算 | ms-carriage-service/.../CarriageServiceImpl.java |
| 线路成本 + AI | ms-transport-service/.../TransportLineServiceImpl.java:117-197 |
| 图路径查询 | ms-transport-service/.../TransportLineRepositoryImpl.java |
| 直线距离 | ms-transport-service/.../utils/DistanceCalculator.java |
空间与轨迹
| 能力 | 文件 |
|---|---|
| 服务范围 | ms-service-scope-service/.../ScopeServiceImpl.java |
| 轨迹 | ms-track-service/.../TrackServiceImpl.java |
| 运输跟踪 | ms-transport-info-service/.../TransportInfoMQListener.java |
AI 集成
| 能力 | 文件 |
|---|---|
| AI 客户端 | ms-aigc-service/.../feign/AgentServerClient.java |
| 登录风控 | ms-aigc-service/.../RiskControlServiceImpl.java |
| 成本因子 | ms-aigc-service/.../CostFactorServiceImpl.java |
| 特征设计 | ms-dispatch-service/.../ml/model/*(零引用) |