实战:新增业务模块
作者: luote (luote) · 个人主页 luote996.cn
下面以「通知 notice」为例,演示从 0 到 1 新增一个完整业务模块。你可以照着做一遍,再换成自己的业务名。
目标
实现通知的增删改查:
- 后端接口:
/notices - 前端页面:
/notice列表页
第一步:建表
在 data.sql 末尾追加(或直接在 MySQL 执行):
sql
CREATE TABLE notices (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
title VARCHAR(128) NOT NULL COMMENT '标题',
content TEXT COMMENT '内容',
user_id BIGINT NOT NULL COMMENT '创建人ID',
deleted TINYINT DEFAULT 0 COMMENT '软删除:0否 1是',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) COMMENT '通知表';第二步:后端 Domain
po(对应数据库表)
domain/po/Notice.java:
java
@Data
@TableName("notices")
public class Notice {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String content;
private Long userId;
@TableLogic
private Integer deleted;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}dto(接收前端参数)
domain/dto/NoticeDTO.java:
java
@Data
public class NoticeDTO {
private Long id;
@NotBlank(message = "标题不能为空")
@Size(max = 128)
private String title;
private String content;
}vo(返回给前端)
domain/vo/NoticeVO.java:
java
@Data
public class NoticeVO {
private Long id;
private String title;
private String content;
private LocalDateTime createTime;
}第三步:Mapper
mapper/NoticeMapper.java:
java
@Mapper
public interface NoticeMapper extends BaseMapper<Notice> {
}项目已配置 @MapperScan("cn.luote.mapper"),新建 Mapper 无需额外注册。
第四步:Service
接口 service/NoticeService.java:
java
public interface NoticeService extends IService<Notice> {
IPage<NoticeVO> pageNotices(long page, long size);
void addNotice(NoticeDTO dto);
void updateNotice(NoticeDTO dto);
void deleteNotice(Long id);
}实现类 service/impl/NoticeServiceImpl.java 核心逻辑:
java
@Service
@RequiredArgsConstructor
public class NoticeServiceImpl extends ServiceImpl<NoticeMapper, Notice> implements NoticeService {
@Override
public IPage<NoticeVO> pageNotices(long page, long size) {
Page<Notice> pageParam = new Page<>(page, size);
IPage<Notice> result = page(pageParam, new LambdaQueryWrapper<Notice>()
.orderByDesc(Notice::getCreateTime));
return result.convert(this::toVo);
}
@Override
public void addNotice(NoticeDTO dto) {
Notice notice = new Notice();
notice.setTitle(dto.getTitle());
notice.setContent(dto.getContent());
notice.setUserId(UserContext.getUserId());
save(notice);
}
private NoticeVO toVo(Notice notice) {
NoticeVO vo = new NoticeVO();
BeanUtil.copyProperties(notice, vo);
return vo;
}
}第五步:Controller
controller/NoticeController.java:
java
@Tag(name = "通知管理")
@RestController
@RequestMapping("/notices")
@RequiredArgsConstructor
@Validated
@SecurityRequirement(name = "Bearer")
public class NoticeController {
private final NoticeService noticeService;
@Operation(summary = "分页查询通知")
@GetMapping
public Result<IPage<NoticeVO>> page(
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) long size) {
return Result.ok(noticeService.pageNotices(page, size));
}
@Operation(summary = "新增通知")
@PostMapping
public Result<Void> add(@Valid @RequestBody NoticeDTO dto) {
noticeService.addNotice(dto);
return Result.ok();
}
@Operation(summary = "删除通知")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
noticeService.deleteNotice(id);
return Result.ok();
}
}第六步:Swagger 自测
- 重启后端
- 打开 Swagger,先调
/auth/login拿 Token - 点右上角 Authorize,填入
Bearer {token} - 测试
/notices各接口,确认返回code: 200
后端没问题后再写前端。
第七步:前端 API
api/types.ts 追加类型:
typescript
export interface NoticeVO {
id: number
title: string
content: string
createTime?: string
}api/notice.ts:
typescript
import request from './request'
import type { NoticeVO, PageResult } from './types'
export function getNoticePage(page: number, size: number) {
return request.get<PageResult<NoticeVO>>('/notices', { params: { page, size } })
}
export function addNotice(data: { title: string; content?: string }) {
return request.post<void>('/notices', data)
}
export function deleteNotice(id: number) {
return request.delete<void>(`/notices/${id}`)
}第八步:前端页面
view/notice/index.vue 最简列表页:
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getNoticePage, addNotice, deleteNotice } from '@/api/notice'
import type { NoticeVO } from '@/api/types'
const list = ref<NoticeVO[]>([])
const title = ref('')
async function loadData() {
const result = await getNoticePage(1, 10)
list.value = result.records
}
async function handleAdd() {
if (!title.value) return
await addNotice({ title: title.value })
ElMessage.success('新增成功')
title.value = ''
loadData()
}
onMounted(loadData)
</script>
<template>
<el-card shadow="never">
<el-input v-model="title" placeholder="通知标题" style="width: 240px; margin-right: 12px" />
<el-button type="primary" @click="handleAdd">新增</el-button>
<el-table :data="list" style="margin-top: 16px">
<el-table-column prop="title" label="标题" />
<el-table-column prop="createTime" label="创建时间" />
</el-table>
</el-card>
</template>第九步:注册路由
router/index.ts 的 children 中追加:
typescript
{
path: 'notice',
name: 'Notice',
component: () => import('@/view/notice/index.vue')
}访问 http://localhost:5173/notice 即可看到页面。
第十步:联调检查清单
- 后端 Swagger 接口全部 200
- 前端 Network 请求状态 200
- 401 时是否跳转登录页
- 新增后列表是否刷新
- 控制台无报错
复制模板速查
新增模块时,直接复制「用户模块」改名字最快:
| 复制来源 | 改成 |
|---|---|
User.java | 你的 po |
UserDTO.java | 你的 dto |
UserVO.java | 你的 vo |
UserMapper.java | 你的 Mapper |
UserService + UserServiceImpl | 你的 Service |
UserController.java | 你的 Controller |
api/user.ts | 你的 api 文件 |
view/home/index.vue | 你的页面 |