Skip to content

实战:新增业务模块

作者: 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 自测

  1. 重启后端
  2. 打开 Swagger,先调 /auth/login 拿 Token
  3. 点右上角 Authorize,填入 Bearer {token}
  4. 测试 /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.tschildren 中追加:

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你的页面

MIT Licensed