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
└── implEntity Package and Code Structure
The Entity class serves as the database mapping layer and must follow the standard structure below:
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:
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:
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
| Method | Endpoint Pattern | Description |
|---|---|---|
| GET | /v1/{module}/page | Paginated query |
| POST | /v1/{module}/create | Create new record |
| POST | /v1/{module}/modify | Update existing record |
| POST | /v1/{module}/remove | Delete records (batch support) |
| GET | /v1/{module}/detail | Get single record by ID |
| GET | /v1/{module}/export | Export data |
| GET | /v1/{module}/template | Download import template |
| POST | /v1/{module}/import | Import 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@Operationannotations 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:
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
| Method | Parameter | Description |
|---|---|---|
page | *PageParam | Paginated query with filters |
create | *Param | Create new entity |
modify | *Param | Update existing entity |
remove | IdsParam | Delete by ID list |
detail | IdParam | Query by single ID |
export | *ExportParam, HttpServletResponse | Export data to Excel |
downloadTemplate | HttpServletResponse | Download import template |
importData | MultipartFile | Import 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:
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
| Method | Transactional | Key Logic |
|---|---|---|
page | No | Builds query wrapper with SQL injection protection |
create | Yes | Converts param to entity, sets ID to null, saves |
modify | Yes | Checks existence before update, throws exception if not found |
remove | Yes | Batch delete by ID list |
detail | No | Retrieves by ID |
export | No | Supports CURRENT/SELECTED/ALL export types |
importData | Yes | Uses 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. TheQueryWrappershould 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
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 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
| Component | Location | Description |
|---|---|---|
| Mapper interface | {module-name}.mapper | Extends BaseMapper<T>, annotated with @Mapper |
| XML file | resources/mapper/{module-name}/ | Same name as interface, placed in src/main/resources |
| Namespace | XML namespace attribute | Must match the fully qualified mapper interface name |
Note: For standard CRUD operations, no additional methods are needed as MyBatis-Plus
BaseMapperprovides 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)
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)
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)
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
| Class | Purpose | Key Fields |
|---|---|---|
*Param | Create and update operations | id (optional for create, required for update), business fields |
*PageParam | Paginated queries | current (page number), size (page size), filter fields |
*ExportParam | Data export | exportType, current, size, selectedId, filter fields |
Note: All parameter classes must implement
Serializableand includeserialVersionUID. Use@Schemaannotations for API documentation. Theidfield in*Paramis used to identify which record to update and should benullwhen creating new records.