ai-engineering-init 1.14.0 → 1.14.2
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.
- package/.claude/hooks/skill-forced-eval.js +1 -0
- package/.claude/hooks/stop.js +1 -2
- package/.claude/skills/auto-test/SKILL.md +182 -10
- package/.claude/skills/code-patterns/SKILL.md +119 -0
- package/.claude/skills/codex-code-review/SKILL.md +39 -0
- package/.claude/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.claude/skills/leniu-crud-development/SKILL.md +26 -7
- package/.claude/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.claude/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.claude/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.claude/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.claude/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.codex/skills/code-patterns/SKILL.md +119 -0
- package/.codex/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.codex/skills/leniu-crud-development/SKILL.md +26 -7
- package/.codex/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.codex/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.codex/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.codex/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.codex/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.cursor/hooks/cursor-skill-eval.js +4 -29
- package/.cursor/rules/skill-activation.mdc +1 -7
- package/.cursor/skills/code-patterns/SKILL.md +119 -0
- package/.cursor/skills/leniu-code-patterns/SKILL.md +179 -2
- package/.cursor/skills/leniu-crud-development/SKILL.md +26 -7
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +25 -16
- package/.cursor/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.cursor/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.cursor/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.cursor/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/CLAUDE.md +0 -28
- package/bin/index.js +145 -52
- package/package.json +1 -1
|
@@ -115,6 +115,7 @@ const instructions = `## 强制技能激活流程(必须执行)
|
|
|
115
115
|
- add-todo: 添加待办/TODO/任务添加
|
|
116
116
|
- update-status: 更新状态/状态变更
|
|
117
117
|
- skill-creator: 创建技能模板/技能脚手架/skill scaffold
|
|
118
|
+
- leniu-report-scenario: 报表/报表开发/报表查询/报表导出/合计行/totalLine/汇总报表/定制报表/report_order_info/金额处理/分转元/餐次/mealtime/ReportBaseTotalVO/CustomNumberConverter
|
|
118
119
|
|
|
119
120
|
### 步骤 2 - 激活(逐个调用,等待每个完成)
|
|
120
121
|
|
package/.claude/hooks/stop.js
CHANGED
|
@@ -42,8 +42,7 @@ try {
|
|
|
42
42
|
|
|
43
43
|
if (changedFiles.length > 0) {
|
|
44
44
|
console.error(`\n💡 检测到 ${changedFiles.length} 个代码文件变更,建议执行代码审查:`);
|
|
45
|
-
console.error(` → 输入 "review" 或 "审查代码" 使用 code-reviewer
|
|
46
|
-
console.error(` → 输入 "codex review" 调用 Codex 进行深度代码审查\n`);
|
|
45
|
+
console.error(` → 输入 "review" 或 "审查代码" 使用 code-reviewer 进行规范检查\n`);
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
} catch {
|
|
@@ -280,20 +280,192 @@ voucher_type_id: jsonpath "$.data.records[0].id"
|
|
|
280
280
|
|
|
281
281
|
### 第四步:生成 .hurl 文件
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
#### 4.1 查询条件类型识别与测试策略
|
|
284
284
|
|
|
285
|
+
读取 Param 类后,**逐字段识别条件类型**,按下表生成对应测试用例:
|
|
286
|
+
|
|
287
|
+
| 条件类型 | 识别特征 | 示例字段 | 必须生成的测试用例 |
|
|
288
|
+
|---------|---------|---------|----------------|
|
|
289
|
+
| **日期范围** | `startDate`/`endDate` 成对 | `startDate`, `endDate` | ① 正常区间 ② 单日(start==end) ③ 跨月区间 ④ 必填时留空(预期报错/空结果) |
|
|
290
|
+
| **关键词搜索** | `keyword`, `name`, `code` 等 | `keyword`, `canteenName` | ① 精确匹配已知值 ② 模糊匹配前缀 ③ 无匹配的乱码(验证空数组) ④ 留空(不过滤,验证正常返回) |
|
|
291
|
+
| **枚举/状态** | 字段类型为枚举或 Integer 语义明确 | `status`, `type`, `mealtype` | ① 每个有效枚举值各一条请求 ② 可选时留空(验证不过滤) |
|
|
292
|
+
| **ID / 外键** | 字段名含 `Id`,关联其他表 | `canteenId`, `areaId` | ① 传有效 ID(有数据,验证过滤生效) ② 留空(不过滤,验证正常返回) ③ 传不存在 ID(验证空结果) |
|
|
293
|
+
| **布尔开关** | boolean / Integer 表示开关 | `isSubmitted`, `isEnabled` | ① `true`/`1` ② `false`/`0` ③ 留空 |
|
|
294
|
+
| **集合/多选** | `List<Integer>`, `List<String>` | `mealtimeTypes`, `statusList` | ① 单项 ② 多项组合 ③ 空列表或留空 |
|
|
295
|
+
| **分页** | `page.current`/`page.size` | `page` | ① 第1页 size=10 ② 第2页(验证翻页) ③ size=1(验证精确分页) |
|
|
296
|
+
|
|
297
|
+
#### 4.2 各条件类型 Hurl 示例
|
|
298
|
+
|
|
299
|
+
**① 日期范围**
|
|
300
|
+
```hurl
|
|
301
|
+
# TC-DATE-1 正常日期区间
|
|
302
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
303
|
+
...
|
|
304
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31"}}
|
|
305
|
+
HTTP 200
|
|
306
|
+
[Asserts]
|
|
307
|
+
jsonpath "$.code" == 10000
|
|
308
|
+
jsonpath "$.data.resultPage.records" count > 0
|
|
309
|
+
|
|
310
|
+
# TC-DATE-2 单日查询(start == end)
|
|
311
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
312
|
+
...
|
|
313
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-15","endDate":"2024-01-15"}}
|
|
314
|
+
HTTP 200
|
|
315
|
+
[Asserts]
|
|
316
|
+
jsonpath "$.code" == 10000
|
|
317
|
+
|
|
318
|
+
# TC-DATE-3 空区间(故意用无数据的日期)
|
|
319
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
320
|
+
...
|
|
321
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2099-01-01","endDate":"2099-01-31"}}
|
|
322
|
+
HTTP 200
|
|
323
|
+
[Asserts]
|
|
324
|
+
jsonpath "$.code" == 10000
|
|
325
|
+
jsonpath "$.data.resultPage.records" count == 0
|
|
285
326
|
```
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
327
|
+
|
|
328
|
+
**② 关键词搜索**
|
|
329
|
+
```hurl
|
|
330
|
+
# TC-KW-1 精确关键词匹配
|
|
331
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
332
|
+
...
|
|
333
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31","keyword":"{{known_canteen_name}}"}}
|
|
334
|
+
HTTP 200
|
|
335
|
+
[Asserts]
|
|
336
|
+
jsonpath "$.code" == 10000
|
|
337
|
+
jsonpath "$.data.resultPage.records" count > 0
|
|
338
|
+
|
|
339
|
+
# TC-KW-2 无匹配关键词
|
|
340
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
341
|
+
...
|
|
342
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31","keyword":"__NO_MATCH_XYZ__"}}
|
|
343
|
+
HTTP 200
|
|
344
|
+
[Asserts]
|
|
345
|
+
jsonpath "$.code" == 10000
|
|
346
|
+
jsonpath "$.data.resultPage.records" count == 0
|
|
347
|
+
|
|
348
|
+
# TC-KW-3 留空 keyword(不过滤)
|
|
349
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
350
|
+
...
|
|
351
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31"}}
|
|
352
|
+
HTTP 200
|
|
353
|
+
[Asserts]
|
|
354
|
+
jsonpath "$.code" == 10000
|
|
355
|
+
jsonpath "$.data.resultPage.records" isCollection
|
|
295
356
|
```
|
|
296
357
|
|
|
358
|
+
**③ 枚举/状态**
|
|
359
|
+
```hurl
|
|
360
|
+
# TC-ENUM-1 状态=1(已提交)
|
|
361
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
362
|
+
...
|
|
363
|
+
{"content": {"page":{"current":1,"size":10},"status":1}}
|
|
364
|
+
HTTP 200
|
|
365
|
+
[Asserts]
|
|
366
|
+
jsonpath "$.code" == 10000
|
|
367
|
+
jsonpath "$.data.records[0].status" == 1
|
|
368
|
+
|
|
369
|
+
# TC-ENUM-2 状态=2(已完成)
|
|
370
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
371
|
+
...
|
|
372
|
+
{"content": {"page":{"current":1,"size":10},"status":2}}
|
|
373
|
+
HTTP 200
|
|
374
|
+
[Asserts]
|
|
375
|
+
jsonpath "$.code" == 10000
|
|
376
|
+
|
|
377
|
+
# TC-ENUM-3 不传 status(全部状态)
|
|
378
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
379
|
+
...
|
|
380
|
+
{"content": {"page":{"current":1,"size":10}}}
|
|
381
|
+
HTTP 200
|
|
382
|
+
[Asserts]
|
|
383
|
+
jsonpath "$.code" == 10000
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**④ ID / 外键**
|
|
387
|
+
```hurl
|
|
388
|
+
# TC-ID-1 传有效 canteenId(有数据,过滤生效)
|
|
389
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
390
|
+
...
|
|
391
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31","canteenId":{{canteen_id}}}}
|
|
392
|
+
HTTP 200
|
|
393
|
+
[Asserts]
|
|
394
|
+
jsonpath "$.code" == 10000
|
|
395
|
+
jsonpath "$.data.resultPage.records" count > 0
|
|
396
|
+
jsonpath "$.data.resultPage.records[0].canteenId" == {{canteen_id}}
|
|
397
|
+
|
|
398
|
+
# TC-ID-2 不传 canteenId(不过滤)
|
|
399
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
400
|
+
...
|
|
401
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31"}}
|
|
402
|
+
HTTP 200
|
|
403
|
+
[Asserts]
|
|
404
|
+
jsonpath "$.code" == 10000
|
|
405
|
+
|
|
406
|
+
# TC-ID-3 传不存在 ID(空结果)
|
|
407
|
+
POST {{base_url}}/api/v2/web/report/subject/page
|
|
408
|
+
...
|
|
409
|
+
{"content": {"page":{"current":1,"size":10},"startDate":"2024-01-01","endDate":"2024-01-31","canteenId":999999999}}
|
|
410
|
+
HTTP 200
|
|
411
|
+
[Asserts]
|
|
412
|
+
jsonpath "$.code" == 10000
|
|
413
|
+
jsonpath "$.data.resultPage.records" count == 0
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**⑤ 集合/多选**
|
|
417
|
+
```hurl
|
|
418
|
+
# TC-LIST-1 单项
|
|
419
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
420
|
+
...
|
|
421
|
+
{"content": {"page":{"current":1,"size":10},"mealtimeTypes":[1]}}
|
|
422
|
+
HTTP 200
|
|
423
|
+
[Asserts]
|
|
424
|
+
jsonpath "$.code" == 10000
|
|
425
|
+
|
|
426
|
+
# TC-LIST-2 多项组合
|
|
427
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
428
|
+
...
|
|
429
|
+
{"content": {"page":{"current":1,"size":10},"mealtimeTypes":[1,2,3]}}
|
|
430
|
+
HTTP 200
|
|
431
|
+
[Asserts]
|
|
432
|
+
jsonpath "$.code" == 10000
|
|
433
|
+
jsonpath "$.data.records" isCollection
|
|
434
|
+
|
|
435
|
+
# TC-LIST-3 空列表(不过滤)
|
|
436
|
+
POST {{base_url}}/api/v2/web/order/page
|
|
437
|
+
...
|
|
438
|
+
{"content": {"page":{"current":1,"size":10},"mealtimeTypes":[]}}
|
|
439
|
+
HTTP 200
|
|
440
|
+
[Asserts]
|
|
441
|
+
jsonpath "$.code" == 10000
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### 4.3 测试场景清单(完整版)
|
|
445
|
+
|
|
446
|
+
每个查询接口必须覆盖以下场景,按顺序在同一 `.hurl` 文件中组织:
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
必须生成(顺序执行):
|
|
450
|
+
TC-BASE 基础分页查询 — 只传必填参数,验证分页结构 + VO 所有字段存在
|
|
451
|
+
TC-TOTAL 合计行验证 — 如返回类型含 totalLine,验证所有合计字段
|
|
452
|
+
TC-DATE-* 日期条件用例 — 若有 startDate/endDate(见上方策略)
|
|
453
|
+
TC-KW-* 关键词用例 — 若有 keyword 类字段(见上方策略)
|
|
454
|
+
TC-ENUM-* 枚举用例 — 若有状态/类型枚举,每个枚举值各一条
|
|
455
|
+
TC-ID-* 外键 ID 用例 — 若有 xxxId 过滤(见上方策略)
|
|
456
|
+
TC-LIST-* 集合用例 — 若有多选字段(见上方策略)
|
|
457
|
+
TC-PAGE 翻页验证 — current=2,验证翻页正确(total > size 时才有意义)
|
|
458
|
+
TC-EMPTY 空结果验证 — 使用必然无数据的条件,验证 records count == 0
|
|
459
|
+
TC-EXPORT 导出接口 — 验证 code=10000(若有导出接口)
|
|
460
|
+
TC-UNAUTH 未授权 — 不带 X-Token,预期 HTTP 401
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
> **命名规范**:在 Hurl 文件中用注释标注场景编号,便于报告定位:
|
|
464
|
+
> ```hurl
|
|
465
|
+
> # TC-DATE-1 正常日期区间
|
|
466
|
+
> # TC-KW-2 无匹配关键词
|
|
467
|
+
> ```
|
|
468
|
+
|
|
297
469
|
**数据正确性验证**(不只是结构存在,还要验证值合理):
|
|
298
470
|
|
|
299
471
|
```hurl
|
|
@@ -101,6 +101,120 @@ feat(order): 新增订单导出功能
|
|
|
101
101
|
Closes #123
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## 数据类型规范
|
|
105
|
+
|
|
106
|
+
### 布尔语义字段必须使用 Boolean
|
|
107
|
+
|
|
108
|
+
```java
|
|
109
|
+
// ❌ 错误
|
|
110
|
+
private Integer ifNarrow;
|
|
111
|
+
private Integer isEnabled;
|
|
112
|
+
|
|
113
|
+
// ✅ 正确
|
|
114
|
+
private Boolean narrow; // getter 自动生成 isNarrow()
|
|
115
|
+
private Boolean enabled; // getter 自动生成 isEnabled()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**规则**:
|
|
119
|
+
- 语义为"是/否"的字段,类型必须为 `Boolean`
|
|
120
|
+
- 字段名不加 `if`/`is`/`has` 前缀(JavaBean 规范中 `Boolean` 的 getter 自动生成 `isXxx()`)
|
|
121
|
+
- 数据库字段使用 `TINYINT(1)` 或 `BIT(1)`
|
|
122
|
+
|
|
123
|
+
### 枚举字段必须提供明确约束
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
// ❌ 错误:调用方无法知道合法值
|
|
127
|
+
@ApiModelProperty(value = "操作类型")
|
|
128
|
+
private Integer tradeType;
|
|
129
|
+
|
|
130
|
+
// ✅ 方案一:VO/DTO 层直接用枚举(推荐)
|
|
131
|
+
@ApiModelProperty(value = "操作类型")
|
|
132
|
+
private AccTradeTypeEnum tradeType;
|
|
133
|
+
|
|
134
|
+
// ✅ 方案二:保留 Integer 但标注合法值
|
|
135
|
+
@ApiModelProperty(value = "操作类型:1-充值 2-消费 3-退款", allowableValues = "1,2,3")
|
|
136
|
+
private Integer tradeType;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 金额字段禁止使用浮点类型
|
|
140
|
+
|
|
141
|
+
```java
|
|
142
|
+
// ❌ 错误
|
|
143
|
+
private Double amount;
|
|
144
|
+
private Float price;
|
|
145
|
+
|
|
146
|
+
// ✅ 正确:Entity/Service 层用 Long(分),VO 层展示用 BigDecimal(元)
|
|
147
|
+
private Long amountFen;
|
|
148
|
+
private BigDecimal amountYuan;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 原始类型 vs 包装类型
|
|
152
|
+
|
|
153
|
+
| 场景 | 用原始类型 | 用包装类型 |
|
|
154
|
+
|------|----------|----------|
|
|
155
|
+
| Entity / VO / DTO 字段 | — | ✅ 统一用包装类型 |
|
|
156
|
+
| 方法参数(不允许 null) | ✅ `int count` | — |
|
|
157
|
+
| 方法参数(允许 null) | — | ✅ `Integer count` |
|
|
158
|
+
| 局部变量 | ✅ `int i = 0` | — |
|
|
159
|
+
|
|
160
|
+
## Optional 使用规范
|
|
161
|
+
|
|
162
|
+
```java
|
|
163
|
+
// ❌ 错误:of() 不接受 null,value 为 null 直接 NPE
|
|
164
|
+
Optional.of(value).orElse(defaultValue);
|
|
165
|
+
|
|
166
|
+
// ✅ 正确:ofNullable() 安全处理 null
|
|
167
|
+
Optional.ofNullable(value).orElse(defaultValue);
|
|
168
|
+
|
|
169
|
+
// ❌ 禁止:Optional 作为方法参数或类字段
|
|
170
|
+
public void process(Optional<String> name) { ... }
|
|
171
|
+
private Optional<String> name;
|
|
172
|
+
|
|
173
|
+
// ✅ 允许:Optional 作为方法返回值、链式处理
|
|
174
|
+
public Optional<Entity> findById(Long id) { ... }
|
|
175
|
+
Optional.ofNullable(entity).map(Entity::getConfig).orElse(DEFAULT_VALUE);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## @Transactional 规范
|
|
179
|
+
|
|
180
|
+
```java
|
|
181
|
+
// ❌ 错误:默认只回滚 RuntimeException
|
|
182
|
+
@Transactional
|
|
183
|
+
public void createOrder() { ... }
|
|
184
|
+
|
|
185
|
+
// ✅ 正确:显式指定回滚异常
|
|
186
|
+
@Transactional(rollbackFor = Exception.class)
|
|
187
|
+
public void createOrder() { ... }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- 所有 `@Transactional` 必须显式写 `rollbackFor = Exception.class`
|
|
191
|
+
- 只读查询不加 `@Transactional`(或用 `readOnly = true`)
|
|
192
|
+
- 事务方法不要 try-catch 吞掉异常,否则事务不回滚
|
|
193
|
+
|
|
194
|
+
## TODO 管理规范
|
|
195
|
+
|
|
196
|
+
```java
|
|
197
|
+
// ❌ 错误:无负责人、无日期、无跟踪
|
|
198
|
+
// TODO 修改一下
|
|
199
|
+
|
|
200
|
+
// ✅ 正确:完整 TODO 格式
|
|
201
|
+
// TODO(@陈沈杰, 2026-03-20, #TASK-1234): 移动端 AppId 赋值逻辑待产品确认
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- 每个 TODO 必须有对应的任务号
|
|
205
|
+
- 超过 2 个迭代未处理的 TODO 必须清理
|
|
206
|
+
- 不用的代码直接删除,不要注释保留
|
|
207
|
+
|
|
208
|
+
## 代码格式化规范
|
|
209
|
+
|
|
210
|
+
| 项目 | 规范 |
|
|
211
|
+
|------|------|
|
|
212
|
+
| 缩进 | 4 个空格(不用 Tab) |
|
|
213
|
+
| 行宽 | 120 字符 |
|
|
214
|
+
| 大括号 | K&R 风格(同行开始) |
|
|
215
|
+
| 空行 | 方法间 1 个空行,逻辑块间 1 个空行 |
|
|
216
|
+
| import | 分组排序:java → jakarta → org → net → com,组间空行 |
|
|
217
|
+
|
|
104
218
|
## 代码示例
|
|
105
219
|
|
|
106
220
|
### 统一响应格式
|
|
@@ -161,3 +275,8 @@ public enum OrderStatusEnum {
|
|
|
161
275
|
| Git 提交信息写"fix bug" | 写清楚修了什么:`fix(order): 修复金额计算精度丢失` |
|
|
162
276
|
| Boolean 变量名:`flag` | 有意义的名字:`isActive`, `hasPermission` |
|
|
163
277
|
| 缩写命名:`usr`, `mgr` | 完整命名:`user`, `manager` |
|
|
278
|
+
| `Optional.of(可能null值)` | `Optional.ofNullable(value)` |
|
|
279
|
+
| `@Transactional` 无 rollbackFor | `@Transactional(rollbackFor = Exception.class)` |
|
|
280
|
+
| TODO 无负责人和日期 | `// TODO(@负责人, 日期, #任务号): 描述` |
|
|
281
|
+
| 布尔字段用 `Integer` | 用 `Boolean` 类型 |
|
|
282
|
+
| 枚举字段无合法值说明 | `@ApiModelProperty` 标注合法值或直接用枚举类型 |
|
|
@@ -88,6 +88,45 @@ Grep pattern: "@Transactional\((?!.*rollbackFor)" path: [命中文件]
|
|
|
88
88
|
```
|
|
89
89
|
- ❌ `@Transactional` → ✅ `@Transactional(rollbackFor = Exception.class)`
|
|
90
90
|
|
|
91
|
+
#### 🔴 A7b. selectOne 无唯一保障
|
|
92
|
+
```bash
|
|
93
|
+
Grep pattern: "selectOne(" path: [目标目录] glob: "*.java"
|
|
94
|
+
# 命中行检查:是否有 LIMIT 1 或注释说明唯一索引
|
|
95
|
+
```
|
|
96
|
+
- ❌ 无 LIMIT 且无唯一索引 → ✅ 加 `.last("LIMIT 1")` 或确保有唯一索引
|
|
97
|
+
|
|
98
|
+
#### 🟡 A7c. selectCount 用于存在性判断
|
|
99
|
+
```bash
|
|
100
|
+
Grep pattern: "selectCount" path: [目标目录] glob: "*.java"
|
|
101
|
+
# 检查命中行是否用于 > 0 判断(存在性),而非真正计数
|
|
102
|
+
```
|
|
103
|
+
- ❌ `selectCount(w) > 0` → ✅ `mapper.exists(w)` 或 `selectList(w.last("LIMIT 1"))`
|
|
104
|
+
|
|
105
|
+
#### 🔴 A7d. Redis KEYS 命令
|
|
106
|
+
```bash
|
|
107
|
+
Grep pattern: "keysByPattern|\.keys\(" path: [目标目录] glob: "*.java"
|
|
108
|
+
```
|
|
109
|
+
- ❌ `redisTemplate.keys()` / `keysByPattern()` → ✅ 使用 Redisson `deleteByPattern()`(内部 SCAN + UNLINK)
|
|
110
|
+
|
|
111
|
+
#### 🟡 A7e. Optional.of 误用
|
|
112
|
+
```bash
|
|
113
|
+
Grep pattern: "Optional\.of\(" path: [目标目录] glob: "*.java"
|
|
114
|
+
# 排除 ofNullable 的命中
|
|
115
|
+
```
|
|
116
|
+
- ❌ `Optional.of(可能为null)` → ✅ `Optional.ofNullable(value)`
|
|
117
|
+
|
|
118
|
+
#### 🟡 A7f. 布尔字段类型错误
|
|
119
|
+
```bash
|
|
120
|
+
Grep pattern: "private Integer (if|is|has)" path: [目标目录] glob: "*.java"
|
|
121
|
+
```
|
|
122
|
+
- ❌ `private Integer isEnabled` → ✅ `private Boolean enabled`
|
|
123
|
+
|
|
124
|
+
#### 🟡 A7g. Wrapper 嵌套过深
|
|
125
|
+
```bash
|
|
126
|
+
# 人工审查:Read 文件时检查 Wrapper 嵌套是否超过 2 层
|
|
127
|
+
```
|
|
128
|
+
- ❌ Wrapper 嵌套 >2 层 → ✅ 迁移到 XML 原生 SQL
|
|
129
|
+
|
|
91
130
|
#### 🟡 A8. 请求体封装
|
|
92
131
|
```bash
|
|
93
132
|
Grep pattern: "@RequestBody [^L]" path: [目标目录] glob: "*Controller.java"
|
|
@@ -394,9 +394,186 @@ public class OrderService {
|
|
|
394
394
|
}
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
##
|
|
397
|
+
## 数据类型规范
|
|
398
|
+
|
|
399
|
+
### 布尔字段命名
|
|
400
|
+
|
|
401
|
+
```java
|
|
402
|
+
// ❌ 错误:前缀冗余、类型错误
|
|
403
|
+
private Integer ifNarrow;
|
|
404
|
+
private Integer isEnabled;
|
|
405
|
+
|
|
406
|
+
// ✅ 正确:Boolean 类型,无前缀
|
|
407
|
+
private Boolean narrow; // getter → isNarrow()
|
|
408
|
+
private Boolean enabled; // getter → isEnabled()
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 枚举字段标注
|
|
412
|
+
|
|
413
|
+
```java
|
|
414
|
+
// ❌ 错误:只靠注释 @see
|
|
415
|
+
/** @see AccTradeTypeEnum */
|
|
416
|
+
private Integer tradeType;
|
|
417
|
+
|
|
418
|
+
// ✅ 方案一:VO/DTO 用枚举类型(配合 @JsonValue)
|
|
419
|
+
private AccTradeTypeEnum tradeType;
|
|
420
|
+
|
|
421
|
+
// ✅ 方案二:@ApiModelProperty 标注合法值
|
|
422
|
+
@ApiModelProperty(value = "操作类型:1-充值 2-消费 3-退款", allowableValues = "1,2,3")
|
|
423
|
+
private Integer tradeType;
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## MyBatis-Plus 安全规范
|
|
427
|
+
|
|
428
|
+
### selectOne 必须有唯一保障
|
|
429
|
+
|
|
430
|
+
```java
|
|
431
|
+
// ❌ 危险:多条记录时抛 TooManyResultsException
|
|
432
|
+
Entity entity = mapper.selectOne(wrapper);
|
|
433
|
+
|
|
434
|
+
// ✅ 方案一:LIMIT 1
|
|
435
|
+
Entity entity = mapper.selectOne(wrapper.last("LIMIT 1"));
|
|
436
|
+
|
|
437
|
+
// ✅ 方案二:selectList 取第一条
|
|
438
|
+
List<Entity> list = mapper.selectList(wrapper);
|
|
439
|
+
Entity entity = CollUtil.isNotEmpty(list) ? list.get(0) : null;
|
|
440
|
+
|
|
441
|
+
// ✅ 方案三:确保有唯一索引(注释说明)
|
|
442
|
+
// 唯一索引:UNIQUE KEY (order_no, del_flag)
|
|
443
|
+
Entity entity = mapper.selectOne(wrapper);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### 存在性判断用 EXISTS,禁用 selectCount
|
|
447
|
+
|
|
448
|
+
```java
|
|
449
|
+
// ❌ 低效:100万行表 ~200ms
|
|
450
|
+
Long count = mapper.selectCount(wrapper);
|
|
451
|
+
if (count > 0) { ... }
|
|
452
|
+
|
|
453
|
+
// ✅ 高效:~2ms(MyBatis-Plus 3.5.4+)
|
|
454
|
+
boolean exists = mapper.exists(wrapper);
|
|
455
|
+
|
|
456
|
+
// ✅ 或 selectList + LIMIT 1
|
|
457
|
+
boolean exists = CollUtil.isNotEmpty(mapper.selectList(wrapper.last("LIMIT 1")));
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Wrapper 嵌套不超过 2 层
|
|
461
|
+
|
|
462
|
+
```java
|
|
463
|
+
// ❌ 过于复杂的 Wrapper
|
|
464
|
+
wrapper.and(w -> w.eq(A::getType, type)
|
|
465
|
+
.or(q -> q.eq(A::getType, 0))
|
|
466
|
+
.or(!PersonTypeEnum.LABOUR.getKey().equals(type),
|
|
467
|
+
q -> q.eq(A::getType, PSN_TYPE_SHARED)));
|
|
468
|
+
|
|
469
|
+
// ✅ 复杂查询写到 XML 中
|
|
470
|
+
List<Entity> list = mapper.selectByTypeCondition(type, isLabour);
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### 禁止 SELECT *
|
|
474
|
+
|
|
475
|
+
```xml
|
|
476
|
+
<!-- ❌ 禁止 -->
|
|
477
|
+
<select id="selectAll">SELECT * FROM t_order WHERE del_flag = 2</select>
|
|
478
|
+
|
|
479
|
+
<!-- ✅ 明确列出字段 -->
|
|
480
|
+
<select id="selectAll">SELECT id, order_no, amount, status, crtime FROM t_order WHERE del_flag = 2</select>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## Redis 使用规范
|
|
484
|
+
|
|
485
|
+
### 禁止 KEYS 命令
|
|
398
486
|
|
|
399
|
-
|
|
487
|
+
```java
|
|
488
|
+
// ❌ 严禁:KEYS 阻塞 Redis
|
|
489
|
+
Set<Object> keys = keysByPattern(pattern);
|
|
490
|
+
Set<Object> keys = redisTemplate.keys(pattern);
|
|
491
|
+
|
|
492
|
+
// ✅ 使用 Redisson deleteByPattern(内部 SCAN + UNLINK)
|
|
493
|
+
RedissonClient redisson = SpringUtil.getBean(RedissonClient.class);
|
|
494
|
+
redisson.getKeys().deleteByPattern(keyPattern);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Optional 使用规范
|
|
498
|
+
|
|
499
|
+
```java
|
|
500
|
+
// ❌ 错误:of() 不接受 null
|
|
501
|
+
Optional.of(value).orElse(defaultValue);
|
|
502
|
+
|
|
503
|
+
// ✅ 正确:ofNullable()
|
|
504
|
+
Optional.ofNullable(value).orElse(defaultValue);
|
|
505
|
+
|
|
506
|
+
// ✅ 链式安全转换
|
|
507
|
+
Optional.ofNullable(model.getReserveRate())
|
|
508
|
+
.map(BigDecimal::new)
|
|
509
|
+
.orElse(BigDecimal.ZERO);
|
|
510
|
+
|
|
511
|
+
// ❌ 禁止作为方法参数或类字段
|
|
512
|
+
// ✅ 允许作为方法返回值
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## @Transactional 规范
|
|
516
|
+
|
|
517
|
+
```java
|
|
518
|
+
// ❌ 默认只回滚 RuntimeException
|
|
519
|
+
@Transactional
|
|
520
|
+
public void createOrder() { ... }
|
|
521
|
+
|
|
522
|
+
// ✅ 显式指定 rollbackFor
|
|
523
|
+
@Transactional(rollbackFor = Exception.class)
|
|
524
|
+
public void createOrder() { ... }
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- 事务方法不要 try-catch 吞掉异常
|
|
528
|
+
- 只读查询不加 `@Transactional`
|
|
529
|
+
|
|
530
|
+
## 业务逻辑分层规范
|
|
531
|
+
|
|
532
|
+
```java
|
|
533
|
+
// ❌ 错误:业务判断混在数据操作中
|
|
534
|
+
public void processOrder(Long orderId) {
|
|
535
|
+
OrderInfo order = orderMapper.selectById(orderId);
|
|
536
|
+
if (order.getStatus() == 1 && order.getPayTime() != null
|
|
537
|
+
&& ChronoUnit.HOURS.between(order.getPayTime(), LocalDateTime.now()) < 24) {
|
|
538
|
+
order.setStatus(2);
|
|
539
|
+
orderMapper.updateById(order);
|
|
540
|
+
accWalletService.deduct(order.getCustId(), order.getAmount());
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ✅ 正确:分层清晰
|
|
545
|
+
public void processOrder(Long orderId) {
|
|
546
|
+
OrderInfo order = orderMapper.selectById(orderId);
|
|
547
|
+
if (ObjectUtil.isNull(order)) {
|
|
548
|
+
throw new LeException(I18n.getMessage("order_not_found"));
|
|
549
|
+
}
|
|
550
|
+
checkCanProcess(order); // 业务校验(独立方法)
|
|
551
|
+
order.markAsProcessed(); // 状态变更(Entity 方法封装)
|
|
552
|
+
orderMapper.updateById(order);
|
|
553
|
+
afterOrderProcessed(order); // 后续动作(独立方法)
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
| 层 | 职责 | 不应做的 |
|
|
558
|
+
|----|------|---------|
|
|
559
|
+
| Controller | 参数接收、格式转换 | 不含业务判断 |
|
|
560
|
+
| Business | 业务编排、跨 Service 协调 | 不直接操作 Mapper |
|
|
561
|
+
| Service | 单表 CRUD、单表事务 | 不含跨表业务逻辑 |
|
|
562
|
+
| Mapper | SQL 映射 | 不含业务逻辑 |
|
|
563
|
+
|
|
564
|
+
## TODO 管理规范
|
|
565
|
+
|
|
566
|
+
```java
|
|
567
|
+
// ❌ 错误
|
|
568
|
+
// TODO 修改一下
|
|
569
|
+
|
|
570
|
+
// ✅ 正确
|
|
571
|
+
// TODO(@陈沈杰, 2026-03-20, #TASK-1234): 移动端 AppId 赋值逻辑待产品确认
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
- 不用的代码直接删除,不要注释保留
|
|
575
|
+
|
|
576
|
+
## 通用代码规范
|
|
400
577
|
|
|
401
578
|
1. **禁止使用 `SELECT *`**:明确指定字段
|
|
402
579
|
2. **使用参数化查询**:`#{}` 而非 `${}`
|
|
@@ -39,6 +39,7 @@ description: |
|
|
|
39
39
|
| 架构 | Controller -> Business -> Service -> Mapper(四层) |
|
|
40
40
|
| 无 DAO 层 | Service 直接注入 Mapper |
|
|
41
41
|
| 对象转换 | `BeanUtil.copyProperties()` (Hutool) |
|
|
42
|
+
| Service 模式 | **两种并存**:简单 CRUD 继承 `ServiceImpl`;业务聚合直接 `@Service` |
|
|
42
43
|
| Entity 基类 | 无基类,自定义审计字段 |
|
|
43
44
|
| 请求封装 | `LeRequest<T>` |
|
|
44
45
|
| 响应封装 | `Page<T>` / `LeResponse<T>` / `void` |
|
|
@@ -110,19 +111,38 @@ private LocalDateTime uptime;
|
|
|
110
111
|
private Integer delFlag; // 1=删除, 2=正常
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
### Service
|
|
114
|
+
### Service 两种模式
|
|
114
115
|
|
|
116
|
+
项目中存在两种 Service 模式,根据业务复杂度选择:
|
|
117
|
+
|
|
118
|
+
**模式 A:简单 CRUD Service**(适用于单表操作,利用 MyBatis-Plus 内置方法)
|
|
115
119
|
```java
|
|
120
|
+
// 接口
|
|
121
|
+
public interface XxxService extends IService<XxxEntity> { }
|
|
122
|
+
|
|
123
|
+
// 实现
|
|
116
124
|
@Slf4j
|
|
117
125
|
@Service
|
|
118
|
-
public class XxxServiceImpl implements XxxService {
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity> implements XxxService {
|
|
127
|
+
// 继承 ServiceImpl 获得 save/updateById/removeById/page 等内置方法
|
|
128
|
+
// 通过 this.baseMapper 访问 Mapper(父类提供)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
121
131
|
|
|
122
|
-
|
|
132
|
+
**模式 B:业务聚合 Service**(适用于跨表操作、复杂业务编排)
|
|
133
|
+
```java
|
|
134
|
+
@Slf4j
|
|
135
|
+
@Service
|
|
136
|
+
public class XxxService {
|
|
137
|
+
@Autowired
|
|
138
|
+
private XxxMapper xxxMapper; // 直接注入 Mapper,无 DAO 层
|
|
139
|
+
@Autowired
|
|
140
|
+
private YyyMapper yyyMapper; // 可注入多个 Mapper
|
|
123
141
|
}
|
|
124
142
|
```
|
|
125
143
|
|
|
144
|
+
**选择建议**:新建简单单表 CRUD 用模式 A;涉及多表联查、报表、业务编排用模式 B。
|
|
145
|
+
|
|
126
146
|
### Controller 请求封装
|
|
127
147
|
|
|
128
148
|
```java
|
|
@@ -366,7 +386,6 @@ private String createBy; // -> crby
|
|
|
366
386
|
entity.setDelFlag(0); // -> setDelFlag(2) 表示正常
|
|
367
387
|
throw new ServiceException("..."); // -> throw new LeException("...")
|
|
368
388
|
MapstructUtils.convert(src, Dst.class); // -> BeanUtil.copyProperties(src, Dst.class)
|
|
369
|
-
extends ServiceImpl<XxxMapper, Xxx> // -> implements XxxService(不继承)
|
|
370
389
|
@Resource private XxxDao xxxDao; // -> @Resource private XxxMapper xxxMapper
|
|
371
390
|
// XML 放 resources/mapper/ // -> 与 Java 同目录
|
|
372
391
|
return null; // -> return Collections.emptyList()
|
|
@@ -377,7 +396,7 @@ return null; // -> return Collections.emptyList()
|
|
|
377
396
|
## 生成前检查清单
|
|
378
397
|
|
|
379
398
|
- [ ] 包名 `net.xnzn.core.*`
|
|
380
|
-
- [ ] Service
|
|
399
|
+
- [ ] Service 模式选择:简单 CRUD 用模式 A(继承 ServiceImpl);业务聚合用模式 B(直接 @Service)
|
|
381
400
|
- [ ] Service 直接注入 Mapper(无 DAO)
|
|
382
401
|
- [ ] 审计字段 crby/crtime/upby/uptime
|
|
383
402
|
- [ ] delFlag: 1=删除, 2=正常
|