@nebula-skills/nebula-code-standards 0.1.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.
- package/README.md +45 -0
- package/bin/cli.cjs +50 -0
- package/package.json +45 -0
- package/scripts/postinstall.cjs +18 -0
- package/skill/SKILL.md +133 -0
- package/skill/backend-standards.md +611 -0
- package/skill/boot-components-catalog.md +546 -0
- package/skill/db-standards.md +232 -0
- package/skill/framework-standards.md +610 -0
- package/skill/frontend-standards.md +310 -0
- package/skill/full-crud-example.md +608 -0
- package/skill/init-new-project.md +842 -0
- package/skill/microapp-guide.md +510 -0
- package/skill/pitfalls-checklist.md +188 -0
- package/skill/rpc-api-reference.md +313 -0
- package/skill/upgrade-decision.md +151 -0
- package/skill/workspace-overview.md +194 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# 完整 CRUD 代码示例
|
|
2
|
+
|
|
3
|
+
> 以 `t_demo` 表(示例管理)为蓝本,展示 Nebula 框架标准 CRUD 完整实现。
|
|
4
|
+
> 新建业务模块时,将 `Demo`/`demo` 替换为实际业务名即可。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 后端完整实现
|
|
9
|
+
|
|
10
|
+
### 1. 数据库建表脚本(Flyway)
|
|
11
|
+
|
|
12
|
+
文件:`nebula-{domain}-server/src/main/resources/db/migration/V1.0.0__init_t_demo.sql`
|
|
13
|
+
|
|
14
|
+
```sql
|
|
15
|
+
CREATE TABLE IF NOT EXISTS `t_demo`
|
|
16
|
+
(
|
|
17
|
+
`id` BIGINT NOT NULL COMMENT '主键(雪花算法)',
|
|
18
|
+
`tenant_id` BIGINT DEFAULT NULL COMMENT '租户ID',
|
|
19
|
+
`create_user_id` BIGINT DEFAULT NULL COMMENT '创建人ID',
|
|
20
|
+
`creator` VARCHAR(100) DEFAULT NULL COMMENT '创建人用户名',
|
|
21
|
+
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
|
|
22
|
+
`modify_user_id` BIGINT DEFAULT NULL COMMENT '最后修改人ID',
|
|
23
|
+
`updater` VARCHAR(100) DEFAULT NULL COMMENT '最后修改人用户名',
|
|
24
|
+
`modify_time` DATETIME DEFAULT NULL COMMENT '最后修改时间',
|
|
25
|
+
`delete_flag` INT NOT NULL DEFAULT 0 COMMENT '逻辑删除(0:未删除 1:已删除)',
|
|
26
|
+
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
|
27
|
+
`custom_fields` TEXT DEFAULT NULL COMMENT '自定义扩展字段(JSON)',
|
|
28
|
+
`demo_name` VARCHAR(100) NOT NULL COMMENT '示例名称',
|
|
29
|
+
`demo_code` VARCHAR(50) NOT NULL COMMENT '示例编码(租户内唯一)',
|
|
30
|
+
`status` INT NOT NULL DEFAULT 0 COMMENT '状态(0:正常 1:禁用)',
|
|
31
|
+
PRIMARY KEY (`id`),
|
|
32
|
+
UNIQUE KEY `uk_tenant_demo_code` (`tenant_id`, `demo_code`),
|
|
33
|
+
KEY `idx_demo_name` (`demo_name`),
|
|
34
|
+
KEY `idx_status` (`status`)
|
|
35
|
+
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='示例表(nebula CRUD 演示)';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### 2. 实体类(DemoDO)
|
|
41
|
+
|
|
42
|
+
```java
|
|
43
|
+
package com.huida.{domain}.{domain}.demo.entity;
|
|
44
|
+
|
|
45
|
+
@Data
|
|
46
|
+
@EqualsAndHashCode(callSuper = true)
|
|
47
|
+
@TableName("t_demo")
|
|
48
|
+
@Schema(description = "示例实体")
|
|
49
|
+
public class DemoDO extends BaseEntity {
|
|
50
|
+
|
|
51
|
+
@Schema(description = "示例名称")
|
|
52
|
+
private String demoName;
|
|
53
|
+
|
|
54
|
+
@Schema(description = "示例编码(租户内唯一)")
|
|
55
|
+
private String demoCode;
|
|
56
|
+
|
|
57
|
+
@Schema(description = "状态(0:正常 1:禁用)")
|
|
58
|
+
private Integer status;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### 3. VO 类
|
|
65
|
+
|
|
66
|
+
**分页入参(DemoPageParamVO):**
|
|
67
|
+
|
|
68
|
+
```java
|
|
69
|
+
package com.huida.{domain}.{domain}.demo.vo.param;
|
|
70
|
+
|
|
71
|
+
@Data
|
|
72
|
+
@EqualsAndHashCode(callSuper = true)
|
|
73
|
+
@Schema(description = "示例分页查询参数")
|
|
74
|
+
public class DemoPageParamVO extends PageParam {
|
|
75
|
+
|
|
76
|
+
@Schema(description = "示例名称(支持模糊匹配)")
|
|
77
|
+
private String demoName;
|
|
78
|
+
|
|
79
|
+
@Schema(description = "状态(0:正常 1:禁用)")
|
|
80
|
+
private Integer status;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**新增/修改入参(DemoSaveParamVO):**
|
|
85
|
+
|
|
86
|
+
```java
|
|
87
|
+
package com.huida.{domain}.{domain}.demo.vo.param;
|
|
88
|
+
|
|
89
|
+
@Data
|
|
90
|
+
@Schema(description = "示例新增/修改参数")
|
|
91
|
+
public class DemoSaveParamVO implements Serializable {
|
|
92
|
+
|
|
93
|
+
public interface UpdateGroup {}
|
|
94
|
+
|
|
95
|
+
@NotNull(groups = UpdateGroup.class, message = "修改时主键 id 不能为空")
|
|
96
|
+
@Schema(description = "主键(新增时不传,修改时必填)")
|
|
97
|
+
private Long id;
|
|
98
|
+
|
|
99
|
+
@NotBlank(message = "示例名称不能为空")
|
|
100
|
+
@Size(max = 100, message = "示例名称长度不能超过 100 个字符")
|
|
101
|
+
@Schema(description = "示例名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
|
102
|
+
private String demoName;
|
|
103
|
+
|
|
104
|
+
@NotBlank(message = "示例编码不能为空")
|
|
105
|
+
@Size(max = 50, message = "示例编码长度不能超过 50 个字符")
|
|
106
|
+
@Schema(description = "示例编码", requiredMode = Schema.RequiredMode.REQUIRED)
|
|
107
|
+
private String demoCode;
|
|
108
|
+
|
|
109
|
+
@Schema(description = "备注")
|
|
110
|
+
private String remark;
|
|
111
|
+
|
|
112
|
+
public boolean isNew() { return id == null; }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**列表响应(DemoRespVO):**
|
|
117
|
+
|
|
118
|
+
```java
|
|
119
|
+
package com.huida.{domain}.{domain}.demo.vo.resp;
|
|
120
|
+
|
|
121
|
+
@Data
|
|
122
|
+
@Schema(description = "示例列表项")
|
|
123
|
+
public class DemoRespVO implements Serializable {
|
|
124
|
+
@Schema(description = "主键ID") private Long id;
|
|
125
|
+
@Schema(description = "示例名称") private String demoName;
|
|
126
|
+
@Schema(description = "示例编码") private String demoCode;
|
|
127
|
+
@Schema(description = "状态") private Integer status;
|
|
128
|
+
@Schema(description = "创建人") private String creator;
|
|
129
|
+
@Schema(description = "创建时间") private LocalDateTime createTime;
|
|
130
|
+
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**详情响应(DemoDetailVO):**
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
package com.huida.{domain}.{domain}.demo.vo.resp;
|
|
138
|
+
|
|
139
|
+
@Data
|
|
140
|
+
@Schema(description = "示例详情")
|
|
141
|
+
public class DemoDetailVO implements Serializable {
|
|
142
|
+
@Schema(description = "主键ID") private Long id;
|
|
143
|
+
@Schema(description = "示例名称") private String demoName;
|
|
144
|
+
@Schema(description = "示例编码") private String demoCode;
|
|
145
|
+
@Schema(description = "状态") private Integer status;
|
|
146
|
+
@Schema(description = "备注") private String remark;
|
|
147
|
+
@Schema(description = "自定义扩展字段") private String customFields;
|
|
148
|
+
@Schema(description = "创建人") private String creator;
|
|
149
|
+
@Schema(description = "创建时间") private LocalDateTime createTime;
|
|
150
|
+
@Schema(description = "创建人ID") private Long createUserId;
|
|
151
|
+
@Schema(description = "最后修改人用户名") private String updater;
|
|
152
|
+
@Schema(description = "最后修改人ID") private Long modifyUserId;
|
|
153
|
+
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 4. 对象转换(DemoConvert)
|
|
160
|
+
|
|
161
|
+
```java
|
|
162
|
+
package com.huida.{domain}.{domain}.demo.convert;
|
|
163
|
+
|
|
164
|
+
@Mapper(config = BaseMapperConfig.class)
|
|
165
|
+
public interface DemoConvert {
|
|
166
|
+
|
|
167
|
+
DemoConvert INSTANCE = Mappers.getMapper(DemoConvert.class);
|
|
168
|
+
|
|
169
|
+
DemoRespVO doToRespVO(DemoDO demoDO);
|
|
170
|
+
DemoDetailVO doToDetailVO(DemoDO demoDO);
|
|
171
|
+
DemoDO saveParamToDo(DemoSaveParamVO saveParam);
|
|
172
|
+
void saveParamMergeToDo(DemoSaveParamVO saveParam, @MappingTarget DemoDO demoDO);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### 5. Mapper 接口(DemoMapper)
|
|
179
|
+
|
|
180
|
+
```java
|
|
181
|
+
package com.huida.{domain}.{domain}.demo.mapper;
|
|
182
|
+
|
|
183
|
+
@Mapper
|
|
184
|
+
public interface DemoMapper extends BaseMapper<DemoDO> {
|
|
185
|
+
// BaseMapper 已提供 save / removeById / updateById / selectById / selectPage 等
|
|
186
|
+
// 如需固定 SQL 查询,在此添加带 @Select 注解的方法
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 6. 动态查询扩展(DemoMapperExt)
|
|
193
|
+
|
|
194
|
+
```java
|
|
195
|
+
package com.huida.{domain}.{domain}.demo.mapper;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 示例动态查询扩展
|
|
199
|
+
*
|
|
200
|
+
* <p>分页采用「先 count,再 fetch」两段式,count=0 时短路,避免无效的 LIMIT 查询。
|
|
201
|
+
*/
|
|
202
|
+
@Repository
|
|
203
|
+
@RequiredArgsConstructor
|
|
204
|
+
public class DemoMapperExt {
|
|
205
|
+
|
|
206
|
+
private final DemoMapper demoMapper;
|
|
207
|
+
|
|
208
|
+
public PageResult<DemoDO> pageQuery(DemoPageParamVO query) {
|
|
209
|
+
LambdaQueryWrapper<DemoDO> wrapper = buildPageWrapper(query);
|
|
210
|
+
|
|
211
|
+
Long count = demoMapper.selectCount(wrapper);
|
|
212
|
+
if (count == null || count == 0) {
|
|
213
|
+
return PageResult.empty(query);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Page<DemoDO> mpPage = new Page<>(query.getPageNo(), query.getPageSize(), false);
|
|
217
|
+
applySort(mpPage, query);
|
|
218
|
+
Page<DemoDO> result = demoMapper.selectPage(mpPage, wrapper);
|
|
219
|
+
return PageResult.of(result.getRecords(), count, query);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public boolean existsByCode(String demoCode, Long excludeId) {
|
|
223
|
+
return demoMapper.selectCount(
|
|
224
|
+
new LambdaQueryWrapper<DemoDO>()
|
|
225
|
+
.eq(DemoDO::getDemoCode, demoCode)
|
|
226
|
+
.ne(excludeId != null, DemoDO::getId, excludeId)
|
|
227
|
+
) > 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private LambdaQueryWrapper<DemoDO> buildPageWrapper(DemoPageParamVO query) {
|
|
231
|
+
return new LambdaQueryWrapper<DemoDO>()
|
|
232
|
+
.like(StringUtils.hasText(query.getDemoName()), DemoDO::getDemoName, query.getDemoName())
|
|
233
|
+
.eq(query.getStatus() != null, DemoDO::getStatus, query.getStatus())
|
|
234
|
+
.orderByDesc(DemoDO::getCreateTime);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private void applySort(Page<DemoDO> mpPage, DemoPageParamVO query) {
|
|
238
|
+
if (!query.hasSorts()) return;
|
|
239
|
+
for (SortItem sortItem : query.getSorts()) {
|
|
240
|
+
String col = camelToSnake(sortItem.getColumn());
|
|
241
|
+
mpPage.addOrder(sortItem.isAscending() ? OrderItem.asc(col) : OrderItem.desc(col));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private static String camelToSnake(String camel) {
|
|
246
|
+
StringBuilder sb = new StringBuilder();
|
|
247
|
+
for (int i = 0; i < camel.length(); i++) {
|
|
248
|
+
char c = camel.charAt(i);
|
|
249
|
+
if (Character.isUpperCase(c) && i > 0) sb.append('_');
|
|
250
|
+
sb.append(Character.toLowerCase(c));
|
|
251
|
+
}
|
|
252
|
+
return sb.toString();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### 7. Service 接口(IDemoService)
|
|
260
|
+
|
|
261
|
+
```java
|
|
262
|
+
package com.huida.{domain}.{domain}.demo.service;
|
|
263
|
+
|
|
264
|
+
public interface IDemoService extends BaseService<DemoDO> {
|
|
265
|
+
|
|
266
|
+
PageResult<DemoRespVO> pageDemo(DemoPageParamVO query);
|
|
267
|
+
|
|
268
|
+
DemoDetailVO getDemoById(Long id);
|
|
269
|
+
|
|
270
|
+
Long createDemo(DemoSaveParamVO saveParam);
|
|
271
|
+
|
|
272
|
+
void updateDemo(DemoSaveParamVO saveParam);
|
|
273
|
+
|
|
274
|
+
void deleteDemo(List<Long> ids);
|
|
275
|
+
|
|
276
|
+
void enableDemo(Long id);
|
|
277
|
+
|
|
278
|
+
void disableDemo(Long id);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### 8. Service 实现(DemoServiceImpl)
|
|
285
|
+
|
|
286
|
+
```java
|
|
287
|
+
package com.huida.{domain}.{domain}.demo.service.impl;
|
|
288
|
+
|
|
289
|
+
@Slf4j
|
|
290
|
+
@Service
|
|
291
|
+
@RequiredArgsConstructor
|
|
292
|
+
public class DemoServiceImpl extends BaseServiceImpl<DemoMapper, DemoDO>
|
|
293
|
+
implements IDemoService {
|
|
294
|
+
|
|
295
|
+
private final DemoMapperExt demoMapperExt;
|
|
296
|
+
|
|
297
|
+
@Override
|
|
298
|
+
public PageResult<DemoRespVO> pageDemo(DemoPageParamVO query) {
|
|
299
|
+
PageResult<DemoDO> pageResult = demoMapperExt.pageQuery(query);
|
|
300
|
+
return pageResult.convert(DemoConvert.INSTANCE::doToRespVO);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@Override
|
|
304
|
+
public DemoDetailVO getDemoById(Long id) {
|
|
305
|
+
DemoDO demoDO = getByIdOrThrow(id);
|
|
306
|
+
return DemoConvert.INSTANCE.doToDetailVO(demoDO);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Override
|
|
310
|
+
@Transactional(rollbackFor = Exception.class)
|
|
311
|
+
public Long createDemo(DemoSaveParamVO saveParam) {
|
|
312
|
+
checkDemoCodeUnique(saveParam.getDemoCode(), null);
|
|
313
|
+
DemoDO demoDO = DemoConvert.INSTANCE.saveParamToDo(saveParam);
|
|
314
|
+
save(demoDO);
|
|
315
|
+
log.info("新增示例成功,id={},code={}", demoDO.getId(), demoDO.getDemoCode());
|
|
316
|
+
return demoDO.getId();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@Override
|
|
320
|
+
@Transactional(rollbackFor = Exception.class)
|
|
321
|
+
public void updateDemo(DemoSaveParamVO saveParam) {
|
|
322
|
+
DemoDO demoDO = getByIdOrThrow(saveParam.getId());
|
|
323
|
+
checkDemoCodeUnique(saveParam.getDemoCode(), saveParam.getId());
|
|
324
|
+
DemoConvert.INSTANCE.saveParamMergeToDo(saveParam, demoDO);
|
|
325
|
+
updateById(demoDO);
|
|
326
|
+
log.info("修改示例成功,id={}", saveParam.getId());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@Override
|
|
330
|
+
@Transactional(rollbackFor = Exception.class)
|
|
331
|
+
public void deleteDemo(List<Long> ids) {
|
|
332
|
+
if (ids == null || ids.isEmpty()) return;
|
|
333
|
+
removeByIds(ids);
|
|
334
|
+
log.info("批量逻辑删除示例成功,ids={}", ids);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@Override
|
|
338
|
+
@Transactional(rollbackFor = Exception.class)
|
|
339
|
+
public void enableDemo(Long id) {
|
|
340
|
+
getByIdOrThrow(id);
|
|
341
|
+
DemoDO update = new DemoDO();
|
|
342
|
+
update.setId(id);
|
|
343
|
+
update.setStatus(0);
|
|
344
|
+
updateById(update);
|
|
345
|
+
log.info("启用示例成功,id={}", id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@Override
|
|
349
|
+
@Transactional(rollbackFor = Exception.class)
|
|
350
|
+
public void disableDemo(Long id) {
|
|
351
|
+
getByIdOrThrow(id);
|
|
352
|
+
DemoDO update = new DemoDO();
|
|
353
|
+
update.setId(id);
|
|
354
|
+
update.setStatus(1);
|
|
355
|
+
updateById(update);
|
|
356
|
+
log.info("禁用示例成功,id={}", id);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 唯一性校验(修改时 excludeId 传入自身 id) */
|
|
360
|
+
private void checkDemoCodeUnique(String demoCode, Long excludeId) {
|
|
361
|
+
if (demoMapperExt.existsByCode(demoCode, excludeId)) {
|
|
362
|
+
throw new BusinessException(GlobalErrorCode.PARAM_INVALID.getCode(),
|
|
363
|
+
"示例编码 [" + demoCode + "] 已存在,请更换编码");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### 9. Controller(DemoController)
|
|
372
|
+
|
|
373
|
+
```java
|
|
374
|
+
package com.huida.{domain}.{domain}.demo.controller;
|
|
375
|
+
|
|
376
|
+
@Tag(name = "示例管理", description = "t_demo 增删改查演示接口")
|
|
377
|
+
@Validated
|
|
378
|
+
@RestController
|
|
379
|
+
@RequestMapping("/{domain}/demo")
|
|
380
|
+
@RequiredArgsConstructor
|
|
381
|
+
public class DemoController {
|
|
382
|
+
|
|
383
|
+
private final IDemoService demoService;
|
|
384
|
+
|
|
385
|
+
@Operation(summary = "分页查询示例列表")
|
|
386
|
+
@PostMapping("/page")
|
|
387
|
+
public R<PageResult<DemoRespVO>> pageDemo(@RequestBody DemoPageParamVO query) {
|
|
388
|
+
return R.ok(demoService.pageDemo(query));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@Operation(summary = "查询示例详情")
|
|
392
|
+
@Parameter(name = "id", description = "示例主键ID", required = true)
|
|
393
|
+
@GetMapping("/{id}")
|
|
394
|
+
public R<DemoDetailVO> getDemoById(@PathVariable Long id) {
|
|
395
|
+
return R.ok(demoService.getDemoById(id));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@Operation(summary = "新增示例")
|
|
399
|
+
@PostMapping
|
|
400
|
+
public R<Long> createDemo(@Valid @RequestBody DemoSaveParamVO saveParam) {
|
|
401
|
+
return R.ok(demoService.createDemo(saveParam));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@Operation(summary = "修改示例")
|
|
405
|
+
@PutMapping
|
|
406
|
+
public R<Void> updateDemo(
|
|
407
|
+
@Validated(DemoSaveParamVO.UpdateGroup.class) @RequestBody DemoSaveParamVO saveParam) {
|
|
408
|
+
demoService.updateDemo(saveParam);
|
|
409
|
+
return R.ok();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@Operation(summary = "批量逻辑删除示例")
|
|
413
|
+
@DeleteMapping
|
|
414
|
+
public R<Void> deleteDemo(@RequestBody List<Long> ids) {
|
|
415
|
+
demoService.deleteDemo(ids);
|
|
416
|
+
return R.ok();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@Operation(summary = "启用示例")
|
|
420
|
+
@Parameter(name = "id", description = "示例主键ID", required = true)
|
|
421
|
+
@PatchMapping("/{id}/v")
|
|
422
|
+
public R<Void> enableDemo(@PathVariable Long id) {
|
|
423
|
+
demoService.enableDemo(id);
|
|
424
|
+
return R.ok();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@Operation(summary = "禁用示例")
|
|
428
|
+
@Parameter(name = "id", description = "示例主键ID", required = true)
|
|
429
|
+
@PatchMapping("/{id}/x")
|
|
430
|
+
public R<Void> disableDemo(@PathVariable Long id) {
|
|
431
|
+
demoService.disableDemo(id);
|
|
432
|
+
return R.ok();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 前端完整实现
|
|
440
|
+
|
|
441
|
+
### 1. TypeScript 类型定义
|
|
442
|
+
|
|
443
|
+
文件:`src/api/types/demo.ts`(或直接写在 `src/api/{domain}.ts` 顶部)
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// 分页入参
|
|
447
|
+
export interface DemoPageParam {
|
|
448
|
+
pageNo?: number
|
|
449
|
+
pageSize?: number
|
|
450
|
+
demoName?: string
|
|
451
|
+
status?: number
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 新增/修改入参
|
|
455
|
+
export interface DemoSaveParam {
|
|
456
|
+
id?: number // 新增不传,修改必传
|
|
457
|
+
demoName: string
|
|
458
|
+
demoCode: string
|
|
459
|
+
remark?: string
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 列表项
|
|
463
|
+
export interface DemoRespVO {
|
|
464
|
+
id: number
|
|
465
|
+
demoName: string
|
|
466
|
+
demoCode: string
|
|
467
|
+
status: number // 0: 正常, 1: 禁用
|
|
468
|
+
creator: string
|
|
469
|
+
createTime: string
|
|
470
|
+
modifyTime: string
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 详情
|
|
474
|
+
export interface DemoDetailVO extends DemoRespVO {
|
|
475
|
+
remark?: string
|
|
476
|
+
customFields?: string
|
|
477
|
+
createUserId?: number
|
|
478
|
+
updater?: string
|
|
479
|
+
modifyUserId?: number
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 2. API 函数
|
|
484
|
+
|
|
485
|
+
文件:`src/api/{domain}.ts`
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { {domain}Request } from './index'
|
|
489
|
+
import type { PageResult } from '@nebula-web/types'
|
|
490
|
+
import type { DemoPageParam, DemoSaveParam, DemoRespVO, DemoDetailVO } from './types/demo'
|
|
491
|
+
|
|
492
|
+
// 分页查询
|
|
493
|
+
export function getDemoPage(data: DemoPageParam): Promise<PageResult<DemoRespVO>> {
|
|
494
|
+
return {domain}Request.post('/{domain}/demo/page', data)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 查询详情
|
|
498
|
+
export function getDemoById(id: number): Promise<DemoDetailVO> {
|
|
499
|
+
return {domain}Request.get(`/{domain}/demo/${id}`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 新增(返回新 id)
|
|
503
|
+
export function createDemo(data: DemoSaveParam): Promise<number> {
|
|
504
|
+
return {domain}Request.post('/{domain}/demo', data)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 修改
|
|
508
|
+
export function updateDemo(data: DemoSaveParam): Promise<void> {
|
|
509
|
+
return {domain}Request.put('/{domain}/demo', data)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 批量删除
|
|
513
|
+
export function deleteDemo(ids: number[]): Promise<void> {
|
|
514
|
+
return {domain}Request.delete('/{domain}/demo', { data: ids })
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 启用
|
|
518
|
+
export function enableDemo(id: number): Promise<void> {
|
|
519
|
+
return {domain}Request.patch(`/{domain}/demo/${id}/v`)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 禁用
|
|
523
|
+
export function disableDemo(id: number): Promise<void> {
|
|
524
|
+
return {domain}Request.patch(`/{domain}/demo/${id}/x`)
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### 3. 路由注册
|
|
529
|
+
|
|
530
|
+
文件:`src/router/routes.ts`
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
{
|
|
534
|
+
path: '/demo',
|
|
535
|
+
name: 'Demo',
|
|
536
|
+
component: () => import('@/pages/demo/index.vue'),
|
|
537
|
+
meta: { title: '示例管理' },
|
|
538
|
+
},
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### 4. 页面组件使用示例
|
|
542
|
+
|
|
543
|
+
文件:`src/pages/demo/index.vue`
|
|
544
|
+
|
|
545
|
+
```vue
|
|
546
|
+
<script setup lang="ts">
|
|
547
|
+
import { ref, onMounted } from 'vue'
|
|
548
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
549
|
+
import { getDemoPage, deleteDemo, enableDemo, disableDemo } from '@/api/{domain}'
|
|
550
|
+
import type { DemoPageParam, DemoRespVO } from '@/api/types/demo'
|
|
551
|
+
import type { PageResult } from '@nebula-web/types'
|
|
552
|
+
|
|
553
|
+
// 查询参数
|
|
554
|
+
const queryForm = ref<DemoPageParam>({ pageNo: 1, pageSize: 10 })
|
|
555
|
+
|
|
556
|
+
// 分页数据
|
|
557
|
+
const pageData = ref<PageResult<DemoRespVO>>({
|
|
558
|
+
records: [],
|
|
559
|
+
total: 0,
|
|
560
|
+
pageNo: 1,
|
|
561
|
+
pageSize: 10,
|
|
562
|
+
pages: 0,
|
|
563
|
+
})
|
|
564
|
+
const loading = ref(false)
|
|
565
|
+
|
|
566
|
+
// 加载分页数据
|
|
567
|
+
async function loadPage() {
|
|
568
|
+
loading.value = true
|
|
569
|
+
try {
|
|
570
|
+
pageData.value = await getDemoPage(queryForm.value)
|
|
571
|
+
} catch (err: any) {
|
|
572
|
+
ElMessage.error(err.message)
|
|
573
|
+
} finally {
|
|
574
|
+
loading.value = false
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 删除
|
|
579
|
+
async function handleDelete(ids: number[]) {
|
|
580
|
+
await ElMessageBox.confirm('确认删除所选数据?', '提示', { type: 'warning' })
|
|
581
|
+
await deleteDemo(ids)
|
|
582
|
+
ElMessage.success('删除成功')
|
|
583
|
+
loadPage()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
onMounted(() => loadPage())
|
|
587
|
+
</script>
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## 菜单初始化 SQL
|
|
593
|
+
|
|
594
|
+
文件:`nebula-system-server/src/main/resources/db/migration/V1.x.x__init_{domain}_demo_menu.sql`
|
|
595
|
+
|
|
596
|
+
```sql
|
|
597
|
+
-- 示例功能菜单
|
|
598
|
+
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, path, sort_order, visible, status)
|
|
599
|
+
VALUES (40001, 40000, 'C', '示例管理', '{domain}:demo:query', '/{domain}app/demo', 1, 1, 1);
|
|
600
|
+
|
|
601
|
+
-- 权限按钮
|
|
602
|
+
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
|
|
603
|
+
VALUES (40010, 40001, 'F', '新增', '{domain}:demo:create', 1);
|
|
604
|
+
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
|
|
605
|
+
VALUES (40011, 40001, 'F', '修改', '{domain}:demo:update', 2);
|
|
606
|
+
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
|
|
607
|
+
VALUES (40012, 40001, 'F', '删除', '{domain}:demo:delete', 3);
|
|
608
|
+
```
|