16|(前端转全栈)前端人排查后端问题:curl、traceId、日志、MySQL、Redis 怎么用?

GitHub仓库github.com/2530622506/...

01|(前端转全栈)前端人第一次打开 Spring Boot 项目,应该先看哪里?

02|(前端转全栈)从 pnpm dev 到 Spring Boot 启动:后端服务到底怎么跑起来?

03|(前端转全栈)Axios 请求进了后端之后:Controller、Request、Response 是怎么接住它的?

04|(前端转全栈)前端状态为什么不够用?从页面数据到 MySQL 持久化

05|(前端转全栈)不手写一堆 SQL,后端怎么操作数据库?MyBatis-Plus 入门

06|(前端转全栈)登录后端到底在做什么?JWT、Spring Security 和权限链路

07|(前端转全栈)为什么后端也要缓存?从前端缓存思维理解 Redis

08|(前端转全栈)一个商品详情接口背后的完整链路:HTTP、Redis、MySQL 与 JSON

09|(前端转全栈)商品为什么不能随便上下架?后端状态机思维入门

10|(前端转全栈)库存扣减为什么最容易出事故?SKU、并发与原子更新

11|(前端转全栈)购物车不能只存在前端:用户维度数据如何在后端落库

12|(前端转全栈)点击提交订单后,后端如何用事务守住价格、库存和订单?

13|(前端转全栈)支付成功不等于结束:回调、幂等、超时关单和状态竞争

14|(前端转全栈)商品详情高频访问怎么扛?Redis Cache Aside 实战

15|(前端转全栈)从一个副标题字段看懂后端完整交付链路

本篇是本系列阶段性收束。你已经读过 Java、Spring Boot、Controller、MySQL、MyBatis-Plus、JWT、Redis、商品、SKU、购物车、订单、支付和副标题实战。现在要建立一套后端排查方法论。文中的前端代码均为"前端侧示意代码",用于类比浏览器 DevTools 和 API 调试习惯。本篇基于当前工程的 ApiResponseApiCodeGlobalExceptionHandlerTraceIdFilterSecurityConfigProductDetailCacheService、测试类和 README 命令。

1. 这篇解决什么问题

前端开发排查问题时,常用路径是:打开浏览器 DevTools,看 Network、Console、Application、Elements;确认请求 URL、HTTP 状态、响应 JSON、localStorage token、页面状态和组件 props。转后端后,这些习惯仍然有用,但不够。因为后端问题可能发生在 Security Filter、Controller 参数绑定、Bean Validation、Facade 业务规则、事务、Mapper SQL、MySQL 约束、Redis 缓存、定时任务、外部回调和测试环境。

本篇要帮你建立一个后端排查闭环:

text 复制代码
现象 → HTTP 状态码 → ApiResponse.code → traceId → 日志 → Controller → Facade → DbService/Mapper → MySQL → Redis → 测试复现

你会学习:

  1. 如何读统一响应 ApiResponse
  2. 如何区分 400、401、403、404、409、500;
  3. 为什么 traceId 是前后端协作排查的关键;
  4. curl 比浏览器更适合隔离后端问题的原因;
  5. 如何判断问题发生在 Security、Controller、Facade、数据库还是 Redis;
  6. 如何用 MySQL 和 Redis CLI 验证事实来源;
  7. 如何从测试名中理解业务规则;
  8. 常见后端坑如何避免。

前端同学最需要转变的地方是:不要只问"页面为什么不对",而要问"后端链路在哪一层已经和预期不一致"。一旦能定位层次,问题通常就不难修。

2. 用前端知识类比

前端 DevTools 的 Network 面板会告诉你:请求 URL、方法、请求头、请求体、状态码、响应体。后端排查也从这些信息开始,但会继续向服务内部深入。

前端排查动作 后端对应动作 本项目例子
看 Network URL 看 Controller 路径映射 /api/products/{id}/api/admin/products/{id}/subtitle
看 HTTP status 看 Security / ExceptionHandler 401、403、404、409、500
看响应 JSON ApiResponse.code PRODUCT_NOT_FOUNDVALIDATION_ERROR
看 Request Headers 看 JWT、Idempotency-KeyX-Mock-Payment-Secret 订单幂等、支付回调
看 localStorage token JwtAuthenticationFilter 解析结果 当前用户和角色
看页面 state 看数据库字段和缓存值 mall_order.status_code、Redis key
Console log 服务端日志和 traceId product_detail_cache event=HIT
Mock 接口复现 JUnit / MockMvc / Testcontainers PaymentMySqlContainerTest

前端侧示意代码:

ts 复制代码
// 前端侧示意代码:出现接口错误时,不要只展示 message,要记录 code 和 traceId。
try {
  await request.patch(`/api/admin/products/${id}/subtitle`, { subtitle })
} catch (error: any) {
  const body = error.response?.data
  console.error('后端错误', {
    httpStatus: error.response?.status,
    code: body?.code,
    traceId: body?.traceId,
    data: body?.data,
  })
}

后端的 traceId 类似你给一次请求贴的排查编号。前端把它提供给后端,后端就能在日志里找到同一次请求的异常栈或业务日志。

3. 先读懂统一响应

本项目所有业务 API 都使用 ApiResponse<T> 作为响应信封:

java 复制代码
private String code;
private String message;
private T data;
private String traceId;
private Instant timestamp;

前端逻辑应该优先依赖 code,不要解析 messagemessage 是给人看的中文提示,未来可能改文案;code 是稳定业务码。例如:

  • SUCCESS:成功;
  • VALIDATION_ERROR:请求参数校验失败;
  • MALFORMED_JSON:JSON 语法错误或枚举转换失败;
  • UNAUTHORIZED:未登录或 Token 无效;
  • FORBIDDEN:已登录但权限不足;
  • PRODUCT_NOT_FOUND:商品不存在或公开不可见;
  • ORDER_STATUS_CONFLICT:当前订单状态不允许操作;
  • PAYMENT_CALLBACK_SECRET_INVALID:模拟支付回调密钥错误;
  • INTERNAL_ERROR:未处理服务端异常。
flowchart TD A[接口响应] --> B[HTTP status] A --> C[ApiResponse.code] A --> D[traceId] B --> E[判断大类] C --> F[判断业务原因] D --> G[关联服务端日志]

很多前端同学只看 HTTP status,不看业务码。比如 404 可能是 URL 不存在,也可能是商品未上架被公开接口当成不存在;409 可能是库存版本冲突,也可能是订单状态不允许取消;400 可能是字段校验失败,也可能是 JSON 格式错误。业务码能把这些情况区分开。

4. HTTP 状态码排查表

4.1 400:请求格式或字段校验问题

400 常见来源有两类。第一类是 @Valid 校验失败,例如副标题为空、注册用户名太短、订单幂等 key 格式不对。GlobalExceptionHandler.handleValidation 会返回 VALIDATION_ERROR,并在 data.fieldErrors 中列出字段名和提示。

第二类是 JSON 解析失败,例如请求体不是合法 JSON、枚举值写错、数字字段传字符串。handleMalformedJson 会返回 MALFORMED_JSON。如果你发现 Controller 断点没有进,但返回 400,大概率问题发生在参数绑定阶段。

4.2 401:未登录或 Token 无效

401 发生在 Controller 之前,由 RestAuthenticationEntryPoint 返回。常见原因:没有 Authorization 头、不是 Bearer xxx 格式、Token 过期、Token 签名不对。前端排查时先看 Network 的 Request Headers。

bash 复制代码
curl -i http://localhost:8080/api/auth/me

预期会返回 401。加上正确 token 后才会返回当前用户。

4.3 403:已登录但权限不足

403 表示后端认识你,但你没有权限。普通用户访问 /api/admin/** 就会被 RestAccessDeniedHandler 返回 FORBIDDEN。这和前端路由守卫不同:即使页面没有显示按钮,用户直接 curl 管理端接口也必须被后端拦住。

flowchart TD A[请求进入 Security] --> B{Token 是否有效} B -->|否| C[401 UNAUTHORIZED] B -->|是| D{URL 是否要求 ADMIN} D -->|否| E[继续进入 Controller] D -->|是| F{用户是否 ADMIN} F -->|否| G[403 FORBIDDEN] F -->|是| E

4.4 404:URL 不存在或业务对象不存在

如果 URL 写错,比如 /api/product/1 少了 s,会由 NoResourceFoundException 统一转成 NOT_FOUND。如果商品不存在,可能返回 PRODUCT_NOT_FOUND。两者 HTTP status 都是 404,但业务码不同。排查时先看 code

商品公开详情有一个特殊点:未上架商品也会对匿名用户表现为 PRODUCT_NOT_FOUND,这是为了不泄露草稿或下架商品。不要因为数据库里有 id,就认为公开接口一定应该返回。

4.5 409:业务状态冲突

409 通常说明请求格式没错、用户也有权限,但当前业务状态不允许操作。例如商品状态流转不合法、库存版本冲突、订单已支付不能取消、支付流水已关闭不能成功回调。前端看到 409 时,不应该盲目重试,而应该刷新状态或提示用户。

4.6 500:未预期服务端异常

500 会被 GlobalExceptionHandler.handleUnknownException 转成 INTERNAL_ERROR,详细堆栈只写服务端日志,对外不暴露内部实现。排查 500 时,前端最有价值的信息不是截图,而是 traceId、请求 URL、请求体和发生时间。

5. traceId 怎么用

TraceIdFilter 会为每次 HTTP 请求生成或透传 X-Trace-Id。如果前端请求头里带了合法 traceId,后端会继续使用;如果没有或格式非法,后端生成一个新的,并写回响应头和响应体。

sequenceDiagram participant FE as 前端或 curl participant Filter as TraceIdFilter participant API as 后端业务 participant Log as 服务端日志 FE->>Filter: 请求可选 X-Trace-Id Filter->>Filter: 校验或生成 traceId Filter->>API: request attribute 带 traceId API->>Log: 日志记录 traceId API-->>FE: 响应体和响应头返回 traceId

手动指定 traceId:

bash 复制代码
curl -i http://localhost:8080/api/products/1 \
  -H 'X-Trace-Id: debugProduct001'

如果响应里也有 debugProduct001,说明链路编号透传成功。后端日志中如果出现异常,就可以搜索这个 traceId。

6. curl 是后端调试的第一工具

浏览器很方便,但它会混入跨域、缓存、页面状态、拦截器、路由、组件渲染等因素。curl 更适合隔离后端:你明确写 URL、方法、header、body,看到原始 HTTP 状态和响应。

常用模板:

bash 复制代码
# GET,显示状态码和响应头
curl -i 'http://localhost:8080/api/products/1'

# POST JSON
curl -i -X POST 'http://localhost:8080/api/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"Admin123456"}'

# 带 JWT
curl -i 'http://localhost:8080/api/auth/me' \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# 带幂等 key
curl -i -X POST 'http://localhost:8080/api/orders' \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H 'Idempotency-Key: debug-order-001'

# 模拟支付回调密钥
curl -i -X POST 'http://localhost:8080/api/mock-payments/callback' \
  -H 'X-Mock-Payment-Secret: local-mock-payment-secret' \
  -H 'Content-Type: application/json' \
  -d '{"paymentNo":"PAY-xxx","callbackId":"cb-001","providerTransactionNo":"mock-tx-001","result":"SUCCESS"}'

如果 curl 正常、页面异常,问题多半在前端请求构造、token 保存、拦截器、状态更新或渲染。如果 curl 也异常,继续沿后端链路排查。

7. 从 Controller 到数据库逐层定位

后端排查不要跳层。推荐顺序:

flowchart TD A[接口现象] --> B[确认 curl 可复现] B --> C[看 HTTP status 和 code] C --> D{Controller 是否进入} D -->|否| E[查 URL Security 参数绑定] D -->|是| F[查 Facade 业务分支] F --> G[查 DbService 和 Mapper] G --> H[查 MySQL 最终数据] H --> I[查 Redis 是否旧值] I --> J[补测试锁定问题]

7.1 Controller 没进

可能是 URL 写错、HTTP 方法错、Security 拦截、JSON 格式错、@Valid 校验失败。看状态码:401/403 多半是 Security;400 多半是参数绑定或校验;404 多半是 URL 或业务对象。

7.2 Controller 进了但 Facade 结果不对

看 Facade 是否拿到了正确当前用户、是否 trim 了输入、是否进入了正确业务分支。例如商品详情 getPublishedProduct 中,NULL_HIT 会直接恢复 404,MISS 才查数据库。

7.3 DbService 影响行数不对

更新类接口要看 UPDATE 影响行数。库存、订单、支付更依赖影响行数判断并发胜负。影响行数为 0 不一定是 SQL 失败,可能是旧状态条件不满足。

7.4 MySQL 正确但接口不正确

检查 Response 组装和 Redis 缓存。副标题修改就是典型例子:MySQL 已经是新值,但公开详情仍旧,可能是旧缓存没有删除,或者 ProductDetailResponse / toDetailResponse 没带新字段。

8. MySQL 和 Redis 的手动检查

8.1 MySQL 是事实来源

商品:

bash 复制代码
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
  -e "SELECT id, title, subtitle, status_code, updated_at FROM mall_product WHERE id = 1;"

库存:

bash 复制代码
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
  -e "SELECT id, product_id, available_stock, locked_stock, version FROM mall_product_sku WHERE id = 1;"

订单和支付:

bash 复制代码
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
  -e "SELECT id, order_no, status_code, total_amount, expires_at FROM mall_order ORDER BY id DESC LIMIT 5;"

docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
  -e "SELECT id, payment_no, order_id, status_code, callback_id FROM mall_payment ORDER BY id DESC LIMIT 5;"

8.2 Redis 是性能层

商品详情缓存:

bash 复制代码
docker exec fullstack-mall-redis redis-cli GET mall:product:detail:v1:1
docker exec fullstack-mall-redis redis-cli TTL mall:product:detail:v1:1
docker exec fullstack-mall-redis redis-cli DEL mall:product:detail:v1:1

如果 MySQL 是新值、Redis 是旧值,问题在缓存失效或 TTL;如果 MySQL 也是旧值,问题在写入链路。

flowchart TD A[页面看到旧商品详情] --> B[查 MySQL] B --> C{MySQL 是否新值} C -->|否| D[排查写接口和事务] C -->|是| E[查 Redis key] E --> F{Redis 是否旧值} F -->|是| G[排查 evict 和 TTL] F -->|否| H[排查 Response 组装或前端缓存]

9. 用测试理解业务规则

本项目测试名本身就是业务文档。例如:

测试类 测试名表达的规则
ProductFacadeCacheTest 缓存命中不查库、MISS 回填、Redis 降级仍查 MySQL、创建和状态变化会删除缓存
ProductDetailCacheServiceTest 坏 JSON 删除自愈、空值缓存恢复业务码、写入或删除失败 fail-open
OrderControllerTest 重复幂等 key 返回原订单、取消只释放一次库存、普通用户不能读别人订单
PaymentControllerTest 回调需要密钥、重复回调幂等、取消后迟到回调被拒绝、超时关单释放库存
PaymentMySqlContainerTest 并发创建支付只有一条流水、回调和关单竞争只能有一个一致赢家
PaymentTransactionRollbackTest 库存确认或释放失败时订单和支付流水一起回滚

前端同学常把测试当"提交前跑一下的东西",后端更应该把测试当成可执行业务规则。你不确定某个边界应该怎么处理时,先找测试名;没有测试,就补一个最小复现测试。

10. 常见坑和修复方向

10.1 看到 404 就以为 Controller 不存在

404 有两种:URL 不存在的 NOT_FOUND,业务对象不存在的 PRODUCT_NOT_FOUND / ORDER_NOT_FOUND。先看业务码。

10.2 看到 403 就刷新 Token

403 不是 Token 过期,而是权限不足。刷新 Token 不能让普通用户变管理员。应该检查账号角色和 URL 权限规则。

10.3 只看 message,不看 code

中文 message 可能变化,前端逻辑应该按稳定 code 分支。后端排查也要记录 code。

10.4 用页面复现复杂问题

页面复现受太多因素影响。先用 curl 把后端问题最小化,再回到页面。

10.5 忘记查看请求头

JWT、Idempotency-KeyX-Mock-Payment-Secret 都在请求头。很多订单和支付问题不是 body 错,而是 header 缺失或错误。

10.6 认为 Redis 里的数据就是最新

Redis 可能是旧缓存。商品详情以 MySQL 为准,排查旧值要同时查 MySQL 和 Redis。

10.7 忽略事务和并发

订单、支付、库存问题不能只看单线程代码。要看 @Transactional、行锁、条件 UPDATE、唯一索引和影响行数。

10.8 本地 profile 和测试环境不一致

local profile 可能使用 H2 并默认关闭缓存;默认 profile 连接 MySQL 和 Redis。排查时先确认当前启动参数和环境变量。

10.9 只改正式 SQL,忘记测试 SQL

字段和表结构要同步正式 schema、H2 test schema、MySQL container schema 和种子数据。否则"本地能跑、测试失败"很常见。

10.10 不保存 traceId

前端报 bug 时如果只有"页面报错了",后端很难定位。带上 traceId、URL、请求体、时间和账号,排查效率会高很多。

11. 三个典型排查案例

11.1 管理员修改副标题后公开详情还是旧值

排查顺序:第一,用 curl 直接调用修改接口,确认不是页面没发请求;第二,看返回 data.subtitle 是否是新值;第三,查 MySQL 的 mall_product.subtitle;第四,查 Redis key mall:product:detail:v1:{id};第五,看日志是否出现 product_detail_cache event=EVICTDEGRADED operation=EVICT;第六,删除 Redis key 后重新请求公开详情。

如果删除 Redis 后公开详情变新,说明缓存失效问题;如果删除后仍旧,说明 Response 组装或数据库写入问题;如果 MySQL 本身旧,回到 ProductDbService.updateSubtitle 看影响行数。

11.2 创建订单重复扣库存

排查顺序:看请求头 Idempotency-Key 是否相同;查 mall_order.idempotency_key 是否有唯一约束;查订单项是否重复;查 SKU 的 available_stocklocked_stock;看 OrderControllerTest.sameIdempotencyKeyReturnsOriginalOrderWithoutLockingAgain 是否覆盖;如果是并发问题,看 MySQL 容器测试和 SkuMapper.lockStock 的条件 UPDATE。

11.3 支付成功和超时关单抢同一个订单

排查顺序:查订单 status_code、支付 status_code、支付 callback_id、订单 expires_at;看回调 secret 是否正确;看 markPaidcloseExpiredOrder 的条件 UPDATE 谁影响行数为 1;看库存 locked_stock 是否被确认或释放;参考 PaymentMySqlContainerTest.callbackAndTimeoutCloseHaveOneConsistentWinner

flowchart LR A[复杂交易问题] --> B[查订单状态] B --> C[查支付状态] C --> D[查库存字段] D --> E[看条件 UPDATE 影响行数] E --> F[对照并发测试]

12. 推荐的后端排查 checklist

每次遇到后端接口问题,可以按下面 checklist:

  1. 用 curl 复现,不先依赖页面;
  2. 记录 URL、HTTP 方法、请求头、请求体;
  3. 看 HTTP status;
  4. ApiResponse.code
  5. 保存 traceId
  6. 判断是否进入 Controller;
  7. 如果没进入,排查 Security、路径、JSON、校验;
  8. 如果进入了,排查 Facade 业务分支;
  9. 写操作看事务和数据库影响行数;
  10. 读旧值先查 MySQL,再查 Redis;
  11. 交易问题看订单、支付、库存三组字段;
  12. 找对应测试名理解预期;
  13. 没有测试时,补最小复现;
  14. 修复后同时验证成功路径和失败路径;
  15. 更新文档或笔记,避免下次重复踩坑。

13. 本章小练习

  1. 用 curl 构造一个 401、一个 403、一个 400、一个 404,并记录每个响应的 code
  2. 给请求加 X-Trace-Id: debugTrace001,确认响应体和响应头都能看到它。
  3. 手动把 Redis 商品详情 key 删除,再请求商品详情,观察 MISS 和 PUT 日志。
  4. 修改副标题后,分别查 MySQL 和 Redis,判断数据从哪里变旧。
  5. 阅读 GlobalExceptionHandler,说明 VALIDATION_ERRORMALFORMED_JSON 的区别。
  6. 阅读 RestAuthenticationEntryPointRestAccessDeniedHandler,说明 401 和 403 为什么发生在 Controller 前。
  7. 找一个测试类,把 5 个测试名翻译成中文业务规则。
  8. 设计一个"普通用户不能修改副标题"的测试用例名称和预期状态码。

14. 系列总结:前端转后端真正要迁移的能力

从第 1 篇到第 16 篇,你已经看到后端不是一堆注解的集合,而是一套围绕数据、边界和一致性的工程体系。前端经验并没有失效:组件分层可以迁移成 Controller / Facade / Service 分层,TypeScript interface 可以迁移成 Request / Response DTO,React Query 缓存可以类比 Redis Cache Aside,路由守卫可以类比 Security 授权,Network 面板可以迁移成 curl 和统一响应排查。

但后端多了几个核心责任:数据库是事实来源;权限必须在服务端强制执行;事务要保证多表一致;并发要靠数据库条件更新、行锁和唯一索引兜底;缓存是性能层而不是事实层;外部回调要鉴权和幂等;错误响应要稳定;日志和 traceId 要能支撑排查;测试要覆盖边界和竞争。

flowchart TD A[前端能力] --> B[HTTP 和 JSON] A --> C[状态管理] A --> D[组件分层] A --> E[调试 Network] B --> F[Controller 和 DTO] C --> G[Redis Cache Aside] D --> H[Facade 和 Service 分层] E --> I[curl traceId 日志] F --> J[后端工程能力] G --> J H --> J I --> J J --> K[数据库 事务 权限 并发 测试]

15. 后续学习建议

完成本系列后,建议你继续做 5 件事:

  1. 自己新增一个小字段,例如商品主图 URL,按第 15 篇流程完整改一遍;
  2. 自己新增一个只读接口,例如按分类查询商品数量,练习 Controller、Facade、DbService 和测试;
  3. 给副标题接口补齐自动化测试,覆盖管理员成功、普通用户 403、空值 400、不存在商品 404、缓存失效;
  4. 用真实 Redis 手动制造坏 JSON,观察自愈;
  5. 设计一个"收藏商品"模块,先写表结构和接口契约,再实现业务。

16. 本篇总结

后端调试的核心不是猜,而是分层定位。先用 curl 固定请求,再看 HTTP status 和业务 code,再用 traceId 关联日志,然后沿 Controller、Facade、DbService、Mapper、MySQL、Redis、测试逐层收缩范围。前端工程师转后端最大的优势是已经熟悉 HTTP、JSON、状态和调试工具;最大的挑战是要补上数据库事实来源、权限边界、事务一致性、并发控制和缓存降级这些后端责任。

如果你以后遇到问题时能自然地说出"先看 code 和 traceId,再查 MySQL 是否真变了,再查 Redis 是否旧值,再找测试复现",说明你已经从"会调用接口的前端"开始转向"能定位和修复后端链路的全栈工程师"。

附录 A:把错误分成 5 个层次

后端排查最怕一句"接口不行"。你要把错误分成层次。第一层是网络层:端口是否启动,URL 是否访问到后端,Docker 依赖是否健康。如果 curl 连不上 localhost:8080,你不应该去看业务代码,而应该看服务是否启动、端口是否被占用、profile 是否正确。

第二层是协议层:HTTP 方法、路径、请求头、Content-Type、JSON 格式是否正确。比如用 GET 调 PATCH 接口,或者忘记 Content-Type: application/json,问题发生在业务之前。

第三层是安全层:JWT 是否有效,角色是否满足,回调密钥是否正确,幂等 key 是否符合格式。401、403、PAYMENT_CALLBACK_SECRET_INVALID 都属于这一层。

第四层是业务层:商品是否上架,库存是否足够,订单状态是否允许取消,支付是否仍然 PENDING,分类是否启用。这一层通常返回 404、409 或特定业务码。

第五层是基础设施层:MySQL 连接、Redis 连接、事务回滚、SQL 约束、缓存旧值、定时任务、外部回调并发。500、DEGRADED 日志、Testcontainers 失败通常在这一层。

flowchart TD A[接口不符合预期] --> B[网络层] B --> C[协议层] C --> D[安全层] D --> E[业务层] E --> F[基础设施层] B --> B1[服务端口 Docker 健康] C --> C1[方法 路径 Header JSON] D --> D1[JWT 角色 密钥 幂等键] E --> E1[状态机 库存 权限归属] F --> F1[MySQL Redis 事务 定时任务]

每次排查都从上往下,不要跳。很多 401 问题看 2 小时业务代码也没用,因为请求根本没进 Controller;很多旧值问题改 5 次前端状态也没用,因为 Redis 里就是旧 JSON。

附录 B:日志应该怎么看,不应该怎么看

看日志不是在海量文本里碰运气,而是带着 traceId 和关键词搜索。当前项目商品缓存日志有固定模式:product_detail_cache event=HITMISSPUTPUT_NULLEVICTDEGRADEDCORRUPTED。支付和订单虽然日志不如缓存集中,但你仍可以通过业务码、traceId、SQL 结果和测试复现定位。

不要只看最后一行异常。最后一行通常只是异常类型,真正原因可能在上方第一段 Caused by。也不要把所有 warn 都当成严重错误。比如 Redis DEGRADED operation=PUT 表示缓存写失败,但当前商品详情请求仍可能成功;这需要关注 Redis 健康,但不等于业务数据错了。

前端同学常习惯用 console.log 看变量。后端也可以打断点,但日志更适合线上和异步场景。好的日志应该包含事件名、关键业务 id、状态、traceId 和异常。坏日志只有"进入方法了""出错了",没有上下文,排查价值很低。

如果你未来给订单或支付增加日志,建议写成结构化风格:

text 复制代码
order_payment event=CALLBACK_SUCCESS orderId=10 paymentNo=PAY-xxx statusBefore=PENDING statusAfter=SUCCESS traceId=abc123

这样前端提供 traceId 或订单号时,后端能很快搜索。

附录 C:如何把一个线上问题变成测试

假设你遇到"重复点击创建订单导致库存锁了两次"。不要只手动修一下。你应该把它变成测试:准备购物车,使用同一个 Idempotency-Key 连续请求两次创建订单,断言返回同一个订单,断言 SKU 的 available_stock 只减少一次,locked_stock 只增加一次。当前项目已经有类似测试 sameIdempotencyKeyReturnsOriginalOrderWithoutLockingAgain

假设你遇到"Redis 里坏 JSON 导致商品详情一直 500"。你应该写测试:手动让 Redis mock 返回非法 JSON,调用 lookup,断言状态为 MISS,断言调用了 delete,断言没有向上抛异常。当前项目 ProductDetailCacheServiceTest.shouldDeleteBrokenJsonAndTreatItAsMiss 就是这种思路。

假设你遇到"支付回调和超时关单同时发生,订单状态混乱"。你应该用并发测试或 MySQL 容器测试复现竞争,让两个线程同时执行回调和关单,最后断言订单、支付、库存三者一致。当前项目 PaymentMySqlContainerTest.callbackAndTimeoutCloseHaveOneConsistentWinner 已经覆盖这个边界。

把 bug 变成测试有 3 个好处:第一,证明你真的理解了问题;第二,防止以后重构再次破坏;第三,让后来学习这个项目的人直接通过测试名理解业务规则。

附录 D:后端 Debug 断点怎么打

如果你在 IDEA 里调试,不建议一开始到处打断点。按链路打关键断点。Controller 断点用于确认请求是否进入和参数是否绑定成功。Facade 断点用于看当前用户、业务分支、状态机判断和缓存状态。DbService 断点用于看查询条件、UPDATE 影响行数和异常转换。Mapper 或 SQL 日志用于看最终数据库操作。ExceptionHandler 断点用于看异常如何转换成响应。

以副标题接口为例,断点顺序是:ProductAdminController.updateSubtitleidrequest.subtitleProductFacade.updateSubtitle 看 trim 后的值;ProductDbService.updateSubtitle 看 wrapper 条件和 affectedRowsProductDetailCacheService.evict 看是否删除缓存;ProductFacade.toDetailResponse 看返回字段;最后 curl 响应看 data.subtitle

以商品详情缓存为例,断点顺序是:ProductController.detailProductFacade.getPublishedProductProductDetailCacheService.lookupRedisOperatorClient.getProductDbService.requirePublishedByIdProductDetailCacheService.put。如果缓存命中,DbService 断点不应该进入;如果 Redis DEGRADED,DbService 应该进入。

以支付回调为例,断点顺序是:MockPaymentCallbackController.callbackPaymentFacade.requireValidCallbackSecretPaymentCallbackTransactionService.handleSuccessPaymentDbService.selectByPaymentNoForUpdateOrderDbService.markPaidPaymentDbService.markSuccessSkuDbService.confirmLockedStock。如果重复回调,应该在已 SUCCESS 分支直接返回,不再确认库存。

附录 E:给前端同学的后端排查口头模板

当你向别人描述一个后端问题时,可以用这个模板:

"我用 curl 复现了问题。请求是 方法 + URL,请求头包含哪些关键 header,请求体是什么。HTTP 状态是几,响应 code 是什么,traceId 是什么。Controller 是否进入。进入后 Facade 走到了哪个分支。MySQL 关键字段当前是什么。Redis key 当前是什么。已有测试是否覆盖这个场景;如果没有,我准备补一个什么测试。"

这个模板能让沟通从"好像不行"变成"证据驱动"。例如:

"我用 curl 调 PATCH /api/admin/products/1/subtitle,管理员 token 正确,HTTP 200,code SUCCESS,traceId 是 debugSubtitle001。MySQL 里 subtitle 已经是新值,但 GET /api/products/1 仍返回旧值。Redis key mall:product:detail:v1:1 里还是旧 JSON,日志没有看到 EVICT,所以问题在写后缓存失效链路。"

这样的描述,后端一听就知道该看 ProductFacade.updateSubtitleProductDetailCacheService.evict,而不是去怀疑前端页面。

附录 F:本系列之后如何继续练后端

如果你已经完成 16 篇阅读,不要停在"看懂"。后端能力要靠动手巩固。建议做 3 个渐进练习。

练习一:给副标题接口补测试。你会熟悉 MockMvc、登录 token、JSON 断言、数据库断言和缓存 mock。这个练习小而完整,非常适合入门。

练习二:新增商品主图字段 coverImageUrl。你要改 SQL、Entity、Request、Response、Facade、Controller、缓存失效和文档。它和副标题类似,但可以加入 URL 格式校验,训练你抽象相同开发流程。

练习三:新增收藏商品模块。它会用到用户私有资源、唯一索引、分页查询、取消收藏、权限隔离和测试。你会把购物车、订单里的"当前用户 + 数据归属"思想再练一遍。

每个练习都按这套流程:先写需求和接口契约,再改数据库,再写最小实现,再写测试,再 curl 验证,再补文档。不要一上来就写 Controller。后端工程最重要的是顺序感和边界感。

附录 G:把一次排查写成团队可复用记录

会 Debug 只是第一步,更高阶的能力是把排查过程写成别人下次能复用的记录。建议每次遇到典型问题时,都按固定模板沉淀:问题现象、影响范围、复现步骤、关键请求、HTTP 状态、业务 code、traceId、相关日志、数据库证据、Redis 证据、根因、修复方案、补充测试、预防措施。这个模板看起来长,但真正写熟后非常快,因为它符合后端问题的自然链路。

比如"管理员修改副标题后公开详情不更新",现象是公开详情仍返回旧副标题;复现步骤是先请求详情制造缓存,再调用管理端修改,再请求详情;关键请求是 PATCH /api/admin/products/{id}/subtitleGET /api/products/{id};数据库证据是 mall_product.subtitle 已更新;Redis 证据是 mall:product:detail:v1:{id} 仍保存旧 JSON;根因可能是写接口漏掉 evict;修复方案是在 ProductFacade.updateSubtitle 数据库更新成功后删除商品详情缓存;补充测试是缓存失效测试。这样一写,团队成员不用重新猜测。

前端同学也可以把它类比为线上 bug 复盘。区别是前端复盘常围绕浏览器、组件状态和用户操作,后端复盘要更多关注请求链路、数据状态和并发时序。不要只在聊天工具里发一句"已修复"。没有证据链的修复,很容易下次换一个接口又犯同样错误。

附录 H:为什么后端排查要优先隔离变量

很多刚转后端的前端同学喜欢一边改页面、一边改接口、一边重启服务、一边清 Redis,最后问题好了却不知道哪个动作起作用。后端排查最重要的方法是隔离变量:一次只改变一个条件,观察结果是否变化。先用 curl 绕过前端页面,确认接口本身;再固定请求体,确认参数;再固定数据库数据,确认业务规则;再临时关闭缓存,确认 Redis 是否相关;最后才回到页面联调。

举一个订单问题:页面提示库存不足。不要立刻改按钮逻辑,也不要立刻怀疑事务。先用 curl 带同一个 JWT 和同一个 Idempotency-Key 复现;再查购物车勾选项是否存在;再查 SKU 的 available_stock;再看 lockStock 影响行数;再确认是否因为重复幂等键返回旧订单。每一步只验证一个假设。如果你同时换用户、换商品、清购物车、改库存,就会把问题变成随机现象。

隔离变量也能帮助你和后端同事沟通。你可以说:"我已经绕过页面,用 curl 复现;同一个商品 id,在 Redis 有旧详情时返回旧副标题,删除 key 后返回新副标题。"这句话比"页面好像有缓存问题"有价值十倍,因为它直接把范围缩小到缓存失效。

附录 I:从前端 DevTools 迁移到后端工具箱

前端排查常用 DevTools 的 Network、Console、Application、Performance。后端也有对应工具。Network 对应 curl、Postman、接口测试和网关日志;Console 对应应用日志、异常栈和 traceId;Application 里的 localStorage / cookie 对应 JWT、Redis、数据库状态;Performance 对应接口耗时、慢 SQL、缓存命中率和线程池情况。你不需要一次掌握所有后端工具,但要知道每个前端工具在后端世界里的"同类物"。

本项目最小工具箱建议是:curl 看 HTTP 契约,jq 格式化 JSON,mysql 或数据库客户端看事实数据,redis-cli 看缓存,IDEA Debug 看调用栈,mvn test 看自动化验证,日志看 traceId。刚开始不要沉迷复杂 APM 平台,因为本地学习项目最重要的是理解链路。等你能不用页面、只靠 curl 和数据库把问题复现出来,你的后端调试能力就已经明显超过"只会看页面报错"的阶段。

使用工具时还要注意环境。你在本地 Redis 查到的 key,不代表测试环境也有;你在 dev profile 关闭了缓存,不代表生产也关闭;你本地数据库 seed 数据和别人机器可能不同。后端问题经常不是代码本身,而是配置、数据、环境组合导致的。因此每次反馈问题都要带上环境:本地 / 测试 / 线上,profile,数据库版本,是否启动 Redis,当前登录用户角色,请求头和请求体。

附录 J:给本系列读者的一套毕业练习

如果你想检验自己是否真正完成"前端转后端"的第一阶段,可以在本项目上做一套毕业练习。第一题,新增"收藏商品"接口:用户可以收藏、取消收藏、查看自己的收藏列表。你需要设计表、Controller、Facade、DbService、权限、唯一约束和测试。重点是 userId 必须来自 JWT,不能信任前端传参。第二题,新增"商品评价"接口:已支付订单才能评价,重复评价要拒绝,公开详情可以展示评价摘要。重点是跨表校验和状态约束。第三题,给商品详情增加图片列表:需要考虑 Response 结构变化、缓存 key 版本、后台上传和公开读取。

做这些练习时,不要急着写代码。先按本系列的方法画图:请求链路图、表关系图、状态机图、异常返回图、缓存失效图。再列文件清单:SQL、Entity、Mapper、DbService、Facade、Controller、Request、Response、Security、测试。最后用 curl 写验收脚本。你会发现,后端开发不是神秘魔法,而是一套可重复的工程流程。

真正的全栈能力不是"前端也能写一点 Java",而是你能理解一次用户操作背后,浏览器、HTTP、鉴权、业务规则、事务、数据库、缓存、日志和测试如何共同保证系统正确。这个系列已经带你走完商城核心闭环:商品、SKU、购物车、订单、支付、缓存、后台修改和调试。接下来每做一个新模块,都可以复用同一套思路。

附录 K:后端排查时如何避免"猜测驱动"

猜测不是坏事,但猜测必须被证据验证。很多问题一开始都会有多个可能原因:接口路径错、权限不对、参数校验失败、业务状态不允许、数据库数据不符合、Redis 旧值、事务回滚、并发冲突、测试环境配置不同。好的排查不是选择一个最像的原因直接修改,而是把可能原因排成列表,用最便宜的证据一个个排除。

例如看到 403,不要马上说"Token 过期"。先看 HTTP 状态和业务 code;如果是 401 才更像未认证,如果是 403 则说明大概率已认证但权限不足。再看请求路径是否命中 /api/admin/**,再看当前用户角色是否是 ADMIN,再看 SecurityConfig 的规则。这个过程没有一步依赖猜测,都是证据。

看到 404 也一样。先区分是 Spring 路由 404,还是业务对象 404。如果响应体仍然是统一 ApiResponse,并且 code 是 PRODUCT_NOT_FOUND,那 Controller 其实已经进来了;如果根本没有统一响应结构,才更可能是路径不存在或请求方法不匹配。很多前端同学把所有 404 都当成 URL 写错,这是因为在前端路由里 404 往往代表页面不存在,但后端 404 既可能是路由不存在,也可能是资源不存在。

附录 L:把本系列文章当成一张后端学习地图

读完 16 篇后,你可以用一张地图复盘自己的能力。第 1 到第 3 篇让你知道项目结构、启动流程、Controller 契约;第 4 到第 5 篇让你理解 MySQL 和 MyBatis-Plus;第 6 到第 7 篇补上 JWT、Spring Security、Redis;第 8 到第 13 篇进入商城核心业务,从商品详情、状态机、SKU、购物车、订单到支付;第 14 到第 15 篇把缓存和真实需求开发串起来;第 16 篇负责把调试方法收束成可复用流程。

后续你学习任何后端框架,都可以按这张地图迁移。换成 Node.js、Go、Python 或其他 Java 框架,HTTP、鉴权、数据库、缓存、事务、幂等、日志、测试这些主题仍然存在。框架 API 会变,工程思维不会变。这也是前端转全栈最重要的信心来源:你不是从零开始,而是在已有的组件化、状态管理、接口联调和工程化能力上,补齐服务端的数据一致性与系统边界能力。

附录 M:给未来自己的排查习惯建议

第一,任何接口问题先保存原始请求和原始响应,不要只截图页面。请求方法、URL、请求头、请求体、状态码、响应 JSON 都是证据。第二,每次看到统一响应体,都先读 codetraceId,不要只读 message。第三,遇到数据不一致,先确定事实来源:订单看订单表,库存看 SKU 表,商品详情看商品表,缓存只能作为加速层参考。第四,遇到权限问题,先区分 401 和 403。第五,遇到重复提交,优先检查幂等键、唯一索引和事务边界。

第六,不要害怕读测试。测试往往比实现更接近业务规则,因为它用输入和输出描述了系统承诺。第七,不要把所有问题都交给前端复现。只要能用 curl 复现,问题就脱离了页面复杂度。第八,修复后一定补一个能失败再变绿的测试,否则你只是临时修好一次。第九,写日志时要服务排查,不要只打印"进入方法"。第十,保持谦虚:后端问题常常来自多个因素叠加,证据链比直觉更可靠。

最后提醒一句:排查能力不是背命令,而是建立顺序。先确认入口,再确认身份,再确认参数,再确认业务规则,再确认数据,再确认缓存和并发。顺序稳定,问题就会变小。

相关推荐
anyup1 小时前
uni-app 没有根组件?仅需几行代码实现全局 Toast 和 Modal
前端·架构·uni-app
亦暖筑序1 小时前
重新认识 AgentScope-Java 2.0:ReActAgent 负责推理,HarnessAgent 负责运行
java·后端·agent
小林ixn1 小时前
Redis 实战避坑指南:从缓存击穿到高可用,一文全搞定
redis·后端
用户0510122572961 小时前
DAY 5-智能指针与 C++ 内存管理
c++·面试
苍何1 小时前
用 AI 做了套品牌周边,感觉能挂Shopify开卖了
后端
并不喜欢吃鱼1 小时前
一.前端web开发:零基础吃透 HTML5 核心知识
前端·html·html5
用户298698530141 小时前
免费在线将 PDF 转为 Word:5 款好用的转换工具
人工智能·后端
newerp1 小时前
unsafe 包与底层编程
后端