Skip to content

Commit 63c6138

Browse files
committed
docs(notifications): add API test report + PRP implementation report
- docs/notifications-api-test-report.md: curl-based test report for /admin/notifications POST/PUT/DELETE, including P0 bug analysis - .claude/PRPs/reports/fix-batchinsert-typehandler-report.md: PRP report documenting the batchInsert typeHandler fix end-to-end2
1 parent 7552137 commit 63c6138

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Admin Notifications API —真实接口测试报告
2+
3+
|字段 ||
4+
|---|---|
5+
|报告生成时间 |2026-06-08 |
6+
| 测试目标 | `POST /admin/notifications``PUT /admin/notifications/{id}``DELETE /admin/notifications/{id}` |
7+
| 后端版本 | `ulticode-9001`(Spring Boot3.2.5 / Java17,profile=`dev`|
8+
| 后端 PID |309021(PM2 运行中) |
9+
| 测试账号 | dev seed `admin` / `admin123``DevUserBootstrapRunner` 创建,`role=ADMIN`|
10+
|鉴权 | JWT Bearer(Cookie)+ CSRF `X-CSRF-Token` Header(每次写操作旋转) |
11+
| 测试工具 | curl8.5.0 + Docker exec mysql + Python3 |
12+
| Controller源 | `backend-spring/.../admin/controller/AdminNotificationController.java` |
13+
| Service源 | `backend-spring/.../admin/service/impl/AdminNotificationServiceImpl.java` |
14+
| DTO | `CreateSystemNotificationRequest``UpdateSystemNotificationRequest` |
15+
16+
---
17+
18+
##1.控制器契约(来自源码审计)
19+
20+
|端点 |鉴权注解 |限流 | 请求体 |响应 |
21+
|---|---|---|---|---|
22+
| `POST /admin/notifications` | `@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")` | `admin:notification-create`30/分钟 | `CreateSystemNotificationRequest``title`/`content`/`type`全部 `@NotBlank``target` `@NotBlank``ALL``USERS`),可选 `category``userIds` | `Result<AdminNotificationVO>` |
23+
| `PUT /admin/notifications/{id}` | `@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")` | `admin:notification-update`30/分钟 | `UpdateSystemNotificationRequest``title`/`content` `@NotBlank`,可选 `type``category` | `Result<AdminNotificationVO>` |
24+
| `DELETE /admin/notifications/{id}` | `@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")` | `admin:notification-delete`30/分钟 || `Result<Void>` |
25+
26+
GET(顺带验证)走 `AdminNotificationQueryDTO` 分页 +字段过滤(`category=SYSTEM`),并按 `announcement_id` dedup。
27+
28+
---
29+
30+
##2. 测试环境前置
31+
32+
|| 实测值 |
33+
|---|---|
34+
| 后端 `GET /admin/notifications` 无 Cookie | `HTTP401 {"code":40100,"message":"Unauthorized"}`|
35+
| `POST /auth/login` with `admin/admin123` | `HTTP200`,下发 `access_token`(217B JWT)+ `refresh_token`(195B)+ `csrf_token`(65B)Cookie +响应体 `csrfToken`字段 |
36+
| Cookie 中包含 `Set-Cookie: access_token=…; HttpOnly; SameSite=lax``refresh_token=…; HttpOnly``csrf_token=…` | ✅全部 `HttpOnly``JWT_COOKIE_SECURE=false`(开发 HTTP) |
37+
|响应头 | `Content-Security-Policy: default-src 'self'``X-Frame-Options: DENY``Permissions-Policy: camera=(), microphone=(), geolocation=()` |
38+
39+
---
40+
41+
##3. 测试结果汇总
42+
43+
| # | 用例 | HTTP | 实测结果 |期望 |结论 |
44+
|---|---|---|---|---|---|
45+
| **POST** | | | | | |
46+
| P1 | happy path(`target=ALL`| **500** | `Database error: Type handler was null on parameter mapping for property '__frch_item_0.metadata'` |200 |**真实 Bug**(详见 §6) |
47+
| P2 |`title` |400 | `{"title":"Title cannot be blank"}` |400 |`@NotBlank`生效 |
48+
| P3 | `target=INVALID` |400 | `"No target users found"` |400(业务)或422 | ✅ Service拒绝(fall-through 后返回空列表) |
49+
| P4 | `target=USERS, userIds=[]` |400 | `"No target users found"` |400 ||
50+
| P5 | `target=USERS, userIds=[无效 id]` |400 | `"No target users found"` |400 ||
51+
| P6 |`X-CSRF-Token` |403 | `"CSRF token is required"` |403 |`CsrfValidationFilter`拦截 |
52+
| P7 | 无 Cookie |401 | `"Unauthorized"` |401 ||
53+
| **PUT** | | | | | |
54+
| U1 | happy path(更新 `N1`,共享 `announcement_id`| **200** | 更新 title/body成功,**`announcement_id` 的另一条记录也同步更新**(验证 SQL截图) |200 | ✅ Service广播到 `announcement_id` 同组 |
55+
| U2 | id 不存在 |404 | `"Notification not found"` |404 ||
56+
| U3 |`title` |400 | `"Title cannot be blank"` |400 ||
57+
| U4 |`X-CSRF-Token` |403 | `"CSRF token is required"` |403 ||
58+
| U5 | 无 Cookie |401 | `"Unauthorized"` |401 ||
59+
| **DELETE** | | | | | |
60+
| D1 | happy path(删除 `N1`| **200** | **`announcement_id` 的全部2 条都被删除**`SELECT COUNT(*)=0`验证) |200 | ✅ Service 级联删除 |
61+
| D2 | id 不存在 |404 | `"Notification not found"` |404 ||
62+
| D3 |`X-CSRF-Token` |403 | `"CSRF token is required"` |403 ||
63+
| D4 | 无 Cookie |401 | `"Unauthorized"` |401 ||
64+
| D5 |重复删除同一 id |404 | `"Notification not found"` |404 | ✅幂等 |
65+
| **GET(辅助)** | | | | | |
66+
| G1 | `GET /admin/notifications?page=1&limit=5` 无 Cookie |401 | `"Unauthorized"` |401 ||
67+
| G2 | `GET /admin/notifications` 有 Cookie |200 | `{"items":[…],"total":1,"pageSize":5,…}`,按 `announcement_id` dedup |200 ||
68+
69+
###响应时间(已测样本)
70+
71+
| 操作 | 时延 |
72+
|---|---|
73+
| `GET /admin/notifications`(空表) |9.3 ms |
74+
| `GET /admin/notifications`(1 条 dedup) |14.4 ms |
75+
| `POST /admin/notifications` happy path |4.5 ms(500 即返回) |
76+
| `PUT /admin/notifications/{id}` happy |19.2 ms |
77+
| `DELETE /admin/notifications/{id}` happy |19.1 ms |
78+
| `DELETE`404 |11.5 ms |
79+
| `DELETE`403(无 CSRF) |2.4 ms |
80+
81+
---
82+
83+
##4. PUT happy path —广播验证证据
84+
85+
**请求**`PUT /admin/notifications/701bf8a8-b7da-49e1-a434-04bfeee33715`,body `{"title":"[FIXTURE] 系统升级预告 (已更新)","content":"升级时间调整至03:00,请重新安排。","type":"SYSTEM","category":"SYSTEM"}`
86+
87+
**响应**(节选):
88+
```json
89+
{"code":0,"message":"success","data":{"id":"701bf8a8-b7da-49e1-a434-04bfeee33715","announcementId":"947d761c-affe-401a-b806-e80d5fa80e9f","title":"[FIXTURE] 系统升级预告 (已更新)","content":"升级时间调整至03:00,请重新安排。","type":"SYSTEM","category":"SYSTEM","creator":{"id":"9f6bc78a-…","username":"admin","avatar":""}},"traceId":"t-1780930447729"}
90+
```
91+
92+
**MySQL验证(DELETE之前)**
93+
```
94+
id title body
95+
701bf8a8-b7da-49e1-a434-04bfeee33715 [FIXTURE] 系统升级预告 (已更新)升级时间调整至03:00,请重新安排。
96+
ca781967-ffcd-45ba-8862-eada77b522e0 [FIXTURE] 系统升级预告 (已更新)升级时间调整至03:00,请重新安排。
97+
```
98+
两条共享 `announcement_id=947d761c-…` 的记录都被更新——确认 Service 的 `LambdaUpdateWrapper.eq(Notification::getAnnouncementId, ...)`正确广播。
99+
100+
---
101+
102+
##5. DELETE happy path — 级联删除证据
103+
104+
**请求**`DELETE /admin/notifications/701bf8a8-b7da-49e1-a434-04bfeee33715`
105+
106+
**响应**`HTTP/1.1200 {"code":0,"message":"success","traceId":"t-1780930509722"}`(19.1 ms)
107+
108+
**MySQL验证**
109+
```
110+
SELECT COUNT(*) AS remaining FROM notifications WHERE announcement_id='947d761c-…';
111+
remaining
112+
0
113+
```
114+
两条共享 `announcement_id` 的记录均被删除。Service 的 `deleteByAnnouncementId` 分支(`announcementId != null`路径)正确触发。
115+
116+
---
117+
118+
##6. 🔴关键发现 — `POST /admin/notifications`500 Bug
119+
120+
###现象
121+
合法请求 → `HTTP500 {"code":50001,"message":"Database error","traceId":"t-1780929374933"}`
122+
123+
###根因(PM2 out 日志)
124+
```
125+
java.lang.IllegalStateException: Type handler was null on parameter mapping for
126+
property '__frch_item_0.metadata'. It was either not specified and/or could
127+
not be found for the javaType (java.util.Map) : jdbcType (null) combination.
128+
129+
The error may exist in com/ulticode/modules/notification/mapper/NotificationMapper.java
130+
The error may involve com.ulticode.modules.notification.mapper.NotificationMapper.batchInsert
131+
The error occurred while executing an update
132+
```
133+
134+
### 代码定位
135+
`backend-spring/.../notification/mapper/NotificationMapper.java`
136+
```java
137+
@Insert("<script>INSERT INTO notifications (…metadata…) VALUES " +
138+
"<foreach collection='list' item='item' separator=','>" +
139+
"(#{item.id}, …, #{item.metadata}, …)" +
140+
"</foreach></script>")
141+
int batchInsert(@Param("list") List<Notification> list);
142+
```
143+
144+
`Notification.java``@TableField(typeHandler = JacksonTypeHandler.class)``Map<String,Object> metadata` 注册到 MyBatis-Plus 元数据,但 `@Insert` 自定义 SQL 的 `#{item.metadata}`不会继承 `@TableField` 的 typeHandler——MyBatis 无法把 `Map`绑定到 `JSON` 列上。
145+
146+
###修复建议(最小改动)
147+
在 mapper注解里显式声明 typeHandler:
148+
```java
149+
@Insert("<script>INSERT INTO notifications " +
150+
"(id, user_id, type, category, title, body, link, metadata, announcement_id, is_read, read_at, created_at, updated_at) VALUES " +
151+
"<foreach collection='list' item='item' separator=','>" +
152+
"(#{item.id}, #{item.userId}, #{item.type}, #{item.category}, #{item.title}, " +
153+
" #{item.body}, #{item.link}, " +
154+
" #{item.metadata, typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class}, " +
155+
" #{item.announcementId}, #{item.isRead}, #{item.readAt}, #{item.createdAt}, #{item.updatedAt})" +
156+
"</foreach></script>")
157+
int batchInsert(@Param("list") List<Notification> list);
158+
```
159+
160+
或者改为逐条 `insert`(自循环),由 MyBatis-Plus 元数据自动选择 typeHandler。
161+
162+
### 影响
163+
- 前端管理后台 `management/src/api/admin/notifications.ts` 中的 `notificationsApi.create(...)`路径整体不可用。
164+
- 工作流:"管理员发公告" 是核心运维动作,发布期间完全失败。
165+
- 由于本测试中用 fixture绕过 batchInsert 才完成 PUT/DELETE 测试,**PUT/DELETE 通过是真实业务结果,但 POST 的业务路径本身不能端到端验证**
166+
167+
---
168+
169+
##7.鉴权 &防护矩阵
170+
171+
|维度 | 实现 | 测试结果 |
172+
|---|---|---|
173+
| JWT鉴权 | `JwtAuthenticationFilter` + `access_token` Cookie(HttpOnly) | ✅401 无 Cookie 时拦截 |
174+
| RBAC | `@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")` | ✅三个写端点均有注解(管理员有 `ADMIN`角色通过) |
175+
| CSRF | `CsrfValidationFilter`(POST/PUT/DELETE/PATCH校验) | ✅403缺 `X-CSRF-Token` 时拦截 |
176+
| CSRF Token Rotation | `csrfService.validateAndRotateToken` | ✅每次写操作返回 `X-New-CSRF-Token`**前端必须每次都用最新值** |
177+
|限流 | `@RateLimit(key="admin:notification-…", limit=30, period=60)` | ✅注解存在,未触发(未压测) |
178+
|审计 | `@Audited(action = CREATE/UPDATE/DELETE_NOTIFICATION, entityType = ENTITY_NOTIFICATION)` | ✅注解存在,未在数据库单独验证 |
179+
| 参数校验 | `@Valid` + `@NotBlank`(DTO字段级) | ✅ title/content/type/target 空值400 |
180+
|业务校验 | `getTargetUserIds` 检查 target/USERS有效性 | ✅ INVALID/空 userIds →400 |
181+
182+
---
183+
184+
##8. 一致性观察
185+
186+
- **数据库→列表 dedup**`GET /admin/notifications` 通过 `selectDedupedAnnouncements``announcement_id` 去重,每条公告只展示一次;这与 PUT/DELETE 按 `announcement_id`广播修改/删除保持一致。
187+
- **审计身份取自 principal**`createSystemNotification``SecurityUtil.getCurrentUserId()` + `userMapper.selectById` 获取 creator,未从请求体取——符合项目 "审计身份取自认证 principal,不取自请求体"约定。
188+
- **错误码体系**:成功 `code=0`,业务错4xxxx(400/403/404),系统错5xxxx(50001 数据库),与项目 `Result<T>` 信封一致。
189+
190+
---
191+
192+
##9.总结与建议
193+
194+
### 通过情况
195+
|端点 |核心路径 |边界用例 | 总评 |
196+
|---|---|---|---|
197+
| `POST /admin/notifications` | ❌(500 bug) | ✅校验/鉴权/CSRF/Auth 都正确 | **不通过** —需修复 batchInsert |
198+
| `PUT /admin/notifications/{id}` ||| **通过** |
199+
| `DELETE /admin/notifications/{id}` ||| **通过** |
200+
201+
###优先级建议
202+
1. **🔴 P0**:修复 `NotificationMapper.batchInsert` 的 typeHandler缺失(详见 §6)。
203+
2. **🟡 P1**:补一个 `AdminNotificationControllerIT`(Testcontainers),覆盖 POST happy path 的回归保护——目前三个端点没有 `*IT.java`集成测试(仓库结构扫描确认)。
204+
3. **🟢 P2**:补充 CSRF旋转失败时的错误处理文档(前端 `X-New-CSRF-Token`失效场景下的重试策略)。
205+
206+
### 测试覆盖率(已测用例)
207+
-正常路径:4(GET×1, POST跳过, PUT, DELETE)
208+
-鉴权路径:6(401×3,403×3)
209+
-业务校验路径:4(P2-P5, U2, U3)
210+
-幂等性:1(D5重复删除)
211+
212+
###端到端验证注意事项
213+
- 由于 POST bug,本报告对 PUT/DELETE 使用了「MySQL手工插入 fixture → HTTP 调用 → MySQL 回读校验」的替代流程,验证的是 controller/service 对数据库中已有记录的真实行为,**未走 controller→service→mapper写入路径**
214+
-修复 POST 后应补一组 fixture 测试,验证「POST →立即 GET →列表 dedup正确」以及「POST →立即 DELETE → 同 announcement_id 全删」。

0 commit comments

Comments
 (0)