# 样卷 Section D 多出来的五块(真题里没有) > 这五块合计 **13 分**,全部落在 `volunteer-hub-练习` 工程里,每一块都有可运行代码或可直接背的答案。 | # | 块 | 分值 | 代码位置 | 怎么验 | |---|---|---|---|---| | 1 | Q4(b) 聚合查询 | 2 | `repository/SignupRepository#findTopVolunteers` + `service/SignupQueryService#topVolunteers` | `curl "localhost:8080/api/stats/top-volunteers?minHours=5&limit=10"` | | 2 | Q4(c) 动态条件查询 | 3 | `dto/SignupSearch` + `service/SignupQueryService#search` | `curl "localhost:8080/api/signups?volunteerName=ali&from=2026-11-01"` | | 3 | Q4(e) 表单 HTML | 3 | `templates/event-form.html` + `web/EventFormController` | 浏览器打开 `/events/new`,空提交看红字 | | 4 | Q5(a) 校验分层问答 | 3 | `dto/EventForm` 的注解 + 本文第 4 节答案 | 背下来即可 | | 5 | Q5(b) 数据层单元测试 | 2 | `src/test/java/.../SignupRepositoryTest` | `mvn test` | --- ## 1. Q4(b) 聚合查询 **题目**:找出累计时数超过 N 的前 10 名志愿者(原题是“消费超过 10 万的前 10 名会员”)。 **三个动作缺一不可**: | 动作 | SQL | |---|---| | 按人累计 | `GROUP BY volunteer` + `SUM(hours)` | | 超过阈值 | `HAVING SUM(hours) >= :minHours`(不能写 WHERE) | | 前 10 名 | `ORDER BY SUM(hours) DESC` + 限行数 | ```java @Query("select s.volunteer, sum(s.hours) from Signup s " + "group by s.volunteer " + "having sum(s.hours) >= :minHours " + "order by sum(s.hours) desc") List findTopVolunteers(@Param("minHours") int minHours, Pageable pageable); ``` 调用:`findTopVolunteers(5, PageRequest.of(0, 10))` —— **JPQL 没有 LIMIT,用 Pageable 限条数**。 **采分**:分组+聚合 1 分;HAVING + 排序 + 限 10 共 1 分。 **坑**:`where sum(...)` 语法错;忘 `group by` 只剩一个总数;排的是单条而不是累计。 --- ## 2. Q4(c) 动态条件查询 **题目**:按活动、姓名、日期区间**任意组合**筛选。 **两条路**: | 写法 | 代码 | 何时用 | |---|---|---| | 固定 `@Query` + `(:p is null or …)` | 见下 | 考试(短、稳) | | `Specification` | `service/SignupQueryService#search` | 工程(可组合、可复用) | ```java // 考试版:参数为 null 时该行恒真,等于不过滤 @Query(""" select s from Signup s where (:eventId is null or s.event.id = :eventId) and (:name is null or lower(s.volunteer.name) like lower(concat('%', :name, '%'))) and (:from is null or s.signupDate >= :from) and (:to is null or s.signupDate <= :to) """) List search(...); ``` ```java // 工程版:运行时拼谓词 List ps = new ArrayList<>(); if (search.getEventId() != null) ps.add(cb.equal(root.get("event").get("id"), search.getEventId())); ... return cb.and(ps.toArray(new Predicate[0])); ``` **采分**:条件为空时不过滤 1 分;全部 AND 拼接 1 分;能编译/跑通 1 分。 **坑**:写死四个条件(只覆盖“全给”);Repository 忘 `JpaSpecificationExecutor`。 --- ## 3. Q4(e) 表单 HTML 四要素: | # | 标签 | 作用 | |---|---|---| | 1 | `th:action="@{/events}"` + `method="post"` | 提交路径 | | 2 | `th:object="${eventForm}"`(在 form 上) | 绑定对象 | | 3 | `th:field="*{title}"` | 生成 name/id/value,提交后回显 | | 4 | `th:errors="*{title}"` + `th:if="${#fields.hasErrors('title')}"` | 错误回显 | Controller 两件事:GET 时把**空对象**放进 model;POST 时 `@Valid` + `BindingResult`,有错回同一视图,成功后 `redirect:/events`(PRG)。 **坑**:POST 后直接 `return "events"`(刷新重复提交);出错返回时忘了重新给下拉/关联数据。 --- ## 4. Q5(a) 校验分层问答(背这段) > **Yes, validation is necessary.** Without it, invalid or incomplete data — a blank title, a > negative capacity — would reach the database, and the failure would only surface much later. > > The most suitable Spring components are: > > 1. **Bean Validation annotations on the form-backing object** — `@NotBlank` on the title, > `@NotNull` / `@FutureOrPresent` on the date, `@Min(1)` on the capacity. They are > declarative, reusable and framework-agnostic. > 2. **`@Valid` + `BindingResult` in the controller** — `@Valid` runs the checks before the > method body executes, and `BindingResult` lets the same form be re-rendered with > `th:errors` instead of returning a 400 page. > 3. **The service layer for business rules** — rules that need other data (e.g. “the venue is > already booked that day”, “the title must be unique”) cannot be expressed as field > annotations and belong to the service, where a transaction is available. > 4. **Client-side attributes (`required`, `min`) are a convenience only** — they improve UX > but can be bypassed, so they never replace server-side validation. **采分**:先说“必要 + 理由”1 分;点名组件 1 分;分层说明 1 分。 --- ## 5. Q5(b) 数据层单元测试 ```java @DataJpaTest class SignupRepositoryTest { @Autowired private SignupRepository signupRepository; @Test void createsSignup() { Signup saved = signupRepository.save(new Signup(alice, LocalDate.now(), 4)); assertNotNull(saved.getId()); } } ``` | 注解 | 启动什么 | 何时用 | |---|---|---| | `@DataJpaTest` | 只 JPA + 内嵌库,自动回滚 | 测实体/仓储(本题) | | `@WebMvcTest` | 只 Web 层,Service/Repo 要 mock | 测 Controller 与状态码 | | `@SpringBootTest` | 整个应用 | 端到端 | **坑**:用 `@SpringBootTest`(慢且不是切片考点);写了 `save()` 却没有断言。 --- ## 跑一遍 ```bash cd volunteer-hub-练习 mvn test # 两个测试类 mvn spring-boot:run # 起服务 curl "localhost:8080/api/stats/top-volunteers?minHours=2&limit=5" curl "localhost:8080/api/signups?volunteerName=ali" open http://localhost:8080/events/new ```