外观
完整 CRUD 代码示例
2026-04-01
以
t_demo表(示例管理)为蓝本,展示 Nebula 框架标准 CRUD 完整实现。
新建业务模块时,将Demo/demo替换为实际业务名即可快速搭建骨架。
目录
一、后端完整实现
1.1 数据库建表脚本(Flyway)
文件:{项目}-server/src/main/resources/db/migration/V1.0.0__init_t_demo.sql
CREATE TABLE IF NOT EXISTS `t_demo`
(
`id` BIGINT NOT NULL COMMENT '主键(雪花算法)',
`tenant_id` BIGINT DEFAULT NULL COMMENT '租户ID',
`create_user_id` BIGINT DEFAULT NULL COMMENT '创建人ID',
`creator` VARCHAR(128) DEFAULT NULL COMMENT '创建人用户名',
`create_time` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`modify_user_id` BIGINT DEFAULT NULL COMMENT '最后修改人ID',
`updater` VARCHAR(128) DEFAULT NULL COMMENT '最后修改人用户名',
`modify_time` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '最后修改时间',
`delete_flag` INT NOT NULL DEFAULT 0 COMMENT '逻辑删除(0:未删除 1:已删除)',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
`custom_fields` TEXT DEFAULT NULL COMMENT '自定义扩展字段(JSON)',
`demo_name` VARCHAR(100) NOT NULL COMMENT '示例名称',
`demo_code` VARCHAR(50) NOT NULL COMMENT '示例编码(租户内唯一)',
`status` INT NOT NULL DEFAULT 0 COMMENT '状态(0:正常 1:禁用)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_demo_code` (`tenant_id`, `demo_code`),
KEY `idx_demo_name` (`demo_name`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='示例表(nebula CRUD 演示)';1.2 实体类(DemoDO)
package com.huida.nebula.ltc.demo.entity;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_demo")
@Schema(description = "示例实体")
public class DemoDO extends BaseEntity {
@Schema(description = "示例名称")
private String demoName;
@Schema(description = "示例编码(租户内唯一)")
private String demoCode;
@Schema(description = "状态(0:正常 1:禁用)")
private Integer status;
}1.3 VO 类
分页入参(DemoPageParamVO):
package com.huida.nebula.ltc.demo.vo.param;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "示例分页查询参数")
public class DemoPageParamVO extends PageParam {
@Schema(description = "示例名称(支持模糊匹配)")
private String demoName;
@Schema(description = "状态(0:正常 1:禁用)")
private Integer status;
}新增/修改入参(DemoSaveParamVO):
package com.huida.nebula.ltc.demo.vo.param;
@Data
@Schema(description = "示例新增/修改参数")
public class DemoSaveParamVO implements Serializable {
public interface UpdateGroup {}
@NotNull(groups = UpdateGroup.class, message = "修改时主键 id 不能为空")
@Schema(description = "主键(新增时不传,修改时必填)")
private Long id;
@NotBlank(message = "示例名称不能为空")
@Size(max = 100, message = "示例名称长度不能超过 100 个字符")
@Schema(description = "示例名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String demoName;
@NotBlank(message = "示例编码不能为空")
@Size(max = 50, message = "示例编码长度不能超过 50 个字符")
@Schema(description = "示例编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String demoCode;
@Schema(description = "备注")
private String remark;
public boolean isNew() { return id == null; }
}列表响应(DemoRespVO):
package com.huida.nebula.ltc.demo.vo.resp;
@Data
@Schema(description = "示例列表项")
public class DemoRespVO implements Serializable {
@Schema(description = "主键ID") private Long id;
@Schema(description = "示例名称") private String demoName;
@Schema(description = "示例编码") private String demoCode;
@Schema(description = "状态") private Integer status;
@Schema(description = "创建人") private String creator;
@Schema(description = "创建时间") private LocalDateTime createTime;
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
}详情响应(DemoDetailVO):
package com.huida.nebula.ltc.demo.vo.resp;
@Data
@Schema(description = "示例详情")
public class DemoDetailVO implements Serializable {
@Schema(description = "主键ID") private Long id;
@Schema(description = "示例名称") private String demoName;
@Schema(description = "示例编码") private String demoCode;
@Schema(description = "状态") private Integer status;
@Schema(description = "备注") private String remark;
@Schema(description = "自定义扩展字段") private String customFields;
@Schema(description = "创建人") private String creator;
@Schema(description = "创建时间") private LocalDateTime createTime;
@Schema(description = "创建人ID") private Long createUserId;
@Schema(description = "最后修改人用户名") private String updater;
@Schema(description = "最后修改人ID") private Long modifyUserId;
@Schema(description = "最后修改时间") private LocalDateTime modifyTime;
}1.4 对象转换(DemoConvert)
package com.huida.nebula.ltc.demo.convert;
@Mapper(config = BaseMapperConfig.class)
public interface DemoConvert {
DemoConvert INSTANCE = Mappers.getMapper(DemoConvert.class);
DemoRespVO doToRespVO(DemoDO demoDO);
DemoDetailVO doToDetailVO(DemoDO demoDO);
DemoDO saveParamToDo(DemoSaveParamVO saveParam);
void saveParamMergeToDo(DemoSaveParamVO saveParam, @MappingTarget DemoDO demoDO);
}1.5 Mapper 接口(DemoMapper)
package com.huida.nebula.ltc.demo.mapper;
@Mapper
public interface DemoMapper extends BaseMapper<DemoDO> {
// BaseMapper 已提供 save / removeById / updateById / selectById / selectPage 等
}1.6 动态查询扩展(DemoMapperExt)
package com.huida.nebula.ltc.demo.mapper;
/**
* 示例动态查询扩展
*
* <p>分页采用「先 count,再 fetch」两段式,count=0 时短路,避免无效的 LIMIT 查询。
*/
@Repository
@RequiredArgsConstructor
public class DemoMapperExt {
private final DemoMapper demoMapper;
public PageResult<DemoDO> pageQuery(DemoPageParamVO query) {
LambdaQueryWrapper<DemoDO> wrapper = buildPageWrapper(query);
Long count = demoMapper.selectCount(wrapper);
if (count == null || count == 0) {
return PageResult.empty(query);
}
Page<DemoDO> mpPage = new Page<>(query.getPageNo(), query.getPageSize(), false);
applySort(mpPage, query);
Page<DemoDO> result = demoMapper.selectPage(mpPage, wrapper);
return PageResult.of(result.getRecords(), count, query);
}
public boolean existsByCode(String demoCode, Long excludeId) {
return demoMapper.selectCount(
new LambdaQueryWrapper<DemoDO>()
.eq(DemoDO::getDemoCode, demoCode)
.ne(excludeId != null, DemoDO::getId, excludeId)
) > 0;
}
private LambdaQueryWrapper<DemoDO> buildPageWrapper(DemoPageParamVO query) {
return new LambdaQueryWrapper<DemoDO>()
.like(StringUtils.hasText(query.getDemoName()), DemoDO::getDemoName, query.getDemoName())
.eq(query.getStatus() != null, DemoDO::getStatus, query.getStatus())
.orderByDesc(DemoDO::getCreateTime);
}
private void applySort(Page<DemoDO> mpPage, DemoPageParamVO query) {
if (!query.hasSorts()) return;
for (SortItem sortItem : query.getSorts()) {
String col = camelToSnake(sortItem.getColumn());
mpPage.addOrder(sortItem.isAscending() ? OrderItem.asc(col) : OrderItem.desc(col));
}
}
private static String camelToSnake(String camel) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < camel.length(); i++) {
char c = camel.charAt(i);
if (Character.isUpperCase(c) && i > 0) sb.append('_');
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
}1.7 Service 接口(IDemoService)
package com.huida.nebula.ltc.demo.service;
public interface IDemoService extends BaseService<DemoDO> {
PageResult<DemoRespVO> pageDemo(DemoPageParamVO query);
DemoDetailVO getDemoById(Long id);
Long createDemo(DemoSaveParamVO saveParam);
void updateDemo(DemoSaveParamVO saveParam);
void deleteDemo(List<Long> ids);
void enableDemo(Long id);
void disableDemo(Long id);
}1.8 Service 实现(DemoServiceImpl)
package com.huida.nebula.ltc.demo.service.impl;
@Slf4j
@Service
@RequiredArgsConstructor
public class DemoServiceImpl extends BaseServiceImpl<DemoMapper, DemoDO>
implements IDemoService {
private final DemoMapperExt demoMapperExt;
@Override
public PageResult<DemoRespVO> pageDemo(DemoPageParamVO query) {
PageResult<DemoDO> pageResult = demoMapperExt.pageQuery(query);
return pageResult.convert(DemoConvert.INSTANCE::doToRespVO);
}
@Override
public DemoDetailVO getDemoById(Long id) {
DemoDO demoDO = getByIdOrThrow(id);
return DemoConvert.INSTANCE.doToDetailVO(demoDO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDemo(DemoSaveParamVO saveParam) {
checkDemoCodeUnique(saveParam.getDemoCode(), null);
DemoDO demoDO = DemoConvert.INSTANCE.saveParamToDo(saveParam);
save(demoDO);
log.info("新增示例成功,id={},code={}", demoDO.getId(), demoDO.getDemoCode());
return demoDO.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateDemo(DemoSaveParamVO saveParam) {
DemoDO demoDO = getByIdOrThrow(saveParam.getId());
checkDemoCodeUnique(saveParam.getDemoCode(), saveParam.getId());
DemoConvert.INSTANCE.saveParamMergeToDo(saveParam, demoDO);
updateById(demoDO);
log.info("修改示例成功,id={}", saveParam.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDemo(List<Long> ids) {
if (ids == null || ids.isEmpty()) return;
removeByIds(ids);
log.info("批量逻辑删除示例成功,ids={}", ids);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void enableDemo(Long id) {
getByIdOrThrow(id);
DemoDO update = new DemoDO();
update.setId(id);
update.setStatus(0);
updateById(update);
log.info("启用示例成功,id={}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void disableDemo(Long id) {
getByIdOrThrow(id);
DemoDO update = new DemoDO();
update.setId(id);
update.setStatus(1);
updateById(update);
log.info("禁用示例成功,id={}", id);
}
/** 唯一性校验(修改时 excludeId 传入自身 id) */
private void checkDemoCodeUnique(String demoCode, Long excludeId) {
if (demoMapperExt.existsByCode(demoCode, excludeId)) {
throw new BusinessException(GlobalErrorCode.PARAM_INVALID.getCode(),
"示例编码 [" + demoCode + "] 已存在,请更换编码");
}
}
}1.9 Controller(DemoController)
package com.huida.nebula.ltc.demo.controller;
@Tag(name = "示例管理", description = "t_demo 增删改查演示接口")
@Validated
@RestController
@RequestMapping("/ltc/demo")
@RequiredArgsConstructor
public class DemoController {
private final IDemoService demoService;
@Operation(summary = "分页查询示例列表")
@PostMapping("/page")
public R<PageResult<DemoRespVO>> pageDemo(@RequestBody DemoPageParamVO query) {
return R.ok(demoService.pageDemo(query));
}
@Operation(summary = "查询示例详情")
@Parameter(name = "id", description = "示例主键ID", required = true)
@GetMapping("/{id}")
public R<DemoDetailVO> getDemoById(@PathVariable Long id) {
return R.ok(demoService.getDemoById(id));
}
@Operation(summary = "新增示例")
@PostMapping
public R<Long> createDemo(@Valid @RequestBody DemoSaveParamVO saveParam) {
return R.ok(demoService.createDemo(saveParam));
}
@Operation(summary = "修改示例")
@PutMapping
public R<Void> updateDemo(
@Validated(DemoSaveParamVO.UpdateGroup.class) @RequestBody DemoSaveParamVO saveParam) {
demoService.updateDemo(saveParam);
return R.ok();
}
@Operation(summary = "批量逻辑删除示例")
@DeleteMapping
public R<Void> deleteDemo(@RequestBody List<Long> ids) {
demoService.deleteDemo(ids);
return R.ok();
}
@Operation(summary = "启用示例")
@Parameter(name = "id", description = "示例主键ID", required = true)
@PatchMapping("/{id}/v")
public R<Void> enableDemo(@PathVariable Long id) {
demoService.enableDemo(id);
return R.ok();
}
@Operation(summary = "禁用示例")
@Parameter(name = "id", description = "示例主键ID", required = true)
@PatchMapping("/{id}/x")
public R<Void> disableDemo(@PathVariable Long id) {
demoService.disableDemo(id);
return R.ok();
}
}二、前端完整实现
2.1 TypeScript 类型定义
文件:src/api/types/demo.ts
// 分页入参
export interface DemoPageParam {
pageNo?: number
pageSize?: number
demoName?: string
status?: number
}
// 新增/修改入参
export interface DemoSaveParam {
id?: number // 新增不传,修改必传
demoName: string
demoCode: string
remark?: string
}
// 列表项
export interface DemoRespVO {
id: number
demoName: string
demoCode: string
status: number // 0: 正常, 1: 禁用
creator: string
createTime: string
modifyTime: string
}
// 详情
export interface DemoDetailVO extends DemoRespVO {
remark?: string
customFields?: string
createUserId?: number
updater?: string
modifyUserId?: number
}2.2 API 函数
文件:src/api/ltc.ts
import { ltcRequest } from './index'
import type { PageResult } from '@nebula-web/types'
import type { DemoPageParam, DemoSaveParam, DemoRespVO, DemoDetailVO } from './types/demo'
export function getDemoPage(data: DemoPageParam): Promise<PageResult<DemoRespVO>> {
return ltcRequest.post('/ltc/demo/page', data)
}
export function getDemoById(id: number): Promise<DemoDetailVO> {
return ltcRequest.get(`/ltc/demo/${id}`)
}
export function createDemo(data: DemoSaveParam): Promise<number> {
return ltcRequest.post('/ltc/demo', data)
}
export function updateDemo(data: DemoSaveParam): Promise<void> {
return ltcRequest.put('/ltc/demo', data)
}
export function deleteDemo(ids: number[]): Promise<void> {
return ltcRequest.delete('/ltc/demo', { data: ids })
}
export function enableDemo(id: number): Promise<void> {
return ltcRequest.patch(`/ltc/demo/${id}/v`)
}
export function disableDemo(id: number): Promise<void> {
return ltcRequest.patch(`/ltc/demo/${id}/x`)
}2.3 路由注册
文件:src/router/routes.ts
{
path: '/demo',
name: 'Demo',
component: () => import('@/pages/demo/index.vue'),
meta: { title: '示例管理' },
},2.4 页面组件使用示例
文件:src/pages/demo/index.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDemoPage, deleteDemo, enableDemo, disableDemo } from '@/api/ltc'
import type { DemoPageParam, DemoRespVO } from '@/api/types/demo'
import type { PageResult } from '@nebula-web/types'
const queryForm = ref<DemoPageParam>({ pageNo: 1, pageSize: 10 })
const pageData = ref<PageResult<DemoRespVO>>({
records: [],
total: 0,
pageNo: 1,
pageSize: 10,
pages: 0,
})
const loading = ref(false)
async function loadPage() {
loading.value = true
try {
pageData.value = await getDemoPage(queryForm.value)
} catch (err: any) {
ElMessage.error(err.message)
} finally {
loading.value = false
}
}
async function handleDelete(ids: number[]) {
await ElMessageBox.confirm('确认删除所选数据?', '提示', { type: 'warning' })
await deleteDemo(ids)
ElMessage.success('删除成功')
loadPage()
}
onMounted(() => loadPage())
</script>三、菜单初始化 SQL
文件:nebula-system-server/src/main/resources/db/migration/V1.x.x__init_ltc_demo_menu.sql
-- 示例功能菜单
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, path, sort_order, visible, status)
VALUES (40001, 40000, 'C', '示例管理', 'ltc:demo:query', '/ltcapp/demo', 1, 1, 1);
-- 权限按钮
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
VALUES (40010, 40001, 'F', '新增', 'ltc:demo:create', 1);
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
VALUES (40011, 40001, 'F', '修改', 'ltc:demo:update', 2);
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, sort_order)
VALUES (40012, 40001, 'F', '删除', 'ltc:demo:delete', 3);