# VolunteerHub 志愿者报名平台 — Paper 2 Section D 模拟题参考工程 > 这是**一道完整的 20 分模拟题**(结构、分值、问法与 C25 真题 Section D 一致,但业务完全不同), > 加上一份可运行的 Spring Boot 参考实现。 --- # 题目 / Question(中英双语) **EN —** A non-profit organisation runs community volunteer events. Volunteers sign up for events; one volunteer may join many events, and one event has many volunteers. Every sign-up records **when** the volunteer signed up and **how many hours** he or she contributed. Organisers need a page that lists every event together with its volunteers, the number of sign-ups, the total contributed hours and the **fill rate** (number of sign-ups ÷ capacity). Volunteers also need a REST endpoint to sign up for an event. The system is built with **Spring Boot, Spring Data JPA and Thymeleaf**. Controllers interact directly with the JPA repositories (no service layer in the original paper). **You do NOT need to write import statements or getters/setters. State any assumptions.** **中 —** 公益组织举办社区志愿活动:一个志愿者可以报名多个活动,一个活动有多个志愿者。每条报名记录**报名日期**与**服务时数**。运营页要列出每个活动及其志愿者、报名人数、总服务时数、**满员率**(报名人数 ÷ 容量)。志愿者还需要一个报名用的 REST 接口。 系统使用 **Spring Boot + Spring Data JPA + Thymeleaf**;Controller 直接调用 JPA Repository(原卷不要求 Service 层)。**不需要写 import,也不需要写 getter/setter;可声明假设。** --- ## 五个小问 / Five sub-questions | # | 题目 / Question | 分值 | 代码位置 | |---|---|---|---| | **a** | **EN** — Design and write the Java code for the entity models `Volunteer`, `Event` and `Signup`. You may add attributes and methods where necessary.
**中** — 设计并写出 `Volunteer`、`Event`、`Signup` 三个实体;可按需增加属性与方法。 | 6 | `model/Volunteer.java`、`model/Event.java`、`model/Signup.java` | | **b** | **EN** — Write the JPA Repository code with the necessary JPA Queries (if any) to support the use case *Display Events and their Volunteers*.
**中** — 写出支持“显示活动及其志愿者”页面的 JPA Repository 代码(如有需要,附上查询)。 | 2 | `repository/EventRepository.java` | | **c** | **EN** — Write the Controller code for the use case *Display Events and their Volunteers*.
**中** — 写出该用例的 Controller 代码。 | 2 | `web/EventController.java` | | **d** | **EN** — Write the HTML with Thymeleaf: for each event show its title, date, location, capacity, number of sign-ups, total hours and fill rate as a percentage; for each volunteer show the name and the hours contributed to that event.
**中** — 用 Thymeleaf 写页面:每个活动显示标题、日期、地点、容量、报名人数、总时数、满员率(百分比);每个志愿者显示姓名与该活动中的服务时数。 | 5 | `src/main/resources/templates/events.html` | | **e** | **EN** — A REST API allows a volunteer to sign up: `POST /api/events/{eventId}/signup`, body contains `volunteerId` and `hours`. If the event does not exist, or it is already full, return **422 Unprocessable Entity**; otherwise return **201 Created**.
**中** — 报名 REST 接口:`POST /api/events/{eventId}/signup`,请求体含 `volunteerId` 与 `hours`;活动不存在或已满员返回 **422**,成功返回 **201**。 | 5 | `api/SignupRestController.java` + `service/SignupService.java` | > **关于 Service**:原卷明确“Service 层以后再加”,所以考试答案不需要 Service。 > 本工程额外写了 `service/` 两个文件,是用**枚举**表达业务结果的练习版(`SignupResult` + `SignupService`), > 用来对照“Controller 直连 Repository”与“分层”两种写法的差别。 --- ## 文件结构 / Project layout ``` volunteer-hub-练习/ ├── README.md ← 本文件(题目 + 说明) ├── pom.xml ← Spring Boot 3.3 / Java 17 ├── src/main/java/com/example/volunteer/ │ ├── VolunteerHubApplication.java ← 启动类(含题目总述) │ ├── model/ │ │ ├── Volunteer.java ← (a) 反向端 @OneToMany │ │ ├── Event.java ← (a)(d) 三个计算属性 + cascade/orphanRemoval │ │ └── Signup.java ← (a) 中间实体,两个 @ManyToOne │ ├── repository/ │ │ ├── EventRepository.java ← (b) @Query + join fetch + distinct │ │ ├── VolunteerRepository.java ← (e) 校验志愿者 │ │ └── SignupRepository.java ← (e) save + existsBy… │ ├── service/ │ │ ├── SignupResult.java ← 业务结果枚举(6 种) │ │ └── SignupService.java ← 五条业务规则 + @Transactional │ ├── web/EventController.java ← (c) @Controller + Model + 视图名 │ ├── api/SignupRestController.java ← (e) @RestController + 422/201 │ └── config/DataInitializer.java ← 示例数据(非考点) ├── src/main/resources/ │ ├── application.properties ← H2 + JPA 配置 │ └── templates/events.html ← (d) Thymeleaf 页面 └── relations-examples/ ← 【本题之外】6 种关系映射示例 ├── README.md ├── 01-one-to-many/ ← 1:N(Post / Comment) ├── 02-many-to-many/ ← M:N 无属性(Student / Course) ├── 03-many-to-many-with-attributes/ ← M:N 带属性(Employee / Project / Assignment) ├── 04-one-to-one/ ← 1:1(Employee / Desk) ├── 05-self-reference/ ← 自关联(Category) └── 06-two-relations/ ← 同一对实体两条关系(Doctor / Department) ``` --- ## 怎么跑 / How to run ```bash cd volunteer-hub-练习 mvn spring-boot:run ``` - 页面: - 报名接口: ```bash curl -i -X POST http://localhost:8080/api/events/1/signup \ -H "Content-Type: application/json" \ -d '{"volunteerId":1,"hours":3}' # 期望:HTTP/1.1 201 Created curl -i -X POST http://localhost:8080/api/events/999/signup \ -H "Content-Type: application/json" \ -d '{"volunteerId":1,"hours":3}' # 期望:HTTP/1.1 422 Unprocessable Entity ``` H2 控制台:(JDBC URL `jdbc:h2:mem:volunteerdb`,用户 `sa`,密码空) --- ## 答案要点 / Marking points | 问 | 关键判断 | 常见失分 | |---|---|---| | a | **M:N 带字段 → 拆 `Signup` 实体**,不是 `@ManyToMany` | 写成 `@ManyToMany`,丢掉 `hours` | | b | 2 分只需一个接口;正式写法用 `left join fetch` + `distinct` | JPQL 写成 SQL;用 `join` 丢掉无报名的活动 | | c | `@Controller` + `Model` + 返回视图名 | 用 `@RestController`;键名与模板不一致 | | d | 报名数 / 总时数 / 满员率×100 / 志愿者名与时数 | 漏项;满员率没乘 100 | | e | 两个 422 条件 + 201,`@RestController` + `ResponseEntity` | 返回 404/400;忘判满员;`findById().get()` 未判空 | --- ## 说明 / Notes - 每份 Java 文件的头部注释里都写着它对应的**中英双语题目**,方便对照“题目要求 ↔ 代码实现”。 - `relations-examples/` 不参与编译(在 `src` 之外),每题独立,可直接复制注解。 - 本机当前没有 JDK / Maven(`javac`、`mvn` 均未安装),因此**未做编译验证**;代码按 Spring Boot 3.3 + Java 17 编写,若你的环境是 Boot 2.x,需把 `jakarta.persistence` 改回 `javax.persistence`。 --- # 追加:样卷补充五块(13 分,真题里没有) 原工程只覆盖**真题风格**的五问。为了覆盖 **C25 样卷**(Minishop)多出来的考点,追加了以下内容; 详细讲解见 [`docs/样卷补充-五块.md`](docs/样卷补充-五块.md)。 | # | 块 | 分值 | 新增文件 | |---|---|---|---| | 1 | Q4(b) 聚合查询(TOP N + HAVING) | 2 | `repository/SignupRepository#findTopVolunteers`、`service/SignupQueryService#topVolunteers`、`service/VolunteerHours` | | 2 | Q4(c) 动态条件查询 | 3 | `dto/SignupSearch`、`service/SignupQueryService#search`、`service/SignupView`、`api/QueryApiController` | | 3 | Q4(e) 表单 HTML + PRG | 3 | `dto/EventForm`、`web/EventFormController`、`templates/event-form.html` | | 4 | Q5(a) 校验分层问答 | 3 | `dto/EventForm` 的 Bean Validation 注解 + 文档第 4 节(可直接背的英文答案) | | 5 | Q5(b) 数据层单元测试 | 2 | `src/test/java/com/example/volunteer/SignupRepositoryTest`(`@DataJpaTest`)、`EventControllerTest`(`@WebMvcTest`) | ## 新增接口 / New endpoints ```bash # 样卷 Q4(b):累计时数 ≥ 2 的前 5 名志愿者 curl "http://localhost:8080/api/stats/top-volunteers?minHours=2&limit=5" # → [{"volunteerId":1,"name":"Alice Tan","totalHours":6}, …] # 样卷 Q4(c):任意组合查询(每个参数都可省略) curl "http://localhost:8080/api/signups?volunteerName=ali&from=2026-11-01" curl "http://localhost:8080/api/signups?eventId=1" curl "http://localhost:8080/api/signups" ``` ## 新增页面 / New page - `GET /events/new` → 表单页(`templates/event-form.html`) - `POST /events` → 提交;校验失败回显红字,成功 `302 → /events` ## 跑测试 / Run the tests ```bash mvn test ``` | 测试类 | 注解 | 验证什么 | |---|---|---| | `SignupRepositoryTest` | `@DataJpaTest` | 创建报名(id 生成、exists 查询)、累计时数聚合 | | `EventControllerTest` | `@WebMvcTest` | `/events` 返回 200、视图名 `events`、model 里有 `events` | ## 更新后的目录(只列新增部分) ``` volunteer-hub-练习/ ├── docs/ │ └── 样卷补充-五块.md ← 五块的讲解 + Q5(a) 可背答案 ├── src/main/java/com/example/volunteer/ │ ├── dto/ │ │ ├── EventForm.java ← @NotBlank/@Min/@FutureOrPresent(Q5a 的落地) │ │ └── SignupSearch.java ← 四个可选条件(Q4c) │ ├── service/ │ │ ├── SignupQueryService.java ← 聚合 + Specification 动态查询 │ │ ├── VolunteerHours.java ← 聚合结果投影 │ │ └── SignupView.java ← 动态查询结果投影 │ ├── api/QueryApiController.java ← 两个查询接口的 HTTP 出口 │ └── web/EventFormController.java ← GET 表单 / POST + @Valid + PRG ├── src/main/resources/templates/event-form.html └── src/test/java/com/example/volunteer/ ├── SignupRepositoryTest.java └── EventControllerTest.java ```