考场上会翻的那几页The pages you actually flip to
只收两个真实项目里实际用到的东西。命令都是这两个项目能跑的,报错都是审计时真实出现过的。Only what the two real projects actually use. Every command runs in one of them; every error message really came up during the audit.
命令Commands
注意两个项目的差别:react-notes-app 没有 test script, node-subgraph 有。Note the difference between the two projects: react-notes-app has no test script; node-subgraph has one.
test、start、stop、restart。其余都必须写 npm run xxx ——npm build 不会跑你的 build script。test, start, stop, restart. Everything else needs npm run xxx. npm build does not run your build script.
package.json 字段package.json fields
| 字段Field | 管什么What it controls | 两个项目里的值Values in the two projects |
|---|---|---|
name | 包名Package name | react-notes-app / order-subgraph |
private | 禁止发布到 npmBlocks publishing to npm | react-notes-app 有;subgraph 没有react-notes-app has it; subgraph does not |
version | 自身版本号This package's own version number | 都是 1.0.01.0.0 in both |
type | module = 用 ESM(import/export); 不写 = CommonJS(require)module = ESM (import/export). Leave it out = CommonJS (require) | 两个都是 modulemodule in both |
main | 包的入口文件The package's entry file | subgraph 是 src/index.jssrc/index.js in the subgraph |
scripts | npm run <名字> 能跑的命令Commands that npm run <name> can run | react: dev/build/q2;subgraph: start/test/test:watchreact: dev/build/q2; subgraph: start/test/test:watch |
dependencies | 产品运行时需要Needed while the product runs | react+react-dom / apollo+graphql+dataloader |
devDependencies | 只在开发/构建/测试时需要Needed only for development, build and tests | vite/vitest/typescript / jest |
jest 等工具名jest and other tool names | 内嵌配置 —— 找不到 jest.config.js 时看这里Inline config — look here when there is no jest.config.js | subgraph 的 jest 配置就在这里The subgraph keeps its jest config here |
subgraph 的 dependencies 里有 dataloader, 而 TODO 里正好要求「用 DataLoader 防 N+1」。拿到新项目先读 dependencies,特殊的包就是考点。The subgraph lists dataloader in dependencies, and one TODO asks you to stop N+1 queries with DataLoader. On a new project, read dependencies first. An unusual package is the thing being tested.
React Hooks(这道题用到的)React Hooks (the ones this exam uses)
react-notes-app/src/① 改 state 只能通过 setter,且必须造新对象(push / splice / arr[i]= 都会让界面不更新且不报错)。
② effect 里修改的 state 不能出现在它自己的依赖数组里。
③ 能从现有 state 算出来的值不要做成 state。① Change state only through its setter, and always build a new object (push / splice / arr[i]= leave the interface stale, and raise no error).
② A state value that an effect writes must not appear in that effect's own dependency array.
③ If a value can be computed from state you already have, do not make it state.
GraphQL SDL
graphql-federation-practice/node-subgraph/src/schema.graphqlResolverResolvers
Federation directive 与 entityFederation directives and entities
| 东西Thing | 含义Meaning |
|---|---|
@key(fields: "id") | 「别的 subgraph 给出 id 就能定位同一个我」。 可以复合("isbn edition"),可以有多个Given an id from another subgraph, this one can find the same object. A key can be composite ("isbn edition"), and a type can have several |
@external | 这个字段由别的 subgraph 定义,我只借来做身份识别Another subgraph defines this field; this one only borrows it to identify the object |
@shareable | 允许多个 subgraph 定义同一个字段。 本项目 import 了但没用到Lets several subgraphs define the same field. This project imports it but never uses it |
__resolveReference | 把 representation 变成本地对象。它的返回值就是下游字段 resolver 的 parentTurns a representation into a local object. What it returns becomes the parent for the field resolvers under it |
_service | 自动生成的字段,返回 federation SDL。 Router 启动时查它A generated field that returns the federation SDL. The Router queries it at startup |
_entities | 自动生成的字段,Router 运行时靠它做实体解析A generated field. At runtime the Router uses it to resolve entities |
buildSubgraphSchema | 来自 @apollo/subgraph。 认识 federation directive,并自动加上面两个字段From @apollo/subgraph. It understands the federation directives and adds the two fields above |
HTTP 状态码与 Spring 写法HTTP status codes and how Spring writes them
| 码Code | 什么时候When | Spring 写法How Spring writes it |
|---|---|---|
| 200 | 成功,有内容返回Success, with a body | ResponseEntity.ok(body) |
| 201 | 创建成功(POST)Created (POST) | ResponseEntity.status(HttpStatus.CREATED).body(x) |
| 204 | 成功但没有内容(DELETE)Success with no body (DELETE) | ResponseEntity.noContent().build() |
| 400 | 请求本身不合法The request itself is not valid | @Valid 自动,或 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg)@Valid does it, or throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg) |
| 404 | 目标不存在The target does not exist | 不写 —— 让 service 的 EntityNotFoundException 冒到 @RestControllerAdviceDo not write it — let the service's EntityNotFoundException travel up to @RestControllerAdvice |
报错对照表Error table
都是审计和课程里真实出现过的。先看这张表再改代码。Every one of these really came up during the audit or in a lesson. Read this table before you edit code.
| 报错 / 症状Error / symptom | 根因Root cause |
|---|---|
npm error Missing script: "test" | 项目没有 test script → 用 npx vitest runThis project has no test script. Use npx vitest run |
TS2582: Cannot find name 'test' | tsconfig 没配测试框架全局类型。react-notes-app 原生就有这个问题,不是你的错tsconfig does not declare the test framework globals. react-notes-app ships with this problem; it is not your fault |
ERR_MODULE_NOT_FOUND | 原生 ESM 里相对路径漏了 .js 扩展名A relative import in native ESM is missing the .js extension |
Cannot use import statement outside a module | 缺 "type": "module", 或 jest 缺 --experimental-vm-modules"type": "module" is missing, or jest is missing --experimental-vm-modules |
Maximum update depth exceeded | useEffect 依赖里含自己修改的 state; 或 onClick={fn()} 在渲染时就执行了A useEffect dependency holds state that the same effect writes; or onClick={fn()} already ran during render |
| 没报错,数据对但界面不动No error, the data is right but the interface does not change | 改了原对象(push / splice / arr[i]= / obj.x=)You changed the original object (push / splice / arr[i]= / obj.x=) |
| 没报错,组件完全不显示No error, the component does not show at all | 组件名小写开头,被当成 HTML 标签The component name starts with a lower-case letter, so it is read as an HTML tag |
| 没报错,列表空白但数据有值No error, the list is blank but the data is there | map 回调用了花括号却忘了 returnThe map callback uses braces but never returns |
Unable to find an element by: [data-testid=...] | testid 被改了,或那个元素被条件性移除了。报错里会打印整个 DOM,在里面搜相似 testidThe testid changed, or a condition removed that element. The error prints the whole DOM — search it for a similar testid |
| 测试说找不到文字,但代码看着没错The test cannot find the text, but the code looks right | userEvent 前面漏了 awaitA missing await before userEvent |
Cannot return null for non-nullable field | resolver 忘了 ?? []。会向上冒泡, 可能让整个 data 变 nullThe resolver forgot ?? []. It travels upward and can turn the whole data into null |
xxx is not a function | 调了对象上不存在的方法。去定义处核对方法名You called a method the object does not have. Check the name where the object is defined |
Cannot read properties of undefined (reading 'y') | 上一级路径写错了(如 dataSources.orderAPI 不存在)The path one level up is wrong (for example dataSources.orderAPI does not exist) |
| 没报错,某个 GraphQL 字段一直是 nullNo error, one GraphQL field is always null | resolver 键名和 schema 字段名不一致,或挂在了错误的类型下。在 resolver 第一行 log 确认它有没有被调用The resolver key does not match the schema field name, or it sits under the wrong type. Log on the resolver's first line to see whether it runs at all |
| 没报错,DataLoader 的数据串了No error, DataLoader hands back the wrong row for a key | batch 函数里用了 filter,破坏了长度/顺序契约The batch function used filter, which breaks the length and order contract |
| 错误码不对(收到 SERVICE_ERROR 而不是 INVALID_INPUT)Wrong error code (SERVICE_ERROR arrives instead of INVALID_INPUT) | catch 把自己抛的结构化错误重新包装了。 补 if (error instanceof GraphQLError) throw errorThe catch re-wrapped a structured error you threw yourself. Add if (error instanceof GraphQLError) throw error |
Spring:Status expected:<201> but was:<200>Spring: Status expected:<201> but was:<200> | POST 用了 ok();或者端点还是 return nullThe POST used ok(); or the endpoint still says return null |
| Spring:客户端输入错误返回了 500Spring: a bad client input comes back as 500 | Enum.valueOf 抛的 IllegalArgumentException 没被转成 400The IllegalArgumentException thrown by Enum.valueOf was never turned into a 400 |
| Spring:查不存在的 id 返回 200 空 bodySpring: asking for an id that does not exist returns 200 with an empty body | 自己 catch 了 EntityNotFoundException, 全局处理器收不到The code caught EntityNotFoundException itself, so the global handler never sees it |
Unknown directive "@xxx" | @link 的 import 列表里漏了它, 或没用 buildSubgraphSchemaIt is missing from the @link import list, or you did not use buildSubgraphSchema |
Debug 清单Debug checklist
卡住的时候按顺序走一遍。大部分问题在第 3 步之前就解决了。When you are stuck, walk through this in order. Most problems are solved before step 3.
- 分层。这个报错来自 npm(命令/目录不对)、 工具(依赖没装)、还是我的代码?别一看红字就改业务代码。Find the layer. Did this error come from npm (wrong command or wrong directory), from a tool (dependencies not installed), or from my own code? Red text is not a reason to start editing business code.
- 只看第一条报错。类型错误和 GraphQL 错误都会连锁,修掉第一条后面可能自己消失。Read only the first error. Type errors and GraphQL errors come in chains. Fix the first one and the rest may go away by themselves.
- 如果没有报错,按症状查表(见上一节): 数据对但界面不动 / 组件不显示 / 列表空白 / 字段一直 null / 数据串了 —— 这五种各有固定病因。If there is no error, look the symptom up in the table (previous section): data right but interface still / component not showing / blank list / field always null / rows mixed up. Each of these five has one fixed cause.
- 确认代码有没有被执行。在最可疑的函数第一行放一个 log。「日志没打印」和「日志打印了但结果不对」 指向完全不同的方向。Check whether the code runs at all. Put a log on the first line of the most suspicious function. "No log printed" and "log printed but the result is wrong" point in completely different directions.
- 核对跨模块的名字和签名。方法名、context 键名、props 名、参数个数、位置参数 vs 对象参数。 这一步能抓住绝大多数「集成问题」。Check names and signatures across modules. Method names, context keys, prop names, how many arguments, positional arguments versus one object argument. This step catches most integration problems.
- 回去读契约。schema 的可空性、类型定义、README 里的约束、 测试断言的确切字符串。Go back and read the contract. Schema nullability, type definitions, the constraints in the README, the exact strings the test assertions use.
- 改完必须验证,而且要验到题面那一层。测试过 ≠ 做对了 —— 手动造一个测试覆盖不到的场景 (同名数据、多条数据、非法输入、不存在的 id)。After a fix, verify — and verify at the level the task asks about. Passing tests is not the same as being correct. Build a case by hand that the tests do not cover: records with the same name, several records, invalid input, an id that does not exist.
① Java 六个端点全 return null,5 个测试过 3 个。
② subgraph 四个 TODO 全空,10 个测试过 4 个 (其中 3 个是「返回空」的假通过)。
③ React 的删除/编辑测试只有一条数据, 按 title 删、先删再加都能过。① All six Java endpoints return null, and 3 of 5 tests still pass.
② All four subgraph TODOs are empty, and 4 of 10 tests pass (3 of those pass only because returning nothing counts as a pass).
③ The React delete and edit tests use a single record, so deleting by title, or deleting then adding, both pass.
所以:绿色是及格线,不是正确性证明。So: green is the pass mark, not proof of correctness.