Skip to content

Code Organization Guidelines

The directory and Java package structure of module A shall adhere to the following structure:

hei-modules-a
└── src
    └── main
        └── java
            └── io.github.jiangbyte
                ├── config
                │   ├── xxxx
                │   └── xxx
                ├── utils
                │   ├── xxxx
                │   └── xxx
                ├── ...
                │   ├── xxxx
                │   └── xxx
                └── modules
                    ├─── {module-name}
                    │   ├── controller
                    │   ├── entity
                    │   ├── convert
                    │   ├── mapper
                    │   │   └── xml
                    │   ├── param
                    │   ├── provider
                    │   └── service
                    │       └── impl
                    ├─── {module-name}  == If a module has submodules, they can be merged and placed together, for example:
                    │   ├── controller
                    │   │   ├── {submodule-class-1}Controller.java
                    │   │   └── {submodule-class-2}Controller.java
                    │   ├── entity
                    │   │   ├── {submodule-class-1}.java
                    │   │   └── {submodule-class-2}.java
                    │   ├── convert
                    │   │   ├── {submodule-class-1}Convert.java
                    │   │   └── {submodule-class-2}Convert.java
                    │   ├── mapper
                    │   │   ├── {submodule-class-1}Mapper.java
                    │   │   ├── {submodule-class-2}Mapper.java
                    │   │   └── xml
                    │   │       ├── {submodule-class-1}.xml
                    │   │       └── {submodule-class-2}.xml
                    │   ├── param
                    │   │   ├── {submodule-class-1}XXParam.java
                    │   │   ├── {submodule-class-1}XXParam.java
                    │   │   └── ...
                    │   ├── provider
                    │   │   ├── {submodule-class-1}ApiProvider.java
                    │   │   └── {submodule-class-2}ApiProvider.java
                    │   └── service
                    │       ├── {submodule-class-1}XXService.java
                    │       ├── {submodule-class-2}XXService.java
                    │       └── impl
                    │           ├── {submodule-class-1}XXServiceImpl.java
                    │           └── {submodule-class-2}XXServiceImpl.java
                    └── ...
                        ├── controller
                        ├── entity
                        ├── convert
                        ├── mapper
                        │   └── xml
                        ├── param
                        ├── provider
                        └── service
                            └── impl

Entity Package and Code Structure

The Entity class serves as the database mapping layer and must follow the standard structure below:

java
package io.github.jiangbyte.modules.{module-name}.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

@Data
@TableName(value = "table_name", autoResultMap = true)
@Schema(name = "EntityClassName", description = "Description of the entity")
public class EntityClassName implements Serializable {
    @Serial
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    @TableId
    @Schema(description = "主键")
    @org.apache.fesod.sheet.annotation.ExcelProperty("主键")
    private String id;

    // TODO: Add business fields here
    // Example:
    // @Schema(description = "Field description")
    // private String fieldName;

    // ========== Base Entity Fields (Audit & Soft Delete) ==========

    @JsonIgnore
    @TableLogic
    @Schema(description = "软删除")
    @org.apache.fesod.sheet.annotation.ExcelIgnore
    private String isDeleted;

    @JsonIgnore
    @TableField(fill = FieldFill.INSERT)
    @Schema(description = "创建人ID")
    @org.apache.fesod.sheet.annotation.ExcelProperty("创建人ID")
    private String createdBy;

    @JsonIgnore
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @Schema(description = "更新人ID")
    @org.apache.fesod.sheet.annotation.ExcelProperty("更新人ID")
    private String updatedBy;

    @JsonIgnore
    @TableField(fill = FieldFill.INSERT)
    @Schema(description = "创建时间")
    @org.apache.fesod.sheet.annotation.ExcelProperty("创建时间")
    private java.util.Date createdAt;

    @JsonIgnore
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @Schema(description = "更新时间")
    @org.apache.fesod.sheet.annotation.ExcelProperty("更新时间")
    private java.util.Date updatedAt;

}

Convert Package and Code Structure

The Convert class is responsible for converting between different layers (e.g., Param to Entity). It must use MapStruct and follow the standard structure below:

java
package io.github.jiangbyte.modules.{module-name}.convert;

import io.github.jiangbyte.modules.{module-name}.entity.EntityClassName;
import io.github.jiangbyte.modules.{module-name}.param.EntityClassNameParam;
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface EntityClassNameConvert {

    /**
     * Convert Param to Entity
     *
     * @param param the parameter object
     * @return the entity object
     */
    EntityClassName toEntity(EntityClassNameParam param);

}

Controller Package and Code Structure

The Controller class handles HTTP requests and responses. It must follow the RESTful API design pattern and include standard CRUD operations. Below is the complete template:

java
package io.github.jiangbyte.modules.{module-name}.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.jiangbyte.modules.{module-name}.entity.EntityClassName;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameExportParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNamePageParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameParam;
import io.github.jiangbyte.modules.{module-name}.service.EntityClassNameService;
import io.github.jiangbyte.pojo.IdParam;
import io.github.jiangbyte.pojo.IdsParam;
import io.github.jiangbyte.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "{Module}控制器")
@Slf4j
@RequiredArgsConstructor
@RestController("/api")
@Validated
public class EntityClassNameController {
    private final EntityClassNameService entityClassNameService;

    @Operation(summary = "获取{Module}分页")
    @SaCheckPermission("{module}/page")
    @GetMapping("/v1/{module}/page")
    public Result<Page<EntityClassName>> page(@ParameterObject @Valid EntityClassNamePageParam param) {
        return Result.success(entityClassNameService.page(param));
    }

    @Operation(summary = "添加{Module}")
    @SaCheckPermission("{module}/create")
    @PostMapping("/v1/{module}/create")
    public Result<Void> create(@RequestBody @Valid EntityClassNameParam param) {
        entityClassNameService.create(param);
        return Result.success();
    }

    @Operation(summary = "编辑{Module}")
    @SaCheckPermission("{module}/modify")
    @PostMapping("/v1/{module}/modify")
    public Result<Void> modify(@RequestBody @Valid EntityClassNameParam param) {
        entityClassNameService.modify(param);
        return Result.success();
    }

    @Operation(summary = "删除{Module}")
    @SaCheckPermission("{module}/remove")
    @PostMapping("/v1/{module}/remove")
    public Result<Void> remove(@RequestBody @Valid IdsParam param) {
        entityClassNameService.remove(param);
        return Result.success();
    }

    @Operation(summary = "获取{Module}详情")
    @SaCheckPermission("{module}/detail")
    @GetMapping("/v1/{module}/detail")
    public Result<EntityClassName> detail(@ParameterObject @Valid IdParam param) {
        return Result.success(entityClassNameService.detail(param));
    }

    @Operation(summary = "导出{Module}数据")
    @SaCheckPermission("{module}/export")
    @GetMapping("/v1/{module}/export")
    public void export(HttpServletResponse response, @ParameterObject @Valid EntityClassNameExportParam param) {
        entityClassNameService.export(response, param);
    }

    @Operation(summary = "下载{Module}导入模板")
    @SaCheckPermission("{module}/template")
    @GetMapping("/v1/{module}/template")
    public void downloadTemplate(HttpServletResponse response) {
        entityClassNameService.downloadTemplate(response);
    }

    @Operation(summary = "导入{Module}数据")
    @SaCheckPermission("{module}/import")
    @PostMapping("/v1/{module}/import")
    public Result<Void> importData(@RequestPart("file") MultipartFile file) {
        entityClassNameService.importData(file);
        return Result.success();
    }
}

Controller API Endpoint Conventions

MethodEndpoint PatternDescription
GET/v1/{module}/pagePaginated query
POST/v1/{module}/createCreate new record
POST/v1/{module}/modifyUpdate existing record
POST/v1/{module}/removeDelete records (batch support)
GET/v1/{module}/detailGet single record by ID
GET/v1/{module}/exportExport data
GET/v1/{module}/templateDownload import template
POST/v1/{module}/importImport data from Excel

Note: Permission annotations (@SaCheckPermission) are commented out by default. Uncomment and adjust the permission strings based on your security requirements. All controller methods should include proper @Operation annotations for API documentation.

Service Interface Package and Code Structure

The Service interface defines business logic contracts. It extends MyBatis-Plus IService and declares all public business methods:

java
package io.github.jiangbyte.modules.{module-name}.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import io.github.jiangbyte.modules.{module-name}.entity.EntityClassName;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameExportParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNamePageParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameParam;
import io.github.jiangbyte.pojo.IdParam;
import io.github.jiangbyte.pojo.IdsParam;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;

public interface EntityClassNameService extends IService<EntityClassName> {

    Page<EntityClassName> page(EntityClassNamePageParam param);

    void create(EntityClassNameParam param);

    void modify(EntityClassNameParam param);

    void remove(IdsParam param);

    EntityClassName detail(IdParam param);

    void export(HttpServletResponse response, EntityClassNameExportParam param);

    void downloadTemplate(HttpServletResponse response);

    void importData(MultipartFile file);

}

Service Method Conventions

MethodParameterDescription
page*PageParamPaginated query with filters
create*ParamCreate new entity
modify*ParamUpdate existing entity
removeIdsParamDelete by ID list
detailIdParamQuery by single ID
export*ExportParam, HttpServletResponseExport data to Excel
downloadTemplateHttpServletResponseDownload import template
importDataMultipartFileImport data from Excel

Service Implementation Package and Code Structure

The Service Implementation class contains the business logic implementation. It extends MyBatis-Plus ServiceImpl and implements the corresponding service interface:

java
package io.github.jiangbyte.modules.{module-name}.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.github.jiangbyte.enums.ExportTypeEnum;
import io.github.jiangbyte.exception.BusinessException;
import io.github.jiangbyte.modules.{module-name}.convert.EntityClassNameConvert;
import io.github.jiangbyte.modules.{module-name}.entity.EntityClassName;
import io.github.jiangbyte.modules.{module-name}.mapper.EntityClassNameMapper;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameExportParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNamePageParam;
import io.github.jiangbyte.modules.{module-name}.params.EntityClassNameParam;
import io.github.jiangbyte.modules.{module-name}.service.EntityClassNameService;
import io.github.jiangbyte.pojo.IdParam;
import io.github.jiangbyte.pojo.IdsParam;
import io.github.jiangbyte.pojo.PageBounds;
import io.github.jiangbyte.result.ResultCode;
import io.github.jiangbyte.utils.FesodUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.context.AnalysisContext;
import org.apache.fesod.sheet.read.listener.ReadListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class EntityClassNameServiceImpl extends ServiceImpl<EntityClassNameMapper, EntityClassName> implements EntityClassNameService {
    private final EntityClassNameConvert entityClassNameConvert;

    @Override
    public Page<EntityClassName> page(EntityClassNamePageParam param) {
        QueryWrapper<EntityClassName> queryWrapper = new QueryWrapper<EntityClassName>().checkSqlInjection();
        Page<EntityClassName> page = this.page(
                PageBounds.of(param.getCurrent(), param.getSize()),
                queryWrapper
        );
        return page;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void create(EntityClassNameParam param) {
        EntityClassName entity = entityClassNameConvert.toEntity(param);
        entity.setId(null);
        this.save(entity);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void modify(EntityClassNameParam param) {
        if (this.exists(new LambdaQueryWrapper<EntityClassName>()
                .eq(EntityClassName::getId, param.getId()))
        ) {
            EntityClassName entity = entityClassNameConvert.toEntity(param);
            this.updateById(entity);
        } else {
            throw new BusinessException(ResultCode.BAD_REQUEST);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void remove(IdsParam param) {
        List<String> ids = param.getIds();
        this.removeByIds(ids);
    }

    @Override
    public EntityClassName detail(IdParam param) {
        EntityClassName entity = this.getById(param.getId());
        return entity;
    }

    @Override
    public void export(HttpServletResponse response, EntityClassNameExportParam param) {
        String fileName = "{Module}数据";
        String sheetName = "{Module}数据";
        try {
            List<EntityClassName> list = new ArrayList<>();

            if (ExportTypeEnum.CURRENT.getValue().equals(param.getExportType())) {
                QueryWrapper<EntityClassName> queryWrapper = new QueryWrapper<EntityClassName>().checkSqlInjection();
                Page<EntityClassName> page = this.page(
                        PageBounds.of(param.getCurrent(), param.getSize()),
                        queryWrapper
                );
                list = page.getRecords();
            } else if (ExportTypeEnum.SELECTED.getValue().equals(param.getExportType())) {
                list = this.listByIds(param.getSelectedId());
            } else if (ExportTypeEnum.ALL.getValue().equals(param.getExportType())) {
                list = this.list();
            } else {
                throw new BusinessException(ResultCode.BAD_REQUEST);
            }

            FesodUtils.export(response, fileName, sheetName, EntityClassName.class, list);
        } catch (IOException e) {
            log.error("导出{Module}数据失败", e);
            throw new BusinessException(ResultCode.INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    public void downloadTemplate(HttpServletResponse response) {
        String fileName = "{Module}导入模板";
        String sheetName = "{Module}数据";
        try {
            List<EntityClassName> templateList = new ArrayList<>();
            FesodUtils.exportTemplate(response, fileName, sheetName, EntityClassName.class, templateList);
        } catch (IOException e) {
            log.error("下载{Module}导入模板失败", e);
            throw new BusinessException(ResultCode.INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importData(MultipartFile file) {
        try {
            List<EntityClassName> importList = new ArrayList<>();

            ReadListener<EntityClassName> readListener = new ReadListener<>() {
                @Override
                public void invoke(EntityClassName data, AnalysisContext context) {
                    importList.add(data);
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    log.info("{Module}数据导入完成,共导入 {} 条数据", importList.size());
                }
            };

            FesodSheet.read(file.getInputStream(), EntityClassName.class, readListener).sheet().doRead();
            this.saveBatch(importList);
        } catch (IOException e) {
            log.error("导入{Module}数据失败", e);
            throw new BusinessException(ResultCode.INTERNAL_SERVER_ERROR);
        }
    }
}

Service Implementation Conventions

MethodTransactionalKey Logic
pageNoBuilds query wrapper with SQL injection protection
createYesConverts param to entity, sets ID to null, saves
modifyYesChecks existence before update, throws exception if not found
removeYesBatch delete by ID list
detailNoRetrieves by ID
exportNoSupports CURRENT/SELECTED/ALL export types
importDataYesUses FesodSheet listener for batch import

Note: All write operations (create, modify, remove, importData) must be annotated with @Transactional(rollbackFor = Exception.class) to ensure data consistency. The QueryWrapper should always call .checkSqlInjection() to prevent SQL injection attacks.

Mapper Interface and XML Package and Code Structure

The Mapper interface extends MyBatis-Plus BaseMapper and provides database access layer methods. The corresponding XML file contains custom SQL statements.

Mapper Interface

java
package io.github.jiangbyte.modules.{module-name}.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.github.jiangbyte.modules.{module-name}.entity.EntityClassName;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EntityClassNameMapper extends BaseMapper<EntityClassName> {

}

Mapper XML

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.jiangbyte.modules.{module-name}.mapper.EntityClassNameMapper">

    <!-- Custom SQL statements go here -->
    <!-- Example:
    <select id="selectByCondition" resultType="io.github.jiangbyte.modules.{module-name}.entity.EntityClassName">
        SELECT * FROM table_name WHERE ...
    </select>
    -->

</mapper>

Mapper Conventions

ComponentLocationDescription
Mapper interface{module-name}.mapperExtends BaseMapper<T>, annotated with @Mapper
XML fileresources/mapper/{module-name}/Same name as interface, placed in src/main/resources
NamespaceXML namespace attributeMust match the fully qualified mapper interface name

Note: For standard CRUD operations, no additional methods are needed as MyBatis-Plus BaseMapper provides built-in methods. Custom complex SQL queries should be added to both the interface and XML file.

Parameter Object Package and Code Structure

Parameter objects (Param, PageParam, ExportParam) are used for request data binding and validation.

EntityClassNameParam (Create/Update Parameter)

java
package io.github.jiangbyte.modules.{module-name}.params;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

@Data
@Schema(name = "EntityClassNameParam", description = "{Module}处理参数")
public class EntityClassNameParam implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    @Schema(description = "Primary key")
    private String id;

    // TODO: Add business fields here matching the entity
    // Example:
    // @Schema(description = "Field description")
    // private String fieldName;

}

EntityClassNamePageParam (Pagination Parameter)

java
package io.github.jiangbyte.modules.{module-name}.params;

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

import java.io.Serial;
import java.io.Serializable;

@Data
@Schema(name = "EntityClassNamePageParam", description = "{Module}分页请求参数")
public class EntityClassNamePageParam implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    @Schema(description = "Page size", example = "10")
    private Long size;

    @Schema(description = "Current page number", example = "1")
    private Long current;

    // TODO: Add filter fields here
    // Example:
    // @Schema(description = "Search keyword")
    // private String keyword;

}

EntityClassNameExportParam (Export Parameter)

java
package io.github.jiangbyte.modules.{module-name}.params;

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

import java.io.Serial;
import java.io.Serializable;
import java.util.List;

@Data
@Schema(name = "EntityClassNameExportParam", description = "{Module}导出请求参数")
public class EntityClassNameExportParam implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    @Schema(description = "Export type: CURRENT, SELECTED, ALL", allowableValues = {"CURRENT", "SELECTED", "ALL"})
    private String exportType;

    @Schema(description = "Current page number (for CURRENT export type)")
    private Long current;

    @Schema(description = "Page size (for CURRENT export type)")
    private Long size;

    @Schema(description = "Selected IDs (for SELECTED export type)")
    private List<String> selectedId;

    // TODO: Add filter fields for export
}

Parameter Object Conventions

ClassPurposeKey Fields
*ParamCreate and update operationsid (optional for create, required for update), business fields
*PageParamPaginated queriescurrent (page number), size (page size), filter fields
*ExportParamData exportexportType, current, size, selectedId, filter fields

Note: All parameter classes must implement Serializable and include serialVersionUID. Use @Schema annotations for API documentation. The id field in *Param is used to identify which record to update and should be null when creating new records.

Released under the MIT License.