# CRUD 示例代码(ylsw-bw / ledger · meter) 以下示例提炼自 `business/water-meter/src/main/java/com/tofly/wm/ledger/meter/`,生成新模块时替换 `{Domain}`、`{domain}`、`TF_WM_{DOMAIN}_W` 等占位符即可。 ## 1) Controller(标准 CRUD + 权限) ```java package com.tofly.wm.ledger.{domain}; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tofly.common.core.entity.ResultResponse; import com.tofly.entity.enumertion.Permission; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.web.bind.annotation.*; import java.util.List; @RequestMapping("/{domain}") @RestController @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @RequiredArgsConstructor @Api(tags = "{业务中文名}") public class {Domain}Controller { {Domain}Service {domain}Service; @GetMapping("/page") @ApiOperation("{业务中文名}分页查询") public ResultResponse> page({Domain}Query query) { return ResultResponse.success(this.{domain}Service.page(query)); } @GetMapping("/list") @ApiOperation("{业务中文名}列表查询") public ResultResponse> list({Domain}Query query) { return ResultResponse.success(this.{domain}Service.list(query)); } @GetMapping("/{id}") @ApiOperation("{业务中文名}详情查询") public ResultResponse<{Domain}> getById(@PathVariable @ApiParam("主键") Long id) { return ResultResponse.success(this.{domain}Service.getById(id)); } @PostMapping @ApiOperation("{业务中文名}新增") public ResultResponse add(@RequestBody {Domain} entity) { return ResultResponse.success(this.{domain}Service.save(entity)); } @PostMapping("/update") @ApiOperation("{业务中文名}修改") public ResultResponse update(@RequestBody {Domain} entity) { return ResultResponse.success(this.{domain}Service.updateById(entity)); } @DeleteMapping("/deleteByIds") @ApiOperation("{业务中文名}删除") public ResultResponse deleteByIds(@RequestParam @ApiParam("主键,逗号分隔,如 1,2,3") String ids) { return ResultResponse.success(this.{domain}Service.deleteByIds(ids)); } } ``` ## 2) Query(列表 / 分页共用查询对象) `GET /list` 与 `GET /page` **共用同一 Query**,条件字段保持一致;`/page` 额外使用 `PageQuery` 中的 `pageNum`、`pageSize`。 **Query 不使用 `OrganizationEntry` / `UserNameEntry` / `DictEntry`**,部门/用户筛选用 `Long` 或 `String`;值对象仅用于 Entity/Vo。 ```java package com.tofly.wm.ledger.{domain}; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tofly.entity.pojo.PageQuery; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data @ApiModel("{业务中文名}查询") public class {Domain}Query extends PageQuery { @ApiModelProperty("申请部门ID") Long applyDeptId; @ApiModelProperty("示例编码") String code; @Override public void wrapper(LambdaQueryWrapper wrapper) { // 单表场景可留空,条件在 Service 的 buildWrapper 中构建 } } ``` ## 3) Entity(BaseEntity + 业务字段) **强制**:所有 `@TableName` 实体 **`extends BaseEntity`**;公共列由父类映射。**建表**时在 `CREATE TABLE` 中声明公共列(见 [java-ylsw-bw.md](java-ylsw-bw.md) § BaseEntity),勿用 `ALTER TABLE` 补列。 **禁止**在子类重复声明:`createUser`、`createTime`、`updateUser`、`updateTime`、`createCompanyId`~`curDeptId`(**`Boolean deleted`** 若 `BaseEntity` 已提供则不再声明)。 ```java package com.tofly.wm.manage.{biz}; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.tofly.entity.annotation.DictDirectory; import com.tofly.entity.pojo.BaseEntity; import com.tofly.entity.pojo.DictEntry; import com.tofly.entity.pojo.FileEntryList; import com.tofly.entity.pojo.OrganizationEntry; import com.tofly.entity.pojo.UserNameEntry; import com.tofly.wm.cons.MeterConstant; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.FieldDefaults; /** * {业务中文名}实体 * * @author ... * @date ... */ @Data @EqualsAndHashCode(callSuper = true) @FieldDefaults(level = AccessLevel.PRIVATE) @TableName("TF_WM_{DOMAIN}_W") @ApiModel("{业务中文名}") public class {Domain} extends BaseEntity { @TableId(value = "ID", type = IdType.ASSIGN_ID) @ApiModelProperty("主键") Long id; @TableField("APPLY_DEPT_ID") @ApiModelProperty("申请部门") OrganizationEntry applyDept; @TableField("APPLY_USER_ID") @ApiModelProperty("申请人") UserNameEntry applyUser; @DictDirectory(MeterConstant.DICT_METER_JJCD) @TableField("PRIORITY_CODE") @ApiModelProperty("紧急程度") DictEntry priority; @DictDirectory(MeterConstant.DICT_METER_TYPE) @TableField("METER_TYPE_CODE") @ApiModelProperty("水表类型") DictEntry meterType; @TableField(exist = false) @ApiModelProperty("附件") FileEntryList attach; } ``` **逻辑删除**(`BaseEntity` 未含 `deleted` 时在子类声明;库列仍为 `DELETED`): ```java @TableLogic @TableField("DELETED") @ApiModelProperty("删除状态") Boolean deleted = false; ``` **单附件**(仅一个文件时用 `FileEntry`,同样不落库): ```java @TableField(exist = false) @ApiModelProperty("附件") FileEntry attach; ``` **明细表示例**(同样继承 BaseEntity): ```java @TableName("TF_WM_{DOMAIN}_LIST_W") public class {Domain}List extends BaseEntity { @TableId(value = "ID", type = IdType.ASSIGN_ID) Long id; @TableField("PARENT_ID") Long parentId; // 仅业务字段,无 CREATE_USER/DELETED 等 } ``` **保存时赋值(Service):** ```java entity.setApplyDept(OrganizationEntry.of(ApplicationSession.getDepartmentId())); entity.setApplyUser(UserNameEntry.of(ApplicationSession.getUserId())); ``` **建表 COMMENT(关联列须写目标表)**: ```sql COMMENT ON COLUMN TF_WM_{DOMAIN}_W.APPLY_DEPT_ID IS '申请部门ID,关联tf_org表的id字段'; COMMENT ON COLUMN TF_WM_{DOMAIN}_W.APPLY_USER_ID IS '申请用户ID,关联tf_user表的id字段'; COMMENT ON COLUMN TF_WM_{DOMAIN}_W.PRIORITY_CODE IS '紧急程度编号,字典类型,目录码METER_JJCD'; COMMENT ON COLUMN TF_WM_{DOMAIN}_W.TASK_STATUS_CODE IS '任务状态编号,枚举类型,TaskStatusEnum'; ``` ## 4) 业务枚举 ```java package com.tofly.wm.ledger.{domain}.enums; import com.tofly.entity.enumertion.Enumerable; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum {Domain}StatusEnum implements Enumerable { ENABLED(1, "启用"), DISABLED(0, "停用"), ; Integer code; String label; @Override public Integer getKey() { return this.code; } } ``` ## 5) Mapper ```java package com.tofly.wm.ledger.{domain}; import com.github.yulichang.base.MPJBaseMapper; public interface {Domain}Mapper extends MPJBaseMapper<{Domain}> { } ``` ## 6) Service + ServiceImpl(单表 Wrappers + PageHelper) ```java package com.tofly.wm.ledger.{domain}; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.yulichang.base.MPJBaseService; public interface {Domain}Service extends MPJBaseService<{Domain}> { /** 列表查询,条件与 page 一致 */ List<{Domain}> list({Domain}Query query); Page<{Domain}> page({Domain}Query query); /** ids 逗号分隔,如 "1,2,3" */ boolean deleteByIds(String ids); } ``` ```java package com.tofly.wm.ledger.{domain}; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.yulichang.base.MPJBaseServiceImpl; import com.tofly.mybatisplus.page.PageHelper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Service public class {Domain}ServiceImpl extends MPJBaseServiceImpl<{Domain}Mapper, {Domain}> implements {Domain}Service { /** * 列表与分页共用条件 */ private LambdaQueryWrapper<{Domain}> buildWrapper({Domain}Query q) { return Wrappers.<{Domain}>lambdaQuery() //申请部门(Query 用 Long,Entity 字段为 OrganizationEntry) .eq(Objects.nonNull(q.getApplyDeptId()), {Domain}::getApplyDept, q.getApplyDeptId()) //编码 .like(StrUtil.isNotBlank(q.getCode()), {Domain}::getCode, q.getCode()) .eq({Domain}::getDeleted, false); } @Override public List<{Domain}> list({Domain}Query query) { return this.list(buildWrapper(query)); } @Override public Page<{Domain}> page({Domain}Query query) { return PageHelper.startPage(query, p -> this.list(query)); } @Override @Transactional(rollbackFor = Exception.class) public boolean deleteByIds(String ids) { if (StrUtil.isBlank(ids)) { return false; } List idList = Arrays.stream(ids.split(",")) .map(String::trim) .filter(StrUtil::isNotBlank) .map(Long::parseLong) .collect(Collectors.toList()); return this.removeByIds(idList); } } ``` ## 7) 台账 Controller(分页 + 导出,无写接口) 参考 `ledger/arrive`、`ledger/verify`: ```java @Api(tags = "{台账中文名}") @RestController @RequestMapping("/{domain}Ledger") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class {Domain}LedgerController { {Domain}LedgerService {domain}LedgerService; @GetMapping("/page") @ApiOperation("分页查询") public ResultResponse> page({Domain}LedgerQuery query) { return ResultResponse.success({domain}LedgerService.page(query)); } @GetMapping("/export") @ApiOperation("导出") public void export({Domain}LedgerQuery query) { {domain}LedgerService.export(query); } } ``` ## 8) 台账 Service(MPJ 多表关联 + EasyPoi 导出) ```java @Service @AllArgsConstructor public class {Domain}LedgerService { SomeEntityService someEntityService; public List<{Domain}LedgerVo> getList({Domain}LedgerQuery query) { return someEntityService.selectJoinList({Domain}LedgerVo.class, MPJWrappers.lambdaJoin() .selectAs(SomeEntity::getId, {Domain}LedgerVo::getId) .selectAs(Related::getName, {Domain}LedgerVo::getRelatedName) .innerJoin(Related.class, Related::getId, SomeEntity::getRelatedId) .eq(SomeEntity::getDeleted, false) .like(StrUtil.isNotBlank(query.getCode()), SomeEntity::getCode, query.getCode()) ); } public Page<{Domain}LedgerVo> page({Domain}LedgerQuery query) { return PageHelper.startPage(query, this::getList); } public void export({Domain}LedgerQuery query) { List<{Domain}LedgerVo> voList = this.getList(query); Map dataMap = HashMaps.of("dataList", voList); EasyPoiUtil.download("{台账中文名}.xlsx", "/template/{domain}Ledger.xlsx", dataMap); } } ``` ## 9) Mapper.xml(仅复杂 SQL 时使用) ```xml ``` ## 10) MeterConstant(字典目录) ```java package com.tofly.wm.cons; public interface MeterConstant { String DICT_METER_JJCD = "METER_JJCD"; // 紧急程度 String DICT_METER_TYPE = "METER_TYPE"; // 水表类型 String DICT_METER_DIAMETER = "METER_DIAMETER"; // 口径 String DICT_WORKER_ORDER = "METER_ORDER_TYPE"; // 工单类型 String PURCHASE_APPLY = "PURCHASE_APPLY"; // 采购申请流水类型 } ``` ## 11) 反例 ```java // 错误:返回裸 ResultResponse public ResultResponse page(MeterQuery query) { ... } // 错误:在 Mapper 上用注解写 SQL @Select("select * from TF_WM_METER_W") List listAll(); // 错误:Controller 内拼装 Wrappers / 调 Mapper @GetMapping("/page") public ResultResponse> page(MeterQuery query) { return ResultResponse.success(meterMapper.selectPage(...)); } // 错误:提供单条删除(本仓库仅 DELETE /deleteByIds) @DeleteMapping("/{id}") public ResultResponse delete(@PathVariable Long id) { ... } // 错误:与 meter 模块不一致却强行使用 PUT 修改(本仓库 ledger CRUD 用 POST /update) @PutMapping public ResultResponse update(@RequestBody Meter meter) { ... } // 错误:list 与 page 使用不同 Query 或不同条件逻辑(应共用 buildWrapper / list(query)) @GetMapping("/list") public ResultResponse> list(MeterListQuery query) { ... } // 错误:Entity 用 Long + String 拆部门,未用 OrganizationEntry Long applyDeptId; String applyDeptName; // 错误:OrganizationEntry/UserNameEntry 再建 *_name 快照列 // apply_dept_id + apply_dept_name 双列落库 // 错误:DictEntry 映射 NUMBER 或非 VARCHAR 列 @TableField("PRIORITY") Integer priorityCode; // 错误:FileEntry 映射库列 CLOB/ATTACH @TableField("ATTACH") FileEntryList attach; // 错误:FileEntry 未标注 exist = false FileEntryList photos; // 错误:Query 使用 OrganizationEntry(Query 应使用 Long applyDeptId 等) OrganizationEntry applyDept; // 错误:字典字段无 @DictDirectory 或魔法字符串 @TableField("PRIORITY_CODE") String priorityCode; ``` ## 仓库对照路径 | 类型 | 路径 | |------|------| | 标准 CRUD | `ledger/meter/MeterController.java` 等 | | 采购台账 | `ledger/purchase/PurchaseLedgerController.java` | | 到货台账 | `ledger/arrive/MeterArriveLedgerController.java` | | 落地检台账 | `ledger/verify/MeterVerifyLedgerController.java` | | 附件 FileEntryList | Entity 用 `@TableField(exist = false)`,参考 `manage/puachase/claim/PurchaseClaim.java` |