--- name: crud-by-controller-ylsw-bw description: 基于仪陇表务(ylsw-bw / water-meter)ledger 模块风格生成 CRUD、台账查询与导出接口。参考 com.tofly.wm.ledger.meter 等既有实现,统一 ResultResponse、MPJ、PageHelper 与权限注解。用户提到表务、水表台账、ledger、出入库、生成接口时使用。 --- # CRUD 生成 Skill(仪陇表务 ylsw-bw) ## 适用场景 当用户要求在 `business/water-meter` 或表务相关模块中新增/补全接口时,优先对齐 **`com.tofly.wm.ledger`** 下既有代码(首选参考 `ledger/meter`,台账类参考 `ledger/purchase`、`ledger/arrive`、`ledger/verify`)。 ## 公共 Skill 引用 - POM 工作流:`language/java/pom-factory-skill.md`;**Maven 治理**:[`rules/java-pom-governance.mdc`](../../../../../rules/java/java-pom-governance.mdc)。 - **Java CRUD 通用规范**:[`rules/java-crud-common.mdc`](../../../../../rules/java/java-crud-common.mdc)。 - **tf-ylsw 条线 MUST/SHOULD**:[`rules/java-tf-ylsw-backend.mdc`](../../../../../rules/java/java-tf-ylsw-backend.mdc)(与 ledger 实际写法冲突时,以 ledger 存量风格为准,并在输出中注明差异)。 - 当本文件与 **java-crud-common** 冲突时,本文件(ylsw-bw / ledger)优先。 ## 代码落点与包结构(对齐 `com.tofly.wm`) 模块根包 **`com.tofly.wm`**,按业务分层;**新建代码必须落在与职责匹配的包下**,禁止全部堆在 `ledger` 或单一层级。 ### 包树与职责(参考 ledger / manage 存量) ``` com.tofly.wm ├── cons/ # 模块常量(字典目录码、业务编码前缀等) │ └── MeterConstant.java ├── ledger/ # 台账、出入库、只读汇总(少写或不写主数据) │ ├── meter/ # 水表出入库 CRUD │ │ ├── enums/ # 域内枚举(BoundTypeEnum、CompanyTypeEnum…) │ │ └── app/ # 子场景(App 入库、领用等),*Controller/*Service/*Vo │ ├── purchase/ # 采购台账(Vo + Query + Service,无独立 Entity 表) │ ├── arrive/ # 到货台账 │ └── verify/ # 落地检台账 ├── manage/ # 管理端业务写操作、主数据维护 │ ├── puachase/ # 采购(存量包名 puachase,新建子包沿用勿改拼写) │ │ ├── apply/ # 采购申请 │ │ │ └── list/ # 申请明细子表 │ │ ├── plan/ # 采购计划 │ │ │ ├── arrive/ # 到货单 │ │ │ └── arriveList/ # 到货明细 │ │ └── claim/ # 领用 │ ├── brand/ # 厂家 │ ├── verify/ # 检定管理(含 giveback 等子包) │ └── warehouse/ ├── workorder/ # 工单 └── api/ # 对外/第三方接口(verify 等) ``` ### 单包内文件组织(以 `ledger/meter` 为准) | 类型 | 命名 | 位置 | |------|------|------| | 接口 | `{Domain}Controller` | 域包根目录 | | 服务接口/实现 | `{Domain}Service` / `{Domain}ServiceImpl` | 域包根目录;台账可无接口,仅 `{Domain}LedgerService` | | 实体 | `{Domain}` | 域包根目录 | | 查询 | `{Domain}Query` / `{Domain}LedgerQuery` | 域包根目录 | | 展示 | `{Domain}Vo` / `{Domain}LedgerVo` | 域包根目录 | | Mapper | `{Domain}Mapper` | 域包根目录 | | 枚举 | `*Enum` | `{domain}/enums/` | | App/专项 | `App*`、`Claim*` | `{domain}/app/` 等子包 | - **同包平铺**为主,不按 `controller/service/entity` 再分子包(与 ledger 一致)。 - **子表/明细**:独立子包 `list`、`arriveList` 等,类名与主表关联清晰。 - **常量**:字典目录、流水号类型等放 **`com.tofly.wm.cons.MeterConstant`**,禁止魔法字符串。 - 表名前缀 **`TF_WM_*`**,业务表多为 **`_W`**;`mapper.xml` → `src/main/resources/mapper/`。 ### 选型:ledger 还是 manage? | 场景 | 推荐包 | |------|--------| | 分页/列表/导出台账、跨表只读汇总 | `ledger.{domain}` | | 主表 CRUD、流程、审批、写入业务表 | `manage.{业务域}` | | 移动端/专项入口 | `ledger.{domain}.app` 或 `manage.*.app` | ## ylsw-bw 专项约束 ### 接口与返回 - 返回体统一 **`ResultResponse`**(带泛型,禁止裸类型)。 - Controller:`@RestController` + `@RequestMapping("/xxx")` + `@Api(tags = "...")`。 - 依赖注入:`@RequiredArgsConstructor` + `@FieldDefaults(level = PRIVATE, makeFinal = true)`(Controller 常见);Service 可用 `@AllArgsConstructor` 或 `@Service` 无接口实现类。 ### 路由习惯(对齐 meter) | 能力 | 方法 | 路径 | 说明 | |------|------|------|------| | 分页 | GET | `/page` | 与 `/list` 共用 `{Domain}Query`(含 `pageNum`/`pageSize`) | | 列表 | GET | `/list` | 与 `/page` **同一 Query、同一 Service 条件**;不分页,返回 `List` | | 详情 | GET | `/{id}` | | | 新增 | POST | `/` | `@RequestBody` 实体 | | 修改 | POST | `/update` | 本模块用 POST 而非 PUT | | 删除 | DELETE | `/{id}` | | | 批量删除 | DELETE | `/deleteByIds` | `ids` 请求参数,逗号分隔主键 | | 导出 | GET | `/export` | 返回 `void`,`EasyPoiUtil.download`(台账类) | ### 工具类与空值判断 | 场景 | 用法 | 禁止/避免 | |------|------|-----------| | 字符串为空 | `StrUtil.isNotBlank(s)` / `StrUtil.isBlank(s)` | 手写 `s != null && !s.isEmpty()` | | 集合为空 | `CollUtil.isEmpty(list)` / `CollUtil.isNotEmpty(list)` | 仅 `list == null` | | 对象拷贝 | `BeanUtil.copyProperties(...)` | 大段手动 set(无业务差异时) | | **对象**是否为 null | **`Objects.nonNull(x)`** / **`Objects.isNull(x)`** | 对象用 `== null` 写在条件首参外且无一致性时 | | 枚举入库条件 | `Optional.ofNullable(enum).map(Enumerable::getKey).orElse(null)` | 直接 `.eq(field, enum)` 未取 key | | 范围时间 | `.between(Objects.nonNull(q.getStartDate()), ...)` | 未判空即 between | - Hutool 包名:**`cn.hutool.core.*`**(`StrUtil`、`CollUtil`、`BeanUtil` 等),与项目存量一致。 - **字符串**用 Hutool;**引用类型对象**(实体字段、枚举、Long、OrganizationEntry 等)用 **`Objects`**。 ### 分页与查询 - 查询类继承 **`PageQuery`**,Swagger `@ApiModel` / `@ApiModelProperty`。 - **Query 字段类型**:筛选用 `Long`/`String`/枚举/`LocalDate` 等基础类型;**`OrganizationEntry`、`UserNameEntry`、`DictEntry` 仅出现在 Entity/Vo**,不出现在 Query(字典筛选可用字典 code 的 `String`)。 - 单表条件:**`Wrappers.lambdaQuery()`**,条件首参遵循上表;枚举用 **`Enumerable.getKey()`**。 - 列表:**`list(query)`** 与分页共用 `buildWrapper`;`page(query)` 内部 **`PageHelper.startPage(query, () -> list(query))`**。 - 批量删除:Service **`deleteByIds(String ids)`** 解析逗号分隔主键后 `removeByIds`,需 **`@Transactional`**。 - 多表台账只读:**`MPJWrappers.lambdaJoin()`** + `selectJoinList`(见 purchase/verify 台账),不强行拆 XML。 - **关键条件行**加简短行尾注释(见注释规范),如 `//出厂编码`、`//需求部门`。 ### Service / Mapper 基类 - 实体 CRUD:`MPJBaseService` / `MPJBaseServiceImpl`。 - Mapper:`MPJBaseMapper`;简单查询不写 XML。 - 复杂统计、窗口函数、批量状态更新:在 `*Mapper.xml` 编写,**禁止** Mapper 注解 SQL。 ### 实体与 BaseEntity(强制) > **与 `database-design.mdc` 的关系**:公司通用规范写 `create_user`/`is_delete` 等;**本仓库(ylsw-bw / Oracle 11)以 `com.tofly.entity.pojo.BaseEntity` 映射为准**,逻辑删除列名为 **`DELETED`**(`NUMBER(1)`),创建/更新人为 **`NUMBER(19)`**。建表、实体、Skill 自检均以本节为准。 #### 建表:每张业务表必须包含的公共列 **新建** `TF_WM_*` 主表、明细表时,在 **`CREATE TABLE` 语句内**与主键 `ID`、业务字段**一并声明**下列列(Oracle 11),**不要**单独用 `ALTER TABLE ... ADD` 事后补列。 ```sql CREATE TABLE TF_WM_XXX_W ( ID NUMBER(19) NOT NULL, -- BaseEntity 公共列(主表/明细表均须包含) CREATE_USER NUMBER(19), CREATE_TIME TIMESTAMP, UPDATE_USER NUMBER(19), UPDATE_TIME TIMESTAMP, DELETED NUMBER(1) DEFAULT 0, CREATE_COMPANY_ID NUMBER(19), CREATE_COMPANY_NAME VARCHAR2(200), CREATE_DEPT_ID NUMBER(19), CREATE_DEPT_NAME VARCHAR2(200), CREATE_POST_ID NUMBER(19), CREATE_POST_NAME VARCHAR2(200), CREATE_USER_NAME VARCHAR2(100), CUR_COMPANY_ID NUMBER(19), CUR_DEPT_ID NUMBER(19), -- 业务字段 … CONSTRAINT PK_TF_WM_XXX_W PRIMARY KEY (ID) ); COMMENT ON TABLE TF_WM_XXX_W IS '表说明'; COMMENT ON COLUMN TF_WM_XXX_W.ID IS '主键'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_USER IS '创建人'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_TIME IS '创建时间'; COMMENT ON COLUMN TF_WM_XXX_W.UPDATE_USER IS '更新人'; COMMENT ON COLUMN TF_WM_XXX_W.UPDATE_TIME IS '更新时间'; COMMENT ON COLUMN TF_WM_XXX_W.DELETED IS '删除状态:0未删除 1已删除'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_COMPANY_ID IS '创建公司ID'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_COMPANY_NAME IS '创建公司名称'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_DEPT_ID IS '创建部门ID'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_DEPT_NAME IS '创建部门名称'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_POST_ID IS '创建岗位ID'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_POST_NAME IS '创建岗位名称'; COMMENT ON COLUMN TF_WM_XXX_W.CREATE_USER_NAME IS '创建用户名称'; COMMENT ON COLUMN TF_WM_XXX_W.CUR_COMPANY_ID IS '当前公司ID'; COMMENT ON COLUMN TF_WM_XXX_W.CUR_DEPT_ID IS '当前部门ID'; -- 业务列 COMMENT 逐列补充 ``` | 列名 | 类型 | 说明 | |------|------|------| | CREATE_USER | NUMBER(19) | 创建人 ID | | CREATE_TIME | TIMESTAMP | 创建时间 | | UPDATE_USER | NUMBER(19) | 更新人 ID | | UPDATE_TIME | TIMESTAMP | 更新时间 | | DELETED | NUMBER(1) | 逻辑删除:0 未删,1 已删(Java 侧见下节 `deleted`) | | CREATE_COMPANY_ID / CREATE_COMPANY_NAME | | 创建时公司 | | CREATE_DEPT_ID / CREATE_DEPT_NAME | | 创建时部门 | | CREATE_POST_ID / CREATE_POST_NAME | | 创建时岗位 | | CREATE_USER_NAME | VARCHAR2(100) | 创建人姓名 | | CUR_COMPANY_ID / CUR_DEPT_ID | NUMBER(19) | 当前公司/部门(数据权限) | 填充由平台 **`MetaObjectHandler`** / 会话上下文自动写入,业务代码**禁止**在子类重复声明上述字段。 #### 持久层实体(强制) - **所有**映射数据库表的实体类 **`extends BaseEntity`**(含主表、明细表、子表,如 `PurchaseApplyList`、`GiveBackMeter`)。 - 主键仍在子类声明:`@TableId(value = "ID", type = IdType.ASSIGN_ID) Long id`。 - **禁止**在子类再次声明 `createUser`、`createTime`、`updateUser`、`updateTime` 及 `createCompanyId`~`curDeptId` 等 BaseEntity 已映射字段(避免覆盖、类型不一致,如 `BigDecimal updateUser`)。 - Lombok:`@Data`;子类可用 `@EqualsAndHashCode(callSuper = true)`(按需);`@FieldDefaults(level = PRIVATE)`;业务列 **`@TableField` 大写列名**。 #### 逻辑删除(`deleted` / `Boolean`) | 层 | 约定 | |----|------| | 数据库 | 列名 **`DELETED`**,`NUMBER(1) DEFAULT 0`(0 未删,1 已删) | | Java 实体 | 属性名 **`deleted`**,类型 **`Boolean`**(**禁止** `Integer`/`Number`/`is_delete`/`del_flag`) | | 映射 | `@TableField("DELETED")` | | MyBatis-Plus | 须逻辑删除时加 **`@TableLogic`**(参考 `PurchasePlan` 存量写法) | - 继承 **`BaseEntity`** 且父类已提供 **`Boolean deleted`** 时,**子类禁止重复声明**。 - 父类无 `deleted` 时,在实体中显式声明(默认值 `false`): ```java @TableLogic @TableField("DELETED") @ApiModelProperty("删除状态") Boolean deleted = false; ``` - 查询默认过滤已删除数据:依赖 `@TableLogic` 或 Wrapper 自动带 `DELETED = 0`;手写 SQL/XML 须显式 `DELETED = 0`(或布尔映射等价条件)。 #### 附件(`FileEntry` / `FileEntryList`) | 场景 | Java 类型 | 说明 | |------|-----------|------| | 多个附件 | **`com.tofly.entity.pojo.FileEntryList`** | 入库、到货、领用等常见,列名如 `ATTACH` | | 单个附件 | **`com.tofly.entity.pojo.FileEntry`** | 仅一个文件时使用 | ```java @TableField("ATTACH") @ApiModelProperty("附件") FileEntryList attach; ``` - **禁止**用 `String` 存逗号分隔文件 ID、`List` 裸 ID、JSON 字符串等替代(与公共文件服务对接由 `FileEntry*` 类型处理器完成)。 - DTO/Vo 与 Entity 保持一致:接收、返回附件均用 **`FileEntryList` 或 `FileEntry`**。 - 参考:`PurchasePlanArr.attach`、`PurchaseClaim.attach`。 #### 自检(建表 / 新建实体) - [ ] DDL 已包含上表全部公共列及 `COMMENT ON COLUMN` - [ ] 实体 `extends BaseEntity`,且无重复审计字段(`deleted` 若父类已有则不重复) - [ ] 逻辑删除为 **`Boolean deleted`** + 列 **`DELETED`**,需要时含 `@TableLogic` - [ ] 附件字段为 **`FileEntry` / `FileEntryList`**,非字符串拼 ID - [ ] 明细表与主表均具备相同公共列(非「纯关系表」例外须在表注释说明) ### 实体与值对象(部门 / 用户 / 字典) - 主键 `@TableId(value = "ID", type = IdType.ASSIGN_ID)`(雪花)。 - Lombok:`@Accessors(chain = true)`(按需);字段 **`@TableField` 大写列名**。 #### 部门 / 公司(OrganizationEntry) - **仅持久层实体、Vo** 使用 **`OrganizationEntry`** 存部门/公司(含 id、name 映射),**禁止**裸 `Long deptId` + `String deptName` 双字段落库。 - **`Query` / `*LedgerQuery` 中禁止使用 `OrganizationEntry`**:按部门/公司筛选时用 **`Long`**(如 `demandCompany`、`applyDeptId`),按名称模糊筛选用 **`String`**(如 `deptName`,按需);与 `PurchasePlanQuery.demandCompany` 一致。 - **新增/修改赋值(Entity)**:`OrganizationEntry.of(部门或公司ID)`;会话上下文可用 `ApplicationSession.getDepartmentId()` / `getCompanyId()`。 - **Service 条件拼装**:Query 的 `Long` 与实体 `OrganizationEntry` 字段对接,例如 `.eq(Objects.nonNull(q.getDemandCompany()), Entity::getDemandCompany, q.getDemandCompany())`(见 `PurchasePlanServiceImpl`)。 #### 用户(UserNameEntry) - **Entity、Vo** 上申请人、入库人等:**`UserNameEntry`**,**禁止**仅 `Long userId` 落库。 - **Query 中不使用 `UserNameEntry`**;按用户筛选用 **`Long userId`** 或 **`String` 用户名**(如 `inboundUser`)。 - **赋值**:`UserNameEntry.of(ApplicationSession.getUserId())` 或业务传入的用户 ID。 #### 字典(DictEntry + @DictDirectory) - 字典项字段类型 **`DictEntry`**;字段上必须标注字典目录: ```java @DictDirectory(MeterConstant.DICT_METER_JJCD) // 字典目录码,定义在 MeterConstant @TableField("PRIORITY") @ApiModelProperty("紧急程度") DictEntry priority; ``` - **目录常量**集中在 **`com.tofly.wm.cons.MeterConstant`**,例如: - `DICT_METER_JJCD` — 紧急程度 - `DICT_METER_TYPE` — 水表类型 - `DICT_METER_DIAMETER` — 口径 - `DICT_WORKER_ORDER` — 工单类型 - 新建字典目录时:**先在 `MeterConstant` 增加常量** → 再在实体/Vo 上使用 `@DictDirectory`。 - 按字典编码写死业务分支时:`DictEntry.of(MeterConstant.PURCHASE_APPLY)` 等(见 `PurchaseApplyServiceImpl`)。 - Query 中若仅按字典 code 筛选,可用 `String` + `eq`;**回显/入库**仍用实体上的 **`DictEntry`**。 #### 业务枚举(非字典) - 状态、出入库类型等:**实现 `Enumerable`**,`getKey()` / `getLabel()`,放 **`{domain}/enums/`**。 ### 注释规范 | 层级 | 要求 | |------|------| | **类** | JavaDoc:中文业务说明;`@author`、`@date`(与 ledger 存量一致) | | **public 方法(Service)** | JavaDoc:`@param`、`@return`;说明业务含义,非复述方法名 | | **关键代码** | 仅对**非显而易见**逻辑:状态流转、双写出入库、补偿、Wrappers 中每组条件的业务含义(参考 `MeterServiceImpl.list` 行尾 `//总公司/分公司`) | | **禁止** | 无信息注释(`// 查询`、`// 保存`);大量注释掩盖坏命名 | 示例(Service 条件注释,摘自 meter): ```java .eq(Objects.nonNull(q.getCompanyType()), Meter::getCompanyType, Optional.ofNullable(q.getCompanyType()).map(CompanyTypeEnum::getKey).orElse(null)) //出厂编码 .like(StrUtil.isNotBlank(q.getFactoryCode()), Meter::getFactoryCode, q.getFactoryCode()) ``` ### Swagger 与日志 - 类 `@Api(tags)`;方法 `@ApiOperation`;路径参数 `@ApiParam`。 - 写操作按 **`java-tf-ylsw-backend`** 补 `@ToFlyAppLog`(ledger/meter 存量未全覆盖时,新增写接口应补齐)。 ## 生成步骤 1. **识别域与参考类** - 标准 CRUD → `ledger/meter`(`MeterController` ~ `MeterMapper`)。 - 只读台账 + 导出 → `ledger/purchase` 或 `ledger/verify`。 2. **生成 Query / Vo**(分页查询、导出列)。 3. **生成 Entity + Enum**(表结构对齐 `TF_WM_*`)。 4. **生成 Mapper + Service(Impl)**(单表 Wrappers,多表 MPJJoin)。 5. **生成 Controller**(`ResultResponse`、权限、路由)。 6. **按需生成 Mapper.xml**(仅复杂 SQL)。 7. **自检**(见下)。 ## 输出格式 ```markdown ## CRUD 结果(ylsw-bw) - 域包:`com.tofly.wm.ledger.{domain}` - 参考模板:`ledger/meter` | `ledger/purchase` | ... - 新增/修改文件:`...` ## 接口清单 - `GET /{resource}/page` - `GET /{resource}/list` - `DELETE /{resource}/deleteByIds` - ... ## 规范检查 - [x] ResultResponse 带泛型 - [x] 包路径符合 ledger/manage 职责划分 - [x] 字符串用 StrUtil,对象空值用 Objects.nonNull/isNull - [x] Entity/Vo 使用 OrganizationEntry、UserNameEntry、DictEntry + @DictDirectory;Query 不用 Entry 对象 - [x] 字典目录常量来自 MeterConstant - [x] 类/方法/关键逻辑注释符合规范 - [x] 分页使用 PageHelper + PageQuery - [x] 简单查询用 Wrappers / MPJ,复杂 SQL 在 XML - [x] Mapper 无注解 SQL - [x] 表含 BaseEntity 全部公共列;实体 `extends BaseEntity` 且无重复审计字段 - [x] `Boolean deleted` + `DELETED` 列;附件为 `FileEntry` / `FileEntryList` ``` ## 参考 - 模板代码见 [examples-ylsw-bw.md](examples-ylsw-bw.md) - 仓库实现:`business/water-meter/src/main/java/com/tofly/wm/ledger/meter/`