DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 148 个练习 · 第 10 / 13 页。Showing 148 · page 10 / 13.
来自From 两道书面题:延迟传播与生产配置The two written questions: how delay spreads, and production configuration · Federation 考试Federation exam
L1认出来Spot it哪一行是最严重的安全问题Which line is the most serious security problem

题面给的六行配置里,哪一行能直接导致数据库口令泄漏?

Of the six configuration lines in the question, which one can directly leak the database password?

Properties源项目From source
1server.port=8080
2server.address=0.0.0.0
3spring.datasource.url=jdbc:postgresql://${DB_HOST}:5432/orders
4spring.datasource.username=${DB_USER}
5spring.datasource.password=${DB_PASSWORD}
6management.endpoints.web.exposure.include=*
Source: graphql-federation-practice/QUESTIONS.md
先选一个选项Pick an option first
来自From 两道书面题:延迟传播与生产配置The two written questions: how delay spreads, and production configuration · Federation 考试Federation exam
L1认出来Spot it为什么 User subgraph 慢会拖慢 Orders subgraphWhy a slow User subgraph slows the Orders subgraph down

客户端查 { user(id:"1") { name orders { id } } }。 Accounts subgraph 要 500ms,Orders subgraph 只要 10ms。 总延迟大约是多少,为什么?

A client asks for { user(id:"1") { name orders { id } } }. The Accounts subgraph takes 500ms, the Orders subgraph only 10ms. Roughly what is the total latency, and why?

先选一个选项Pick an option first
来自From 两道书面题:延迟传播与生产配置The two written questions: how delay spreads, and production configuration · Federation 考试Federation exam
L3写整块Write a block写出 actuator 那一条的修正配置Write the corrected configuration for the actuator lineDrillLab 自出Written by DrillLab

针对 management.endpoints.web.exposure.include=*, 写出修正后的配置。至少要做到:白名单、管理端口分离、 health 不泄漏细节、支持 k8s 探针。

Write the corrected configuration for management.endpoints.web.exposure.include=*. At a minimum: an allow list, management on its own port, a health endpoint that leaks no detail, and support for Kubernetes probes.

要求Requirements
  • 用白名单列出需要的端点,不用 *List the endpoints you need in an allow list; do not use *
  • management.server.port 设成与业务端口不同的值Set management.server.port to something other than the business port
  • health 端点不显示详情The health endpoint shows no details
  • 开启 health probes(liveness / readiness)Turn on the health probes (liveness and readiness)
Propertiesapplication-prod.properties
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L3Debug LabDebug Lab故障 1 · resolver 写了,字段还是 nullFault 1 · the resolver is written, the field is still null

你确信写了 shippingInfo 的实现, 测试也不报错,但查询返回的 shippingInfonull。控制台里连你加的 log 都没打印。

You are sure you wrote an implementation for shippingInfo, and no test reports anything, but the query returns shippingInfo as null. Not even the log line you added prints on the console.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ node verify-schema.mjs Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","shippingInfo":null}, {"id":"order-457","status":"DELIVERED","shippingInfo":null}]} errors: [] # 你在 resolver 第一行加的 console.log('>>> shippingInfo called') # 一次都没打印。# No error at all. $ node verify-schema.mjs Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","shippingInfo":null}, {"id":"order-457","status":"DELIVERED","shippingInfo":null}]} errors: [] # The console.log('>>> shippingInfo called') you added as the resolver's first line # never printed, not once.
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← 名字
4 console.log('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// 参考 schema.graphql:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← the name
4 console.log('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// For reference, schema.graphql says:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L2Debug LabDebug Lab故障 2 · Cannot return null for non-nullable fieldFault 2 · Cannot return null for non-nullable field

查一个没有订单的用户,整个 data 变成了null,而且 errors 里有一条很长的消息。

You query a user who has no orders, the whole data turns into null, and errors carries one very long message.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ node verify-schema.mjs Query.orders: {"orders":null} errors: [{ "message": "Cannot return null for non-nullable field Query.orders.", "path": ["orders"], "extensions": { "code": "INTERNAL_SERVER_ERROR" } }] # 更严重的情况:如果查询是嵌套的,整个 data 会变成 null$ node verify-schema.mjs Query.orders: {"orders":null} errors: [{ "message": "Cannot return null for non-nullable field Query.orders.", "path": ["orders"], "extensions": { "code": "INTERNAL_SERVER_ERROR" } }] # Worse case: if the query is nested, the whole data object turns into null
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1async orders(_, { userId }, { dataSources, correlationId }) {
2 const orders = await dataSources.orderDataSource.getOrdersByUserId(userId);
3 return orders; // 数据源可能返回 undefined
4}
5
6// 参考 schema.graphql:
7// type Query {
8// orders(userId: ID!): [Order!]! ← 双重非空
9// }
1async orders(_, { userId }, { dataSources, correlationId }) {
2 const orders = await dataSources.orderDataSource.getOrdersByUserId(userId);
3 return orders; // the data source may return undefined
4}
5
6// For reference, schema.graphql says:
7// type Query {
8// orders(userId: ID!): [Order!]! ← non-null twice
9// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L3Debug LabDebug Lab故障 3 · A 拿到了 B 的数据Fault 3 · A receives B's data

查两个订单的物流,返回的数据对上了错的订单。 没有任何报错。这是 DataLoader 最阴险的一类误用。

You query the shipping info for two orders and the data comes back attached to the wrong order. Nothing reports an error. This is the hardest kind of DataLoader misuse to notice.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 查询:{ orders(userId:"123") { id shippingInfo { trackingNumber } } } # # 期望: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # 实际: # order-456 -> TRACK123457 ← 串了! # order-457 -> null # 日志:[DataLoader] Batching 2 shipping info requests ← 合并是生效的# No error at all. # Query: { orders(userId:"123") { id shippingInfo { trackingNumber } } } # # Expected: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # Actual: # order-456 -> TRACK123457 ← got the other one's number! # order-457 -> null # Log: [DataLoader] Batching 2 shipping info requests ← the batching does work
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // 「过滤掉没有物流信息的」—— 看起来很合理
10 return all.filter(info => info !== null);
11 });
12}
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // "drop the ones with no shipping info" — this looks reasonable
10 return all.filter(info => info !== null);
11 });
12}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L2Debug LabDebug Lab故障 4 · PATCH 传了小写状态,返回 500Fault 4 · PATCH sends a lowercase status and gets a 500

Java 那边。mvn test 全过, 但客户端传小写的 shipped 时服务返回 500。

This one is on the Java side. mvn test passes everything, but the service returns 500 when the client sends the lowercase shipped.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ curl -i -X PATCH localhost:8080/api/orders/1/status \ -H 'Content-Type: application/json' -d '{"status":"shipped"}' HTTP/1.1 500 {"timestamp":"...","status":500,"error":"Internal Server Error"} # 服务端日志: java.lang.IllegalArgumentException: No enum constant com.techflow.orders.model.OrderStatus.shipped at java.base/java.lang.Enum.valueOf(Enum.java:293) at com.techflow.orders.model.OrderStatus.valueOf(OrderStatus.java:3) at c.t.orders.controller.OrderController.updateOrderStatus(OrderController.java:71) # mvn test:Tests run: 5, Failures: 0 ← 测试全过$ curl -i -X PATCH localhost:8080/api/orders/1/status \ -H 'Content-Type: application/json' -d '{"status":"shipped"}' HTTP/1.1 500 {"timestamp":"...","status":500,"error":"Internal Server Error"} # Server log: java.lang.IllegalArgumentException: No enum constant com.techflow.orders.model.OrderStatus.shipped at java.base/java.lang.Enum.valueOf(Enum.java:293) at com.techflow.orders.model.OrderStatus.valueOf(OrderStatus.java:3) at c.t.orders.controller.OrderController.updateOrderStatus(OrderController.java:71) # mvn test: Tests run: 5, Failures: 0 ← every test passes
JavaOrderController.java示意Illustrative
1@PatchMapping("/api/orders/{id}/status")
2public ResponseEntity<Order> updateOrderStatus(
3 @PathVariable Long id,
4 @RequestBody Map<String, String> statusUpdate) {
5 OrderStatus status = OrderStatus.valueOf(statusUpdate.get("status"));
6 return ResponseEntity.ok(orderService.updateOrderStatus(id, status));
7}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing · Federation 考试Federation exam
L4从零重写Rebuild from scratch从零重建 Task 1 · Orders subgraphRebuild Task 1 · the Orders subgraph

空目录开始,搭出一个 Apollo Federation subgraph, 实现四个 resolver 加一个 mutation,让 10 个测试全过, 并且 _service_entities 都能正常工作。不要打开源项目的 orderResolvers.js。

Starting from an empty directory, build an Apollo Federation subgraph. Write four resolvers plus one mutation, get all 10 tests passing, and make both _service and _entities work. Do not open orderResolvers.js from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 用 @apollo/server + @apollo/subgraph 起一个 subgraph,监听 4000Start a subgraph with @apollo/server + @apollo/subgraph, listening on 4000
  • schema 从 .graphql 文件读入,用 buildSubgraphSchema 组装Read the schema from a .graphql file and assemble it with buildSubgraphSchema
  • 每个请求构造 context:三个数据源、两个 DataLoader、一个 correlationIdBuild the context per request: three data sources, two DataLoaders, one correlationId
  • correlationId 优先取请求头 x-correlation-id,没有就生成Take correlationId from the x-correlation-id request header, and generate one when it is absent
  • 实现 User.__resolveReference:把 representation 变成本地对象Write User.__resolveReference: turn the representation into a local object
  • 实现 User.orders:按 user.id 取订单,[Order!]! 所以绝不返回 nullWrite User.orders: read orders by user.id; the type is [Order!]!, so never return null
  • 实现 Order.shippingInfo:必须走 DataLoader 防 N+1;可空,找不到返回 nullWrite Order.shippingInfo: it must go through the DataLoader to prevent N+1; it is nullable, so return null when nothing is found
  • 实现 Query.order:走 DataLoader;找不到抛带 ORDER_NOT_FOUND 的 GraphQLErrorWrite Query.order: go through the DataLoader; when nothing is found, throw a GraphQLError carrying ORDER_NOT_FOUND
  • 实现 Query.orders:校验 userId;[Order!]! 所以兜底 []Write Query.orders: validate userId; the type is [Order!]!, so fall back to []
  • 实现 Mutation.createOrder:先查商品价格补全 items,再创建;校验失败抛 INVALID_INPUTWrite Mutation.createOrder: look up product prices to complete items first, then create; throw INVALID_INPUT when validation fails
  • 两个 DataLoader 的 batch 函数:返回数组的长度与顺序必须和 keys 一一对应The batch function of both DataLoaders: the array it returns must match keys in both length and order
  • 所有 resolver 都用 try/catch,catch 第一行放行已有的 GraphQLErrorWrap every resolver in try/catch, and let an existing GraphQLError pass through on the first line of catch
  • 所有日志和错误 extensions 里带上 correlationIdCarry correlationId in every log line and in the extensions of every error
你需要自己建的文件Files you create yourself
文件清单File list
package.json自己写:type: module、start / test script(test 要带 NODE_OPTIONS=--experimental-vm-modules)、依赖 @apollo/server @apollo/subgraph graphql graphql-tag dataloader,devDep jest @jest/globals,以及内嵌 jest 配置You write it: type: module, the start / test scripts (test needs NODE_OPTIONS=--experimental-vm-modules), the dependencies @apollo/server @apollo/subgraph graphql graphql-tag dataloader, the devDependencies jest @jest/globals, and an inline jest config
src/schema.graphql★ 抄源项目的(这是题目):User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input★ Copy it from the source project (this is the question): the User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input
src/dataSources/orderDataSource.js★ 抄源项目的(这是题目):三个 mock 数据源类。注意 OrderDataSource 只有 getOrder / getOrdersByUserId / createOrder★ Copy it from the source project (this is the question): three mock data source classes. Note that OrderDataSource has only getOrder / getOrdersByUserId / createOrder
src/index.js★ 自己写:读 schema、buildSubgraphSchema、ApolloServer + formatError、startStandaloneServer、每请求造 context★ You write it: read the schema, buildSubgraphSchema, ApolloServer + formatError, startStandaloneServer, and build the context per request
src/resolvers/orderResolvers.js★★ 自己写:两个 loader 工厂 + resolvers(User / Order / Query / Mutation)+ ErrorCodes★★ You write it: two loader factories + the resolvers (User / Order / Query / Mutation) + ErrorCodes
__tests__/resolvers.test.js★ 抄源项目的(这是判卷器):10 个测试,beforeEach 里重建 dataSources 与 loaders★ Copy it from the source project (this is what grades you): 10 tests, with dataSources and loaders rebuilt in beforeEach
verify-schema.mjs★ 自己写:进程内查 _service、普通查询、_entities、mutation★ You write it: query _service in process, then a normal query, then _entities, then the mutation
写完后在本机这样验证Verify it locally like this
npm install
依赖装好,出现 node_modules 与 package-lock.jsonThe dependencies install, and node_modules and package-lock.json appear
npm start
打印 Subgraph ready at http://0.0.0.0:4000/It prints Subgraph ready at http://0.0.0.0:4000/
npm test
Tests: 10 passed, 10 total
node verify-schema.mjs
SDL 出得来且含 @key;orders + shippingInfo 有值;order-999 返回 ORDER_NOT_FOUND;_entities 能拿到 orders;createOrder 的 items[0].price 有值且 totalAmount > 0;空 items 返回 INVALID_INPUTThe SDL comes out and contains @key; orders + shippingInfo have values; order-999 returns ORDER_NOT_FOUND; _entities can read orders; items[0].price from createOrder has a value and totalAmount > 0; empty items returns INVALID_INPUT
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing · Federation 考试Federation exam
L4从零重写Rebuild from scratch从零重建 Task 2 · Spring Boot 控制器Rebuild Task 2 · the Spring Boot controller

给你 OrderService 的方法签名和五个测试。 自己搭一个 Spring Boot 项目,写出六个端点。不要打开源项目的 OrderController.java。

You are given the method signatures of OrderService and five tests. Set up a Spring Boot project yourself and write six endpoints. Do not open OrderController.java from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • Spring Boot 3.3 + Java 17,依赖 web / validation / actuator / testSpring Boot 3.3 + Java 17, with the web / validation / actuator / test dependencies
  • 一个 @RestController,构造器注入 OrderServiceOne @RestController, with OrderService injected through the constructor
  • GET /api/orders:?userId= 传了就按用户过滤,没传返回全部;200GET /api/orders: filter by user when ?userId= is given, return everything when it is not; 200
  • GET /api/orders/{id}:200;找不到时由全局异常处理器给出 404(控制器不要 catch)GET /api/orders/{id}: 200; when nothing is found, the global exception handler answers 404 (do not catch it in the controller)
  • GET /api/orders/user/{userId}:200GET /api/orders/user/{userId}: 200
  • POST /api/orders:@Valid 校验请求体;成功返回 201 CreatedPOST /api/orders: validate the request body with @Valid; on success return 201 Created
  • PATCH /api/orders/{id}/status:body 是 {"status":"..."};转成 OrderStatus;缺失或非法值返回 400;成功 200PATCH /api/orders/{id}/status: the body is {"status":"..."}; convert it to OrderStatus; a missing or invalid value returns 400; on success 200
  • DELETE /api/orders/{id}:204 No ContentDELETE /api/orders/{id}: 204 No Content
  • 六个端点都用 SLF4J 打日志,并带上 MDC 里的 correlationIdAll six endpoints log through SLF4J and carry the correlationId from MDC
  • 自己写一个 CorrelationIdFilter:读 X-Correlation-ID 头,没有就生成 UUID,放进 MDC,finally 里清理Write your own CorrelationIdFilter: read the X-Correlation-ID header, generate a UUID when it is absent, put it in MDC, and clear it in finally
  • 自己写 GlobalExceptionHandler:EntityNotFoundException → 404,MethodArgumentNotValidException → 400Write your own GlobalExceptionHandler: EntityNotFoundException → 404, MethodArgumentNotValidException → 400
你需要自己建的文件Files you create yourself
文件清单File list
pom.xmlparent 用 spring-boot-starter-parent 3.3.2,java.version 17,四个依赖 + spring-boot-maven-pluginThe parent is spring-boot-starter-parent 3.3.2, java.version is 17, four dependencies + spring-boot-maven-plugin
src/main/resources/application.propertiesserver.port=8080 就够(顺便按书面题的结论收紧 actuator)server.port=8080 is enough (and tighten actuator while you are here, following the written question)
src/main/java/.../OrderServiceApplication.java@SpringBootApplication + main
src/main/java/.../model/Order.java、OrderItem.java、OrderStatus.java★ 抄源项目的(这是题目)★ Copy it from the source project (this is the question)
src/main/java/.../dto/CreateOrderRequest.java、OrderItemRequest.java★ 抄源项目的:带 @NotBlank / @NotEmpty / @Min / @Valid★ Copy it from the source project: it carries @NotBlank / @NotEmpty / @Min / @Valid
src/main/java/.../repository/OrderRepository.java、InMemoryOrderRepository.java★ 抄源项目的:接口 + 内存实现(含一条种子数据)★ Copy it from the source project: the interface + an in-memory implementation (with one seed record)
src/main/java/.../service/OrderService.java★ 抄源项目的(这是题目):六个方法,三个会抛 EntityNotFoundException★ Copy it from the source project (this is the question): six methods, three of which throw EntityNotFoundException
src/main/java/.../exception/EntityNotFoundException.java、GlobalExceptionHandler.java★ 自己写:两个 @ExceptionHandler★ You write it: two @ExceptionHandler methods
src/main/java/.../config/CorrelationIdFilter.java★ 自己写:OncePerRequestFilter + MDC★ You write it: OncePerRequestFilter + MDC
src/main/java/.../controller/OrderController.java★★ 自己写:六个端点★★ You write it: six endpoints
src/test/java/.../OrderControllerTest.java★ 抄源项目的(这是判卷器):@WebMvcTest + @MockBean + 五个测试★ Copy it from the source project (this is what grades you): @WebMvcTest + @MockBean + five tests
写完后在本机这样验证Verify it locally like this
mvn test
Tests run: 5, Failures: 0, Errors: 0 — BUILD SUCCESS
mvn spring-boot:run
服务起在 8080,日志里能看到 Started OrderServiceApplicationThe service starts on 8080, and the log shows Started OrderServiceApplication
curl -i -s localhost:8080/api/orders/999
404 + {"timestamp":...,"status":404,"message":"Order not found with id: 999"}
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"123","items":[{"productId":"prod-789","quantity":2}]}'
201 Created + 订单 JSON(totalAmount 应为 299.98)201 Created + the order JSON (totalAmount should be 299.98)
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"","items":[]}'
400 Bad Request(Bean Validation 生效)400 Bad Request (Bean Validation is working)
curl -i -s -X PATCH localhost:8080/api/orders/1/status -H 'Content-Type: application/json' -d '{"status":"FLYING"}'
400 Bad Request(不是 500)400 Bad Request (not 500)
curl -i -s -X DELETE localhost:8080/api/orders/1
204 No Content,body 为空204 No Content, with an empty body
curl -i -s -H 'X-Correlation-ID: my-trace-1' localhost:8080/api/orders
响应头里有同一个 X-Correlation-ID;服务端日志里也是它The response header carries the same X-Correlation-ID, and so does the server log
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 16 道题逐题对照The 16 problems, compared one by one · 面试八股Interview questions
L1认出来Spot it认出考点:这道题在考什么Name the point: what is this question testingDrillLab 自出Written by DrillLab

面试官出题:「实现一个 Kanban 看板,卡片可以在三列之间移动。」 这道题最核心的考点是哪一个?

The interviewer says: “Build a Kanban board where a card can move between three columns.” Which is the central point this question tests?

先选一个选项Pick an option first
来自From 缺口一 · Dropdown、Tabs、星级评分Gap 1 · dropdown, tabs and star rating · 面试八股Interview questions
L2填空Fill the blanks补全「点外面关掉」Fill in "click outside closes it"DrillLab 自出Written by DrillLab

四个空。第 2 个用错会导致「点自己内部也关掉」, 第 4 个漏了会泄漏监听器。

Four blanks. Get the 2nd one wrong and a click inside the dropdown closes it too; miss the 4th one and you leak a listener.

TSXsrc/components/Dropdown/index.tsx4 个空4 blanks
1const boxRef = <HTMLDivElement>(null);
2
3useEffect(() => {
4 if (!open) return;
5
6 const onDocClick = (e: MouseEvent) => {
7 if (boxRef.current && !boxRef.current.(e.target as Node)) {
8 setOpen(false);
9 }
10 };
11
12 document.addEventListener("", onDocClick);
13 return () => document.("", onDocClick);
14}, [open]);
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 缺口一 · Dropdown、Tabs、星级评分Gap 1 · dropdown, tabs and star rating · 面试八股Interview questions
L3写整块Write a block自己写出星级评分Write the star rating yourselfDrillLab 自出Written by DrillLab

hover 预览 + 点击选中 + 再点清零。 检查器会查 ??onMouseLeave 的位置和无障碍。

Hover to preview, click to pick, click the same star again to reset. The checker looks at ??, where onMouseLeave sits, and accessibility.

要求Requirements
  • hover 到第 n 颗时前 n 颗显示为选中样式(预览)Hovering star n shows the first n stars in the filled style (a preview)
  • 鼠标移出整个组件后回到已选值Moving the mouse out of the whole component goes back to the picked value
  • 点第 n 颗设为 n 分;再点同一颗清零Clicking star n sets the score to n; clicking the same star again resets to zero
  • 每颗星是 button,带 aria-label,键盘可用Every star is a button with an aria-label, and works from the keyboard
  • 显示值必须是派生的,不许再开第三个 stateThe shown value has to be derived; a third piece of state is not allowed
TSXsrc/components/StarRating/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.