难点总表与诚实边界

10 · 方法论

难点总表 · 哪些是真的,哪些是没做的

逐文件核对后的实现状态审计。面试时说错一个"实现了",比少讲一个亮点危险十倍。

涉及模块:全项目审计

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 · 服务范围

java
// ScopeServiceImpl#saveOrUpdate
var entity = mongoTemplate.findOne(query, ServiceScopeEntity.class);
if (null == entity) {
    entity = new ServiceScopeEntity();     // 并发时两个线程都进这里
    ...
}
mongoTemplate.save(entity);                // → 两条重复文档

为什么难:它不会报错,只会产生数据不一致。而且:

  1. @Indexed 的 bid 和 type 都不是 unique;
  2. 后续的 findOne 会因为返回多行抛 IncorrectResultSizeDataAccessException;
  3. 管理后台"重复点保存"这种场景必然触发。

解法:@CompoundIndex(unique = true) + 改用 MongoDB 原生 upsert(Update.setOnInsert)。

为什么这条值钱:它是教科书级的并发问题,而且每个做过 MongoDB 的后端都能对上号。


难点 3 · Double 算钱

归属:亮点 1 · 运费

java
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

java
// MQServiceImpl 三处 FailMsgEntity.builder()
FailMsgEntity.builder()
        .exchange(exchange)
        .msgId(correlationData.getId())
        .msg(jsonMsg)
        .reason(result.getReason()).build();
        // ↑ 没有 .routingKey(routingKey)
java
// FailMsgJob 重发用的是 null
this.mqService.sendMsg(failMsgEntity.getExchange(),
                       failMsgEntity.getRoutingKey(),   // ← NULL
                       failMsgEntity.getMsg());
this.failMsgService.removeById(failMsgEntity.getId());  // ★ 还删了记录

为什么难:五层兜底设计得很完整(confirm / retry / 落库 / 补偿 / 死信), 但第 4 层因为漏了一个字段而完全失效,而且失效方式是静默的。

教训(这句一定要讲):

可靠性设计的每一层都要独立验证,不能假设上一层是对的。 测试会通过、演示会成功,只有在真正丢消息时才暴露。


难点 5 · mutate().header() 的返回值被丢弃

归属:亮点 6 · 网关

java
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 · 调度

java
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 · 分布式事务

java
@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 处

java
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 服务

java
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(); }
}

为什么难:

java
if (已结束) {
    throw new CCException("任务已结束");    // ❌ 会触发 Seata 全局回滚
}

一个"重复提交,应该被忽略"的请求, 会变成"把整个全局事务回滚"——包括其它服务已经成功的操作。

这一条最能体现"理解分布式事务语义", 因为它区分了"看过代码"和"理解语义"两个层级。


难点 10 · 大文档 + 高频更新的内在矛盾

归属:亮点 9 · 轨迹

java
// 规划折线:一次写入,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 个"
1002Token 刷新风控❌ return nullRiskControlServiceImpl:74"设计了契约,未实现"
1003自然语言运费计算❌ 无 ControllerDTO ComputeCarriageResponse 存在但未用"参与设计"
1004物品识别❌ 无 ControllerDTO ParcelItemCheckResponse 存在"参与设计"
1005机构类型分类❌ 无 ControllerDTO OrganTypeResponse 存在重点讲 resolveOrganType 的关键字匹配缺陷
1006路线成本因子✅ 已实现CostFactorServiceImpl#identifyCostFactor最值得讲的案例
1007服务范围推荐❌ Controller 返回 nullScopeController: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 个完全没人用):

text
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 个死的):

text
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没有任何生产者赋值它
RefundRecordFeignfallbackFactory 错配
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
双 Tokenchain-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
线路成本 + AIms-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/*(零引用)