ai-engineering-init 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,453 @@
1
+ ---
2
+ name: auto-test
3
+ description: |
4
+ API 自动化测试技能。基于 Apifox MCP 读取接口文档,使用 Hurl 生成并执行真实 HTTP 测试,
5
+ 支持单接口测试、单业务组合(CRUD 生命周期)、跨业务串联(多模块流程),生成 HTML/JSON 测试报告。
6
+ 测试失败时自动调用 fix-bug 技能走标准修复流程。
7
+
8
+ 触发场景:
9
+ - 需要对 API 接口进行自动化测试
10
+ - 需要验证接口是否符合文档定义
11
+ - 需要组合多个接口形成业务流程测试
12
+ - 需要生成接口测试报告
13
+ - 开发完成后需要回归测试验证
14
+
15
+ 触发词:自动测试、auto-test、接口测试、API测试、Hurl、测试报告、回归测试
16
+ ---
17
+
18
+ # API 自动化测试(Hurl + Apifox MCP)
19
+
20
+ ## 概述
21
+
22
+ 通过 Apifox MCP 读取接口文档 → AI 生成 Hurl 测试文件 → 执行真实 HTTP 请求 → 生成测试报告 → 失败项**自动调用 `/fix-bug` 走标准修复流程**。
23
+
24
+ **前置依赖**:
25
+ - Hurl CLI(`brew install hurl` / `winget install hurl` / `apt install hurl`)
26
+ - Apifox MCP Server(已配置项目 ID 和 Access Token)
27
+ - 后端服务运行中
28
+
29
+ ## 测试粒度
30
+
31
+ | 粒度 | 说明 | 文件命名 | 示例 |
32
+ |------|------|---------|------|
33
+ | **单接口** | 一个 API 端点的正常+异常场景 | `{接口名}.hurl` | `create-order.hurl` |
34
+ | **单业务组合** | 一个模块的 CRUD 生命周期 | `{模块}-lifecycle.hurl` | `order-lifecycle.hurl` |
35
+ | **跨业务串联** | 多模块联动的完整业务流程 | `{流程名}-flow.hurl` | `menu-order-payment-flow.hurl` |
36
+
37
+ ## 文件结构
38
+
39
+ ```
40
+ tests/hurl/
41
+ ├── env/
42
+ │ ├── dev.env # 开发环境变量
43
+ │ ├── test.env # 测试环境
44
+ │ └── prod.env # 生产环境(只读接口)
45
+ ├── {模块名}/ # 按业务模块组织
46
+ │ ├── {接口名}.hurl # 单接口测试
47
+ │ └── {模块}-lifecycle.hurl # 单业务组合
48
+ ├── flows/ # 跨业务串联
49
+ │ └── {流程名}-flow.hurl
50
+ └── reports/ # 测试报告(gitignore)
51
+ ├── index.html
52
+ └── report.json
53
+ ```
54
+
55
+ ## 系统接口请求响应规范
56
+
57
+ > 详细参考:`references/api-conventions.md`(首次生成测试时必须读取)
58
+
59
+ ### 核心契约速查
60
+
61
+ | 项目 | 规范 |
62
+ |------|------|
63
+ | 请求封装 | `{"content": { ... }}` (LeRequest) |
64
+ | 成功码 | `code == 10000` |
65
+ | 认证头 | `X-Token` + `merchant-id` + `Merchant-Id` |
66
+ | 未授权 | 不带 X-Token → HTTP 401 |
67
+
68
+ ### 返回类型 → data 结构映射
69
+
70
+ | Controller 返回类型 | data 结构 | 断言关键路径 |
71
+ |---------------------|----------|-------------|
72
+ | `Long`(save) | 雪花 ID | `$.data` isInteger |
73
+ | `void`(delete/submit/export) | null | 仅验证 `$.code == 10000` |
74
+ | `XxxVO`(detail) | 对象 | `$.data.id` exists |
75
+ | `PageVO<T>`(设置类 page) | `$.data.records[]` | `$.data.records` isCollection |
76
+ | `ReportBaseTotalVO<T>`(报表 page) | `$.data.resultPage.records[]` + `$.data.totalLine` | 分页 + 合计行 |
77
+
78
+ ### content 传值类型(最易出错)
79
+
80
+ | 场景 | content 类型 | 示例 |
81
+ |------|-------------|------|
82
+ | 分页查询 | 对象 | `{"page":{"current":1,"size":10},"keyword":"xxx"}` |
83
+ | 新增/编辑 | 对象 | `{"name":"xxx","entries":[...]}` |
84
+ | 详情/删除/提交 | **裸 Long** | `{{id}}`(不是 `{"id":{{id}}}`) |
85
+
86
+ > **关键区别**:detail/delete/submit 的 content 是**直接传 ID 值**,不需要包装成对象。
87
+
88
+ ## Hurl 语法速查
89
+
90
+ ### 请求头规范(必须遵守)
91
+
92
+ 每个需要认证的请求必须携带以下 Header:
93
+
94
+ | Header | 来源 | 用途 |
95
+ |--------|------|------|
96
+ | `X-Token` | 环境变量 `{{x_token}}`(配置文件提供) | 认证令牌 |
97
+ | `merchant-id` | 环境变量 `{{merchant_id}}` | 商户路由(数据源切换) |
98
+ | `Merchant-Id` | 环境变量 `{{merchant_id_auth}}` | 商户权限校验 |
99
+
100
+ ### 基础请求 + 断言
101
+
102
+ ```hurl
103
+ # 请求
104
+ POST {{base_url}}/api/v2/web/order/add
105
+ Content-Type: application/json
106
+ X-Token: {{x_token}}
107
+ merchant-id: {{merchant_id}}
108
+ Merchant-Id: {{merchant_id_auth}}
109
+ {
110
+ "content": {
111
+ "menuId": 1001,
112
+ "quantity": 2
113
+ }
114
+ }
115
+
116
+ # 响应断言
117
+ HTTP 200
118
+ [Asserts]
119
+ jsonpath "$.code" == 10000
120
+ jsonpath "$.msg" == "操作成功"
121
+ jsonpath "$.data" isInteger
122
+ ```
123
+
124
+ ### 变量捕获(请求间传递数据)
125
+
126
+ ```hurl
127
+ # 如果需要动态获取 Token,可以通过登录接口捕获
128
+ POST {{base_url}}/api/v2/web/auth/login
129
+ Content-Type: application/json
130
+ {
131
+ "username": "{{username}}",
132
+ "password": "{{password}}"
133
+ }
134
+
135
+ HTTP 200
136
+ [Captures]
137
+ x_token: jsonpath "$.data.token"
138
+ ```
139
+
140
+ > 注意:X-Token 优先使用 env 文件中预配置的值,避免每次都调登录接口。
141
+ > 仅在测试登录流程本身或 Token 过期场景时,才通过登录接口动态获取。
142
+
143
+ ### 常用断言
144
+
145
+ ```hurl
146
+ [Asserts]
147
+ # 状态码
148
+ status == 200
149
+
150
+ # JSON 路径
151
+ jsonpath "$.code" == 10000
152
+ jsonpath "$.data" exists
153
+ jsonpath "$.data" isCollection
154
+ jsonpath "$.data.id" isInteger
155
+ jsonpath "$.data.name" isString
156
+ jsonpath "$.data.list" count > 0
157
+
158
+ # 类型检查
159
+ jsonpath "$.data.price" isFloat
160
+
161
+ # 包含
162
+ jsonpath "$.msg" contains "成功"
163
+
164
+ # 正则
165
+ jsonpath "$.data.phone" matches "^1[3-9]\\d{9}$"
166
+ ```
167
+
168
+ ### 环境变量文件格式
169
+
170
+ ```properties
171
+ # env/dev.env
172
+ base_url=http://192.168.97.235:58300
173
+
174
+ # 认证 Token(X-Token 请求头,配置后所有请求自动携带)
175
+ x_token=你的Token值
176
+
177
+ # 商户路由(merchant-id 请求头,用于数据源切换)
178
+ merchant_id=你的商户ID
179
+
180
+ # 商户权限(Merchant-Id 请求头,用于权限校验)
181
+ merchant_id_auth=你的商户ID
182
+ ```
183
+
184
+ ## 执行命令
185
+
186
+ ```bash
187
+ # 运行单个测试
188
+ hurl --test --variables-file tests/hurl/env/dev.env tests/hurl/order/create-order.hurl
189
+
190
+ # 运行模块所有测试
191
+ hurl --test --variables-file tests/hurl/env/dev.env tests/hurl/order/*.hurl
192
+
193
+ # 运行全部测试 + 生成报告
194
+ hurl --test \
195
+ --variables-file tests/hurl/env/dev.env \
196
+ --report-html tests/hurl/reports \
197
+ --report-json tests/hurl/reports/report.json \
198
+ tests/hurl/**/*.hurl
199
+
200
+ # 传递动态变量(如时间戳避免唯一键冲突)
201
+ TS=$(date +%s) && hurl --test \
202
+ --variables-file tests/hurl/env/dev.env \
203
+ --variable "voucher_type_ts=$TS" \
204
+ --variable "voucher_word_ts=$TS" \
205
+ tests/hurl/finance/*.hurl
206
+
207
+ # 指定超时(毫秒)
208
+ hurl --test --connect-timeout 5000 --max-time 30000 ...
209
+
210
+ # 失败时继续执行(不中断)
211
+ hurl --test --continue-on-error ...
212
+ ```
213
+
214
+ ## 生成测试的流程
215
+
216
+ ### 第一步:读取接口文档
217
+
218
+ 通过 Apifox MCP 读取指定模块的接口列表:
219
+ - 接口路径、HTTP 方法
220
+ - 请求参数(Header、Body、Query)
221
+ - 响应结构(字段名、类型、示例值)
222
+
223
+ ### 第二步:读取 Param/VO 源码(关键!)
224
+
225
+ **必须读取后端源码**以确保测试完整性:
226
+
227
+ 1. **Param 类**:提取所有查询条件字段,确保每个字段都有对应测试用例
228
+ 2. **VO 类**:提取所有响应字段,在断言中验证字段存在性和类型
229
+ 3. **Mapper XML**:理解 SQL 逻辑、JOIN 关系、动态条件
230
+
231
+ ```
232
+ 示例:SubjectDetailParam 有 startDate、endDate、areaId、canteenId、keyword
233
+ → 测试用例必须覆盖:
234
+ - 基础分页查询(startDate + endDate)
235
+ - keyword 搜索(科目编码/名称)
236
+ - 区域+食堂筛选(areaId + canteenId)
237
+ - 单日查询(startDate == endDate)
238
+ - 空结果区间(验证空数组返回)
239
+ - 分页第二页
240
+ - 导出接口
241
+ - 未授权测试
242
+ ```
243
+
244
+ ### 第三步:准备测试数据(先查后用,禁止硬编码)
245
+
246
+ **核心原则:测试数据必须从真实环境动态获取,不能硬编码不存在的 ID 或编码。**
247
+
248
+ ```hurl
249
+ # ✅ 正确:先查询获取真实数据,再用于后续请求
250
+ # 0a. 获取真实食堂 ID
251
+ POST {{base_url}}/api/v2/alloc/canteen/page-canteen
252
+ ...
253
+ [Captures]
254
+ canteen_id: jsonpath "$.data.records[0].canteenId"
255
+ area_id: jsonpath "$.data.records[0].areaId"
256
+
257
+ # 0b. 获取关联表的真实记录
258
+ POST {{base_url}}/report/finance/setting/voucher-type/page
259
+ ...
260
+ [Captures]
261
+ voucher_type_id: jsonpath "$.data.records[0].id"
262
+ ```
263
+
264
+ ```hurl
265
+ # ❌ 错误:硬编码不存在的引用数据
266
+ {
267
+ "content": {
268
+ "costNo": "9999", # 可能不存在
269
+ "voucherTypeId": 1 # 可能不存在
270
+ }
271
+ }
272
+ ```
273
+
274
+ **测试数据准备检查清单**:
275
+ - [ ] 外键引用的 ID 通过查询接口动态获取
276
+ - [ ] 业务编码(如 costNo)使用数据库中已存在的真实记录
277
+ - [ ] 唯一键字段使用时间戳变量避免冲突(`{{xxx_ts}}`)
278
+ - [ ] 创建的测试数据标记 summary 含 `auto-test` 便于识别清理
279
+ - [ ] 测试结束后有清理步骤(删除测试数据)
280
+
281
+ ### 第四步:生成 .hurl 文件
282
+
283
+ **查询条件完整覆盖**(每个 Param 字段都要有测试用例):
284
+
285
+ ```
286
+ 单接口测试场景清单:
287
+ 1. 基础分页查询 — 必填参数,预期 code=10000,验证分页结构+VO 所有字段
288
+ 2. 合计行验证 — 如有 totalLine,验证所有合计字段存在
289
+ 3. 逐个查询条件 — 每个 Param 字段单独或组合测试
290
+ 4. 空结果验证 — 故意使用不匹配的条件,验证 records count == 0
291
+ 5. 分页翻页 — current=2 验证翻页正确
292
+ 6. 导出接口 — 验证 code=10000(异步导出返回 void)
293
+ 7. 未授权测试 — 不带 X-Token,预期 HTTP 401
294
+ 8. 数据清理 — 删除测试创建的数据
295
+ ```
296
+
297
+ **数据正确性验证**(不只是结构存在,还要验证值合理):
298
+
299
+ ```hurl
300
+ # ✅ 结构验证 + 数据正确性
301
+ [Asserts]
302
+ jsonpath "$.code" == 10000
303
+ jsonpath "$.data.resultPage.records" isCollection
304
+ jsonpath "$.data.resultPage.records" count > 0
305
+ # VO 字段存在性
306
+ jsonpath "$.data.resultPage.records[0].costNo" isString
307
+ jsonpath "$.data.resultPage.records[0].costTypeName" isString # 关联字段不能为 null
308
+ jsonpath "$.data.resultPage.records[0].debitAmount" exists
309
+ # 合计行数据合理性
310
+ jsonpath "$.data.totalLine.debitAmount" exists
311
+ jsonpath "$.data.totalLine.creditAmount" exists
312
+ ```
313
+
314
+ ### 第五步:执行并生成报告
315
+
316
+ ```bash
317
+ hurl --test \
318
+ --variables-file tests/hurl/env/dev.env \
319
+ --report-html tests/hurl/reports \
320
+ --report-json tests/hurl/reports/report.json \
321
+ --continue-on-error \
322
+ tests/hurl/{模块}/*.hurl
323
+ ```
324
+
325
+ ### 第六步:解析报告
326
+
327
+ 读取 `report.json`,输出摘要:
328
+
329
+ ```markdown
330
+ ## 测试报告摘要
331
+
332
+ | 状态 | 数量 | 占比 |
333
+ |------|------|------|
334
+ | PASS | 15 | 75% |
335
+ | FAIL | 3 | 15% |
336
+ | ERROR| 2 | 10% |
337
+
338
+ ### 失败详情
339
+
340
+ | 文件 | 接口 | 失败原因 |
341
+ |------|------|---------|
342
+ | order/create-order.hurl | POST /api/v2/web/order/add | 预期 code=10000,实际 code=40001 |
343
+ | menu/query-menu.hurl | GET /api/v2/web/menu/get/1 | 预期 HTTP 200,实际 HTTP 500 |
344
+ ```
345
+
346
+ ### 第七步:失败项自动触发 fix-bug 流程
347
+
348
+ **当测试存在失败项时,必须自动调用 `/fix-bug` 走标准修复流程:**
349
+
350
+ ```
351
+ 测试失败 → 分析失败原因 → 分类处理:
352
+
353
+ 1. 测试数据问题(非代码 Bug):
354
+ - 引用数据不存在 → 修正测试用例中的数据
355
+ - 唯一键冲突 → 添加时间戳变量
356
+ - 权限/状态限制 → 调整测试前置条件
357
+
358
+ 2. 后端代码 Bug:
359
+ → 自动调用 Skill(fix-bug) 走标准修复流程
360
+ → 包含:排查报告 → 用户确认 → 修复代码 → 重跑测试验证
361
+
362
+ 3. 接口文档与实现不一致:
363
+ → 输出差异报告,等待用户确认以哪个为准
364
+ ```
365
+
366
+ **fix-bug 自动触发条件**:
367
+ - HTTP 状态码非预期(如 500)
368
+ - 业务码非预期(如 code != 10000)
369
+ - 响应字段缺失或类型不匹配
370
+ - 关联字段为 null(如 costTypeName 为 null 说明 JOIN 失败)
371
+ - 导出数据异常(原始字段暴露、金额未转换等)
372
+
373
+ **Bug 报告格式**(传递给 fix-bug):
374
+
375
+ ```
376
+ Bug 信息:
377
+ - 接口:{METHOD} {URL}
378
+ - 请求参数:{request body}
379
+ - 预期响应:{expected}
380
+ - 实际响应:{actual}
381
+ - Hurl 文件:{file path}
382
+ - 失败断言:{assertion detail}
383
+ - 关联源码:{Controller/Service/Mapper 文件路径}
384
+ ```
385
+
386
+ ## LeRequest 适配
387
+
388
+ leniu 项目的 POST 请求体使用 `LeRequest<T>` 封装:
389
+
390
+ ```hurl
391
+ # ✅ 正确:使用 content 包装 + 三个必要 Header
392
+ POST {{base_url}}/api/v2/web/order/add
393
+ Content-Type: application/json
394
+ X-Token: {{x_token}}
395
+ merchant-id: {{merchant_id}}
396
+ Merchant-Id: {{merchant_id_auth}}
397
+ {
398
+ "content": {
399
+ "menuId": 1001,
400
+ "quantity": 2
401
+ }
402
+ }
403
+
404
+ # ❌ 错误:直接传业务字段 / 缺少 Header
405
+ POST {{base_url}}/api/v2/web/order/add
406
+ {
407
+ "menuId": 1001,
408
+ "quantity": 2
409
+ }
410
+ ```
411
+
412
+ ## 已知陷阱(Lessons Learned)
413
+
414
+ ### 1. del_flag 约定不统一
415
+ leniu 主表用 `2=正常, 1=删除`,但某些设置表(如 `finance_voucher_type`, `finance_voucher_word`)使用 `@TableLogic` 默认值 `0=正常, 1=删除`。**不要盲目统一,要检查每张表的实际约定。**
416
+
417
+ ### 2. 测试数据必须使用真实存在的引用数据
418
+ 关联查询(JOIN)依赖引用数据存在且状态正确。例如:
419
+ - `cost_type` 表的 `state = 1` 才是有效记录
420
+ - `costNo` 必须在 `cost_type` 中存在且 `state = 1`
421
+ - 否则 LEFT JOIN 后关联字段(如 `costTypeName`)为 null
422
+
423
+ ### 3. 导出验证要点
424
+ 导出接口是异步的(返回 void → code=10000),需要额外关注:
425
+ - VO 的 `@ExcelIgnore` 是否正确标记了内部字段
426
+ - `@ExcelProperty(order=N)` 的顺序是否符合产品要求
427
+ - 金额字段是否配置了 `converter = CustomNumberConverter.class`(分→元)
428
+ - 枚举字段是否有对应的描述字段(如 submitStatus → submitStatusDesc)
429
+
430
+ ### 4. 唯一键冲突
431
+ CRUD 生命周期测试中,新增记录的唯一键字段(如 typeCode)需要加时间戳变量:
432
+ ```hurl
433
+ "typeCode": "AT_HURL_{{voucher_type_ts}}"
434
+ ```
435
+ 执行时通过 `--variable "voucher_type_ts=$(date +%s)"` 传入。
436
+
437
+ ### 5. 后端未重启
438
+ 修改后端代码后必须重启服务才能生效。如果测试结果不符合预期,先确认后端是否已重启。
439
+
440
+ ## .gitignore 配置
441
+
442
+ ```gitignore
443
+ # Hurl 测试报告
444
+ tests/hurl/reports/
445
+ ```
446
+
447
+ ## 注意
448
+
449
+ - 测试文件(`.hurl`)需要 Git 管理,报告目录不需要
450
+ - `env/*.env` 中的密码/Token 建议用环境变量替代,不要提交敏感信息
451
+ - 如果是 leniu 项目的 POST 接口,请求体必须用 `{"content": {...}}` 包装
452
+ - 生成测试前确保 Hurl CLI 已安装(`hurl --version`)
453
+ - 与 `test-development` 技能的区别:本技能是真实 HTTP 请求的集成测试,`test-development` 是 JUnit5 单元测试/MockMvc 测试
@@ -0,0 +1,260 @@
1
+ # 系统接口请求响应规范(详细参考)
2
+
3
+ > 本文件定义了 leniu 系统的标准接口契约。生成测试用例时**必须严格匹配**这些模式。
4
+ > 安装框架后可直接使用,无需额外学习。
5
+
6
+ ## 统一请求格式
7
+
8
+ 所有 POST 接口使用 `LeRequest<T>` 封装,业务参数放在 `content` 字段内:
9
+
10
+ ```json
11
+ {
12
+ "content": { /* 业务参数 */ }
13
+ }
14
+ ```
15
+
16
+ ## 统一响应格式
17
+
18
+ ```json
19
+ {
20
+ "code": 10000, // 业务码:10000=成功,其他=失败
21
+ "msg": "操作成功", // 提示信息
22
+ "data": ... // 业务数据(类型取决于接口)
23
+ }
24
+ ```
25
+
26
+ ## 接口类型与响应模式速查
27
+
28
+ 根据 Controller 方法返回值类型,确定 data 的结构和对应断言:
29
+
30
+ | Controller 返回类型 | data 类型 | 示例 | Hurl 断言模板 |
31
+ |---------------------|----------|------|--------------|
32
+ | `Long` | 雪花 ID | `$.data` = 1234567890 | `jsonpath "$.data" isInteger` |
33
+ | `void` | null | `$.data` = null | `jsonpath "$.code" == 10000` |
34
+ | `XxxVO` | 对象 | `$.data.id`, `$.data.name` | `jsonpath "$.data.id" exists` |
35
+ | `PageVO<XxxVO>` | 分页对象 | `$.data.records[0]` | 见「标准分页」 |
36
+ | `ReportBaseTotalVO<XxxVO>` | 分页+合计 | `$.data.resultPage` + `$.data.totalLine` | 见「报表分页」 |
37
+
38
+ ## 标准分页响应(PageVO)
39
+
40
+ **适用于**:设置类 CRUD 分页查询(如凭证类型、凭证字)
41
+
42
+ ```json
43
+ {
44
+ "code": 10000,
45
+ "data": {
46
+ "records": [ { "id": 1, "name": "..." }, ... ],
47
+ "current": 1,
48
+ "size": 10,
49
+ "total": 25
50
+ }
51
+ }
52
+ ```
53
+
54
+ **Hurl 断言模板**:
55
+
56
+ ```hurl
57
+ HTTP 200
58
+ [Asserts]
59
+ jsonpath "$.code" == 10000
60
+ jsonpath "$.data.records" isCollection
61
+ jsonpath "$.data.current" exists
62
+ jsonpath "$.data.size" exists
63
+ jsonpath "$.data.total" exists
64
+ # VO 字段逐一验证
65
+ jsonpath "$.data.records[0].id" exists
66
+ jsonpath "$.data.records[0].typeName" isString
67
+ ```
68
+
69
+ ## 报表分页响应(ReportBaseTotalVO)
70
+
71
+ **适用于**:报表类分页查询(凭证列表、科目明细、科目汇总),响应包含 `resultPage`(分页数据)+ `totalLine`(合计行)
72
+
73
+ ```json
74
+ {
75
+ "code": 10000,
76
+ "data": {
77
+ "resultPage": {
78
+ "records": [ { ... }, ... ],
79
+ "current": 1,
80
+ "size": 10,
81
+ "total": 25
82
+ },
83
+ "totalLine": {
84
+ "debitAmount": 1285100,
85
+ "creditAmount": 1285000
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Hurl 断言模板**:
92
+
93
+ ```hurl
94
+ HTTP 200
95
+ [Asserts]
96
+ jsonpath "$.code" == 10000
97
+ # 分页结构
98
+ jsonpath "$.data.resultPage" exists
99
+ jsonpath "$.data.resultPage.records" isCollection
100
+ jsonpath "$.data.resultPage.current" exists
101
+ jsonpath "$.data.resultPage.size" exists
102
+ jsonpath "$.data.resultPage.total" exists
103
+ # VO 字段逐一验证(首条记录)
104
+ jsonpath "$.data.resultPage.records[0].voucherDate" isString
105
+ jsonpath "$.data.resultPage.records[0].areaName" isString
106
+ jsonpath "$.data.resultPage.records[0].costTypeName" isString # 关联字段不能为 null
107
+ # 合计行(只有数值字段和方向字段,其余为 null 占位)
108
+ jsonpath "$.data.totalLine" exists
109
+ jsonpath "$.data.totalLine.debitAmount" exists
110
+ jsonpath "$.data.totalLine.creditAmount" exists
111
+ ```
112
+
113
+ ## 各操作类型的请求与响应模式
114
+
115
+ ### 1. 新增(save)→ 返回 Long ID
116
+
117
+ ```hurl
118
+ # Controller: Long save(LeRequest<XxxSaveParam>)
119
+ POST {{base_url}}/report/finance/voucher/save
120
+ Content-Type: application/json
121
+ X-Token: {{x_token}}
122
+ merchant-id: {{merchant_id}}
123
+ Merchant-Id: {{merchant_id_auth}}
124
+ {
125
+ "content": {
126
+ "areaId": {{area_id}},
127
+ "name": "auto-test数据,可删除",
128
+ "entries": [ ... ]
129
+ }
130
+ }
131
+
132
+ HTTP 200
133
+ [Asserts]
134
+ jsonpath "$.code" == 10000
135
+ [Captures]
136
+ created_id: jsonpath "$.data" # 捕获返回的 ID 供后续使用
137
+ ```
138
+
139
+ ### 2. 编辑(save 带 id / update)→ 返回 void
140
+
141
+ ```hurl
142
+ # 方式一:save 带 id(delete-insert 模式,常用于主子表)
143
+ POST {{base_url}}/report/finance/voucher/save
144
+ { "content": { "id": {{created_id}}, "name": "修改后", ... } }
145
+
146
+ # 方式二:独立 update 接口
147
+ POST {{base_url}}/report/finance/setting/voucher-type/update
148
+ { "content": { "id": {{type_id}}, "typeName": "修改后" } }
149
+
150
+ HTTP 200
151
+ [Asserts]
152
+ jsonpath "$.code" == 10000
153
+ ```
154
+
155
+ ### 3. 查详情(detail)→ 返回 XxxVO 对象
156
+
157
+ ```hurl
158
+ # Controller: XxxDetailVO detail(LeRequest<Long>)
159
+ # 注意:content 直接传 ID 值(Long),不需要包装对象
160
+ POST {{base_url}}/report/finance/voucher/detail
161
+ { "content": {{created_id}} }
162
+
163
+ HTTP 200
164
+ [Asserts]
165
+ jsonpath "$.code" == 10000
166
+ jsonpath "$.data.id" exists
167
+ jsonpath "$.data.areaName" isString
168
+ jsonpath "$.data.entries" isCollection # 子表数据
169
+ jsonpath "$.data.entries" count == 2
170
+ jsonpath "$.data.entries[0].id" exists
171
+ ```
172
+
173
+ ### 4. 删除(delete)→ 返回 void
174
+
175
+ ```hurl
176
+ # Controller: void delete(LeRequest<Long>)
177
+ # 注意:content 直接传 ID 值(Long)
178
+ POST {{base_url}}/report/finance/voucher/delete
179
+ { "content": {{created_id}} }
180
+
181
+ HTTP 200
182
+ [Asserts]
183
+ jsonpath "$.code" == 10000
184
+ ```
185
+
186
+ ### 5. 提交/审批等状态变更 → 返回 void
187
+
188
+ ```hurl
189
+ # Controller: void submit(LeRequest<Long>)
190
+ POST {{base_url}}/report/finance/voucher/submit
191
+ { "content": {{created_id}} }
192
+
193
+ HTTP 200
194
+ [Asserts]
195
+ jsonpath "$.code" == 10000
196
+ ```
197
+
198
+ ### 6. 分页查询(page)→ 返回 PageVO 或 ReportBaseTotalVO
199
+
200
+ ```hurl
201
+ # 标准分页(设置类)
202
+ POST {{base_url}}/report/finance/setting/voucher-type/page
203
+ {
204
+ "content": {
205
+ "page": { "current": 1, "size": 10 }
206
+ }
207
+ }
208
+
209
+ # 报表分页(含合计行)
210
+ POST {{base_url}}/report/finance/voucher/page
211
+ {
212
+ "content": {
213
+ "page": { "current": 1, "size": 10 },
214
+ "startDate": "2026-03-01",
215
+ "endDate": "2026-03-31"
216
+ }
217
+ }
218
+ ```
219
+
220
+ ### 7. 导出(export)→ 返回 void(异步)
221
+
222
+ ```hurl
223
+ # Controller: void export(LeRequest<XxxPageParam>)
224
+ # 异步导出,仅验证接口不报错
225
+ POST {{base_url}}/report/finance/voucher/export
226
+ {
227
+ "content": {
228
+ "page": { "current": 1, "size": 10 },
229
+ "startDate": "2026-03-01",
230
+ "endDate": "2026-03-31"
231
+ }
232
+ }
233
+
234
+ HTTP 200
235
+ [Asserts]
236
+ jsonpath "$.code" == 10000
237
+ ```
238
+
239
+ ### 8. 未授权测试 → 预期 HTTP 401
240
+
241
+ ```hurl
242
+ # 去掉 X-Token 请求头
243
+ POST {{base_url}}/report/finance/voucher/page
244
+ Content-Type: application/json
245
+ merchant-id: {{merchant_id}}
246
+ Merchant-Id: {{merchant_id_auth}}
247
+ { "content": { "page": { "current": 1, "size": 10 } } }
248
+
249
+ HTTP 401
250
+ ```
251
+
252
+ ## content 传值类型总结
253
+
254
+ | 场景 | content 类型 | 示例 |
255
+ |------|-------------|------|
256
+ | 分页查询 | 对象(含 page + 查询条件) | `{"page":{"current":1,"size":10},"keyword":"xxx"}` |
257
+ | 新增/编辑 | 对象(业务字段) | `{"name":"xxx","entries":[...]}` |
258
+ | 查详情/删除/提交 | **裸 Long** | `{{id}}` (不是 `{"id": {{id}}}`) |
259
+
260
+ > **关键区别**:detail/delete/submit 的 content 是**直接传 ID 值**,不需要包装成对象。这是最常见的错误。