@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.
@@ -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
+ ```