# Lyzsys 新模块开发指南

## 目录

- [1. 快速开始](#1-快速开始)
- [2. 创建后端模块](#2-创建后端模块)
- [3. 创建前端页面](#3-创建前端页面)
- [4. 配置菜单权限](#4-配置菜单权限)
- [5. 完整示例](#5-完整示例)
- [6. 常见问题](#6-常见问题)

---

## 1. 快速开始

本指南以 **lyzsys-module-demo** 项目管理模块为参考，演示如何快速创建一个新的业务模块。

### 1.1 开发流程

```
1. 设计数据库表
   ↓
2. 创建后端模块
   ├── 创建 Maven 模块
   ├── 编写实体类（DO）
   ├── 编写 Mapper 接口
   ├── 编写 VO 类
   ├── 编写 Service 层
   └── 编写 Controller 层
   ↓
3. 创建前端页面
   ├── 编写 API 接口
   ├── 编写列表页面
   └── 编写表单组件
   ↓
4. 配置菜单权限
   ├── 创建一级菜单
   ├── 创建二级菜单
   └── 创建按钮权限
   ↓
5. 测试验证
```

### 1.2 前置准备

- [ ] 确认数据库表设计
- [ ] 确认菜单结构（一级、二级菜单名称）
- [ ] 确认业务字段和验证规则
- [ ] 准备好模块名称和权限标识

---

## 2. 创建后端模块

### 2.1 创建 Maven 模块

#### 步骤 1：创建模块目录

在 `lyzsys_backend` 下创建新模块：

```
lyzsys_backend/
└── lyzsys-module-{模块名}/
    ├── pom.xml
    └── src/main/java/cn/iocoder/lyzsys/module/{模块名}/
```

#### 步骤 2：编写 pom.xml

参考 `lyzsys-module-demo/pom.xml`：

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>lyzsys</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>lyzsys-module-{模块名}</artifactId>
    <packaging>jar</packaging>

    <name>${project.artifactId}</name>
    <description>
        {模块说明}
    </description>

    <dependencies>
        <!-- 依赖 infra 模块 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-module-infra</artifactId>
            <version>${revision}</version>
        </dependency>

        <!-- 业务组件 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-biz-tenant</artifactId>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- DB 相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-mybatis</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-redis</artifactId>
        </dependency>

        <!-- Test 测试相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 工具类相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>lyzsys-spring-boot-starter-excel</artifactId>
        </dependency>
    </dependencies>

</project>
```

#### 步骤 3：添加模块到父 pom.xml

在 `lyzsys_backend/pom.xml` 中添加：

```xml
<modules>
    <!-- 已有模块 -->
    <module>lyzsys-module-system</module>
    <module>lyzsys-module-infra</module>
    <!-- 新增模块 -->
    <module>lyzsys-module-{模块名}</module>
</modules>
```

#### 步骤 4：添加模块到 server

在 `lyzsys-server/pom.xml` 中添加依赖：

```xml
<dependency>
    <groupId>cn.iocoder.boot</groupId>
    <artifactId>lyzsys-module-{模块名}</artifactId>
    <version>${revision}</version>
</dependency>
```

### 2.2 创建数据库表

参考 `sql/mysql/demo_project.sql`：

```sql
DROP TABLE IF EXISTS `{表名}`;
CREATE TABLE `{表名}` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  -- 业务字段（注意：遵循驼峰命名规范，第一个单词不能只有一个字母）
  `business_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务名称',
  `business_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务编号',
  -- 标准字段（所有表必备）
  `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '业务表';
```

### 2.3 创建实体类（DO）

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/dal/dataobject/{业务}/{业务}DO.java`

参考 `ProjectDO.java`：

```java
package cn.iocoder.lyzsys.module.{模块名}.dal.dataobject.{业务};

import cn.iocoder.lyzsys.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;

/**
 * {业务名称} DO
 *
 * @author {作者}
 */
@TableName("{表名}")
@KeySequence("{表名}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增
@Data
@EqualsAndHashCode(callSuper = true)
public class {业务}DO extends BaseDO {

    /**
     * 主键ID
     */
    @TableId
    private Long id;

    /**
     * 业务字段（注意驼峰命名）
     */
    private String businessName;
    private String businessCode;
    private LocalDateTime businessDate;

}
```

### 2.4 创建 Mapper 接口

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/dal/mysql/{业务}/{业务}Mapper.java`

参考 `ProjectMapper.java`：

```java
package cn.iocoder.lyzsys.module.{模块名}.dal.mysql.{业务};

import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}PageReqVO;
import cn.iocoder.lyzsys.module.{模块名}.dal.dataobject.{业务}.{业务}DO;
import org.apache.ibatis.annotations.Mapper;

/**
 * {业务名称} Mapper
 *
 * @author {作者}
 */
@Mapper
public interface {业务}Mapper extends BaseMapperX<{业务}DO> {

    default PageResult<{业务}DO> selectPage({业务}PageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<{业务}DO>()
                .likeIfPresent({业务}DO::getBusinessName, reqVO.getBusinessName())
                .likeIfPresent({业务}DO::getBusinessCode, reqVO.getBusinessCode())
                .betweenIfPresent({业务}DO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc({业务}DO::getId));
    }

    default {业务}DO selectByBusinessCode(String businessCode) {
        return selectOne({业务}DO::getBusinessCode, businessCode);
    }

}
```

### 2.5 创建 VO 类

#### 分页查询请求 VO

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/controller/admin/{业务}/vo/{业务}PageReqVO.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo;

import cn.iocoder.lyzsys.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDateTime;

import static cn.iocoder.lyzsys.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;

@Schema(description = "管理后台 - {业务名称}分页列表 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class {业务}PageReqVO extends PageParam {

    @Schema(description = "业务名称，模糊匹配", example = "示例")
    private String businessName;

    @Schema(description = "业务编号，模糊匹配", example = "CODE-001")
    private String businessCode;

    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;

}
```

#### 响应 VO

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/controller/admin/{业务}/vo/{业务}RespVO.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo;

import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

@Schema(description = "管理后台 - {业务名称}信息 Response VO")
@Data
@ExcelIgnoreUnannotated
public class {业务}RespVO {

    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty("ID")
    private Long id;

    @Schema(description = "业务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例")
    @ExcelProperty("业务名称")
    private String businessName;

    @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CODE-001")
    @ExcelProperty("业务编号")
    private String businessCode;

    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;

}
```

#### 保存请求 VO

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/controller/admin/{业务}/vo/{业务}SaveReqVO.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;

@Schema(description = "管理后台 - {业务名称}创建/修改 Request VO")
@Data
public class {业务}SaveReqVO {

    @Schema(description = "主键ID", example = "1")
    private Long id;

    @Schema(description = "业务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例")
    @NotBlank(message = "业务名称不能为空")
    @Size(max = 100, message = "业务名称长度不能超过100个字符")
    private String businessName;

    @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CODE-001")
    @NotBlank(message = "业务编号不能为空")
    @Size(max = 50, message = "业务编号长度不能超过50个字符")
    private String businessCode;

}
```

### 2.6 创建 Service 层

#### Service 接口

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/service/{业务}/{业务}Service.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.service.{业务};

import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}PageReqVO;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}SaveReqVO;
import cn.iocoder.lyzsys.module.{模块名}.dal.dataobject.{业务}.{业务}DO;

import java.util.List;

/**
 * {业务名称} Service 接口
 *
 * @author {作者}
 */
public interface {业务}Service {

    /**
     * 创建{业务}
     */
    Long create{业务}({业务}SaveReqVO createReqVO);

    /**
     * 更新{业务}
     */
    void update{业务}({业务}SaveReqVO updateReqVO);

    /**
     * 删除{业务}
     */
    void delete{业务}(Long id);

    /**
     * 批量删除{业务}
     */
    void delete{业务}List(List<Long> ids);

    /**
     * 获得{业务}分页列表
     */
    PageResult<{业务}DO> get{业务}Page({业务}PageReqVO pageReqVO);

    /**
     * 获得{业务}详情
     */
    {业务}DO get{业务}(Long id);

}
```

#### Service 实现类

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/service/{业务}/{业务}ServiceImpl.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.service.{业务};

import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}PageReqVO;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}SaveReqVO;
import cn.iocoder.lyzsys.module.{模块名}.dal.dataobject.{业务}.{业务}DO;
import cn.iocoder.lyzsys.module.{模块名}.dal.mysql.{业务}.{业务}Mapper;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.util.List;

import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.lyzsys.module.{模块名}.enums.ErrorCodeConstants.*;

/**
 * {业务名称} Service 实现类
 *
 * @author {作者}
 */
@Service
@Validated
public class {业务}ServiceImpl implements {业务}Service {

    @Resource
    private {业务}Mapper {业务}Mapper;

    @Override
    public Long create{业务}({业务}SaveReqVO createReqVO) {
        // 校验业务编号的唯一性
        validateBusinessCodeUnique(null, createReqVO.getBusinessCode());

        // 插入
        {业务}DO {业务} = BeanUtils.toBean(createReqVO, {业务}DO.class);
        {业务}Mapper.insert({业务});
        return {业务}.getId();
    }

    @Override
    public void update{业务}({业务}SaveReqVO updateReqVO) {
        // 校验存在
        validate{业务}Exists(updateReqVO.getId());
        // 校验业务编号的唯一性
        validateBusinessCodeUnique(updateReqVO.getId(), updateReqVO.getBusinessCode());

        // 更新
        {业务}DO updateObj = BeanUtils.toBean(updateReqVO, {业务}DO.class);
        {业务}Mapper.updateById(updateObj);
    }

    @Override
    public void delete{业务}(Long id) {
        // 校验存在
        validate{业务}Exists(id);
        // 删除
        {业务}Mapper.deleteById(id);
    }

    @Override
    public void delete{业务}List(List<Long> ids) {
        // 校验存在
        ids.forEach(this::validate{业务}Exists);
        // 批量删除
        {业务}Mapper.deleteBatchIds(ids);
    }

    @Override
    public PageResult<{业务}DO> get{业务}Page({业务}PageReqVO pageReqVO) {
        return {业务}Mapper.selectPage(pageReqVO);
    }

    @Override
    public {业务}DO get{业务}(Long id) {
        return {业务}Mapper.selectById(id);
    }

    // ==================== 校验方法 ====================

    private void validate{业务}Exists(Long id) {
        if ({业务}Mapper.selectById(id) == null) {
            throw exception({业务大写}_NOT_EXISTS);
        }
    }

    private void validateBusinessCodeUnique(Long id, String businessCode) {
        {业务}DO {业务} = {业务}Mapper.selectByBusinessCode(businessCode);
        if ({业务} == null) {
            return;
        }
        if (id == null) {
            throw exception({业务大写}_CODE_DUPLICATE);
        }
        if (!{业务}.getId().equals(id)) {
            throw exception({业务大写}_CODE_DUPLICATE);
        }
    }

}
```

### 2.7 创建 Controller 层

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/controller/admin/{业务}/{业务}Controller.java`

参考 `ProjectController.java`：

```java
package cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务};

import cn.iocoder.lyzsys.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
import cn.iocoder.lyzsys.framework.common.pojo.PageParam;
import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.framework.excel.core.util.ExcelUtils;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}PageReqVO;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}RespVO;
import cn.iocoder.lyzsys.module.{模块名}.controller.admin.{业务}.vo.{业务}SaveReqVO;
import cn.iocoder.lyzsys.module.{模块名}.dal.dataobject.{业务}.{业务}DO;
import cn.iocoder.lyzsys.module.{模块名}.service.{业务}.{业务}Service;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;

import static cn.iocoder.lyzsys.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;

@Tag(name = "管理后台 - {业务名称}管理")
@RestController
@RequestMapping("/{模块名}/{业务}")
@Validated
public class {业务}Controller {

    @Resource
    private {业务}Service {业务}Service;

    @PostMapping("/create")
    @Operation(summary = "创建{业务}")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:create')")
    public CommonResult<Long> create{业务}(@Valid @RequestBody {业务}SaveReqVO createReqVO) {
        Long id = {业务}Service.create{业务}(createReqVO);
        return success(id);
    }

    @PutMapping("/update")
    @Operation(summary = "修改{业务}")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:update')")
    public CommonResult<Boolean> update{业务}(@Valid @RequestBody {业务}SaveReqVO updateReqVO) {
        {业务}Service.update{业务}(updateReqVO);
        return success(true);
    }

    @DeleteMapping("/delete")
    @Operation(summary = "删除{业务}")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:delete')")
    public CommonResult<Boolean> delete{业务}(@RequestParam("id") Long id) {
        {业务}Service.delete{业务}(id);
        return success(true);
    }

    @DeleteMapping("/delete-list")
    @Operation(summary = "批量删除{业务}")
    @Parameter(name = "ids", description = "编号列表", required = true)
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:delete')")
    public CommonResult<Boolean> delete{业务}List(@RequestParam("ids") List<Long> ids) {
        {业务}Service.delete{业务}List(ids);
        return success(true);
    }

    @GetMapping("/page")
    @Operation(summary = "获得{业务}分页列表")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:query')")
    public CommonResult<PageResult<{业务}RespVO>> get{业务}Page(@Valid {业务}PageReqVO pageReqVO) {
        PageResult<{业务}DO> pageResult = {业务}Service.get{业务}Page(pageReqVO);
        return success(BeanUtils.toBean(pageResult, {业务}RespVO.class));
    }

    @GetMapping("/get")
    @Operation(summary = "获得{业务}详情")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:query')")
    public CommonResult<{业务}RespVO> get{业务}(@RequestParam("id") Long id) {
        {业务}DO {业务} = {业务}Service.get{业务}(id);
        return success(BeanUtils.toBean({业务}, {业务}RespVO.class));
    }

    @GetMapping("/export-excel")
    @Operation(summary = "导出{业务}")
    @PreAuthorize("@ss.hasPermission('{模块名}:{业务}:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void export{业务}(HttpServletResponse response, @Valid {业务}PageReqVO exportReqVO) throws IOException {
        exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<{业务}DO> list = {业务}Service.get{业务}Page(exportReqVO).getList();
        ExcelUtils.write(response, "{业务名称}.xls", "数据", {业务}RespVO.class,
                BeanUtils.toBean(list, {业务}RespVO.class));
    }

}
```

### 2.8 创建错误码常量

文件位置：`src/main/java/cn/iocoder/lyzsys/module/{模块名}/enums/ErrorCodeConstants.java`

```java
package cn.iocoder.lyzsys.module.{模块名}.enums;

import cn.iocoder.lyzsys.framework.common.exception.ErrorCode;

/**
 * {模块名称} 错误码枚举类
 *
 * {模块名} 模块，使用 1-0XX-000-000 段
 */
public interface ErrorCodeConstants {

    // ========== {业务名称}模块 1-0XX-001-000 ==========
    ErrorCode {业务大写}_NOT_EXISTS = new ErrorCode(1_0XX_001_000, "{业务}不存在");
    ErrorCode {业务大写}_CODE_DUPLICATE = new ErrorCode(1_0XX_001_001, "已经存在该{业务}编号");

}
```

---

## 3. 创建前端页面

### 3.1 创建 API 接口

文件位置：`src/api/{模块名}/{业务}/index.ts`

参考 `src/api/demo/project/index.ts`：

```typescript
import request from '@/config/axios'

export interface {业务}VO {
  id: number
  businessName: string
  businessCode: string
  createTime: Date
}

export interface {业务}PageReqVO extends PageParam {
  businessName?: string
  businessCode?: string
  createTime?: Date[]
}

// 查询分页
export const get{业务}Page = (params: {业务}PageReqVO) => {
  return request.get({ url: '/{模块名}/{业务}/page', params })
}

// 查询详情
export const get{业务} = (id: number) => {
  return request.get({ url: '/{模块名}/{业务}/get', params: { id } })
}

// 新增
export const create{业务} = (data: {业务}VO) => {
  return request.post({ url: '/{模块名}/{业务}/create', data })
}

// 修改
export const update{业务} = (data: {业务}VO) => {
  return request.put({ url: '/{模块名}/{业务}/update', data })
}

// 删除
export const delete{业务} = (id: number) => {
  return request.delete({ url: '/{模块名}/{业务}/delete', params: { id } })
}

// 批量删除
export const delete{业务}List = (ids: number[]) => {
  return request.delete({ url: '/{模块名}/{业务}/delete-list', params: { ids } })
}

// 导出
export const export{业务} = (params) => {
  return request.download({ url: '/{模块名}/{业务}/export-excel', params })
}
```

### 3.2 创建列表页面

文件位置：`src/views/{模块名}/{业务}/index.vue`

参考 `src/views/demo/project/index.vue`（详细代码见 demo 模块）

**关键点**：
- 使用 `defineOptions({ name: '{模块}{业务}' })`
- 搜索表单 + 列表表格 + 分页组件
- 权限控制：`v-hasPermi="['{模块名}:{业务}:操作']"`

### 3.3 创建表单组件

文件位置：`src/views/{模块名}/{业务}/{业务}Form.vue`

参考 `src/views/demo/project/ProjectForm.vue`（详细代码见 demo 模块）

**关键点**：
- 使用 `defineOptions({ name: '{模块}{业务}Form' })`
- 使用 `defineExpose({ open })` 暴露打开方法
- 表单验证规则
- 提交成功后 `emit('success')`

---

## 4. 配置菜单权限

### 4.1 创建菜单 SQL

文件位置：`sql/mysql/{模块名}_menu.sql`

参考 `sql/mysql/demo_menu.sql`：

```sql
-- 一级菜单
INSERT INTO `system_menu` (...) VALUES (
  {ID}, '{一级菜单名}', '', 1, {排序}, 0,
  '/{模块名}', '{图标}', NULL, NULL,
  0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
);

-- 二级菜单
INSERT INTO `system_menu` (...) VALUES (
  {ID+1}, '{二级菜单名}', '', 2, 1, {一级菜单ID},
  '{业务}', '{图标}', '{模块名}/{业务}/index', '{模块}{业务}',
  0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
);

-- 按钮权限：查询、新增、修改、删除、导出
INSERT INTO `system_menu` (...) VALUES
  ({ID+2}, '{业务}查询', '{模块名}:{业务}:query', 3, 1, {二级菜单ID}, ...), ({ID+3}, '{业务}新增', '{模块名}:{业务}:create', 3, 2, {二级菜单ID}, ...),
  ({ID+4}, '{业务}修改', '{模块名}:{业务}:update', 3, 3, {二级菜单ID}, ...),
  ({ID+5}, '{业务}删除', '{模块名}:{业务}:delete', 3, 4, {二级菜单ID}, ...),
  ({ID+6}, '{业务}导出', '{模块名}:{业务}:export', 3, 5, {二级菜单ID}, ...);

-- 调整系统管理和基础设施菜单排序
UPDATE `system_menu` SET `sort` = 98, `updater` = '1', `update_time` = NOW() WHERE `id` = 1 AND `deleted` = b'0';
UPDATE `system_menu` SET `sort` = 99, `updater` = '1', `update_time` = NOW() WHERE `id` = 2 AND `deleted` = b'0';
```

---

## 5. 完整示例

### 5.1 Demo 模块文件清单

以下是 `lyzsys-module-demo` 的完整文件结构，可作为新模块开发的参考：

```
lyzsys-module-demo/
├── pom.xml
└── src/main/java/cn/iocoder/lyzsys/module/demo/
    ├── controller/admin/project/
    │   ├── ProjectController.java
    │   └── vo/
    │       ├── ProjectPageReqVO.java
    │       ├── ProjectRespVO.java
    │       └── ProjectSaveReqVO.java
    ├── service/project/
    │   ├── ProjectService.java
    │   └── ProjectServiceImpl.java
    ├── dal/
    │   ├── dataobject/project/
    │   │   └── ProjectDO.java
    │   └── mysql/project/
    │       └── ProjectMapper.java
    └── enums/
        └── ErrorCodeConstants.java
```

### 5.2 前端文件清单

```
src/
├── api/demo/project/
│   └── index.ts
└── views/demo/project/
    ├── index.vue
    └── ProjectForm.vue
```

### 5.3 SQL 文件清单

```
sql/mysql/
├── demo_project.sql      # 数据库表
└── demo_menu.sql         # 菜单配置
```

---

## 6. 常见问题

### 6.1 Maven 编译错误

**问题**：找不到父 POM
**解决**：检查 parent 的 groupId、artifactId、version 是否正确

### 6.2 菜单不显示

**问题**：前端菜单不显示
**解决**：
1. 确认已执行菜单 SQL
2. 检查当前用户角色权限
3. 清除浏览器缓存

### 6.3 接口 404

**问题**：前端调用接口返回 404
**解决**：
1. 检查 Controller 的 @RequestMapping 路径
2. 确认模块已添加到 lyzsys-server/pom.xml
3. 重启后端服务

### 6.4 MyBatis Plus 字段映射错误

**问题**：字段映射错误，查询结果为 null
**解决**：检查字段命名是否遵循驼峰规范，确保第一个单词不是单字母

### 6.5 权限验证失败

**问题**：操作提示无权限
**解决**：
1. 检查 @PreAuthorize 权限标识是否正确
2. 确认菜单 SQL 中的权限标识与代码一致
3. 给当前角色分配权限

---

## 7. 检查清单

开发完成后，请检查：

**后端**
- [ ] Maven 模块配置正确
- [ ] 数据库表创建成功
- [ ] 实体类字段遵循命名规范
- [ ] Mapper 接口方法正确
- [ ] VO 类字段完整
- [ ] Service 业务逻辑正确
- [ ] Controller 权限配置正确
- [ ] 错误码定义完整

**前端**
- [ ] API 接口定义正确
- [ ] 列表页面功能完整
- [ ] 表单组件验证规则正确
- [ ] 权限控制配置正确

**菜单**
- [ ] 一级菜单创建成功
- [ ] 二级菜单创建成功
- [ ] 按钮权限创建成功
- [ ] 菜单排序正确

**测试**
- [ ] 列表查询正常
- [ ] 新增功能正常
- [ ] 修改功能正常
- [ ] 删除功能正常
- [ ] 导出功能正常
- [ ] 权限控制正常

---

**文档版本**：v1.0
**参考模块**：lyzsys-module-demo
**最后更新**：2024年
**维护者**：开发团队
