feat(reconciliation): 添加对账完成功能

- 新增 ReconciliationCompleteCmd DTO用于对账完成请求
- 在 ReconciliationServiceI 中添加 complete 方法定义
- 在 ReconciliationGateway 中添加 complete 方法定义
- 实现 Gateway 层 complete 方法,包含状态校验逻辑
- 添加 ReconciliationCompleteCmdExe 执行器处理业务逻辑
- 在 ReconciliationServiceImpl 中注入并实现 complete 方法
- 在 Controller 中添加 completeReconciliation 接口
- 添加 B_BIZ_RECONCILIATION_NOT_PENDING 业务异常码
- 修复 ReconciliationGatewayImpl 中的插入顺序问题
This commit is contained in:
shenyifei 2026-01-12 18:00:36 +08:00
parent e677ecde66
commit a66ca25cef
9 changed files with 406 additions and 5 deletions

View File

@ -0,0 +1,313 @@
---
name: "add-state-transition"
description: "为实体添加状态流转接口(如:完成、取消等)。遵循 ERPTurbo 项目的 DDD 分层架构规范。"
---
# Add State Transition
为实体添加状态流转接口,遵循 DDD 分层架构规范。
## ⚠️ 重要规则
**仅操作 `erp-turbo-business``erp-turbo-admin` 模块,禁止修改 `erp-turbo-svc` 模块!**
详见:`.claude/PROJECT_RULES.md`
## 功能说明
为实体添加状态流转接口,例如:订单完成、对账取消、审核通过等。
## 输入格式
```
Entity: {实体名称}
Action: {操作名称}
TargetState: {目标状态值}
Comment: {操作中文描述}
```
**示例:**
```
Entity: Reconciliation
Action: Complete
TargetState: RECONCILED
Comment: 对账完成
```
## 工作流程
### 1. 创建 Cmd DTO
路径:`erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/dto/cmd/`
**文件名:** `{Entity}{Action}Cmd.java`
```java
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "{Comment}")
@EqualsAndHashCode(callSuper = true)
public class {Entity}{Action}Cmd extends Command {
@Schema(title = "{EntityComment}ID", requiredMode = Schema.RequiredMode.REQUIRED, type = "string")
private Long {entityFieldName}Id;
}
```
### 2. 更新 Service 接口
路径:`erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/api/{Entity}ServiceI.java`
添加方法签名:
```java
{Entity}VO {action}({Entity}{Action}Cmd {entity}{Action}Cmd);
```
### 3. 更新 Gateway 接口
路径:`erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/domain/gateway/{Entity}Gateway.java`
添加方法签名:
```java
{Entity} {action}({Entity}{Action}Cmd {entity}{Action}Cmd);
```
### 4. 实现 Gateway
路径:`erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/infrastructure/gateway/{Entity}GatewayImpl.java`
添加 import
```java
import com.xunhong.erp.turbo.api.biz.dto.cmd.{Entity}{Action}Cmd;
import com.xunhong.erp.turbo.api.biz.dto.enums.{Entity}StateEnum;
```
实现方法:
```java
@Override
public {Entity} {action}({Entity}{Action}Cmd cmd) {
LambdaQueryWrapper<{Entity}DO> queryWrapper = Wrappers.lambdaQuery({Entity}DO.class);
queryWrapper.eq({Entity}DO::get{Entity}Id, cmd.get{Entity}Id());
queryWrapper.select({Entity}DO::get{Entity}Id, {Entity}DO::getState);
queryWrapper.last("limit 1");
{Entity}DO {entity}DO = {entity}Mapper.selectOne(queryWrapper);
if (!{entity}DO.getState().equals({Entity}StateEnum.{CURRENT_STATE})) {
throw new BizException(BizErrorCode.B_BIZ_{ENTITY_UPPER}_NOT_{CURRENT_STATE_UPPER});
}
{entity}DO.setState({Entity}StateEnum.{TARGET_STATE});
{entity}Mapper.updateById({entity}DO);
return {entity}Convert.to{Entity}({entity}DO);
}
```
### 5. 创建 Executor
路径:`erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/app/executor/cmd/{Entity}{Action}CmdExe.java`
```java
package com.xunhong.erp.turbo.biz.app.executor.cmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.{Entity}{Action}Cmd;
import com.xunhong.erp.turbo.api.biz.dto.vo.{Entity}VO;
import com.xunhong.erp.turbo.biz.app.assembler.{Entity}Assembler;
import com.xunhong.erp.turbo.biz.domain.gateway.{Entity}Gateway;
import com.xunhong.erp.turbo.biz.domain.entity.{Entity};
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class {Entity}{Action}CmdExe {
private final {Entity}Gateway {entity}Gateway;
private final {Entity}Assembler {entity}Assembler;
public {Entity}VO execute({Entity}{Action}Cmd cmd) {
{Entity} {entity} = {entity}Gateway.{action}(cmd);
return {entity}Assembler.to{Entity}VO({entity});
}
}
```
### 6. 更新 ServiceImpl
路径:`erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/app/service/{Entity}ServiceImpl.java`
添加字段注入:
```java
private final {Entity}{Action}CmdExe {entity}{Action}CmdExe;
```
添加方法实现:
```java
@Override
public {Entity}VO {action}({Entity}{Action}Cmd cmd) {
return {entity}{Action}CmdExe.execute(cmd);
}
```
### 7. 更新 Controller
路径:`erp-turbo-admin/src/main/java/com/xunhong/erp/turbo/admin/controller/{Entity}Controller.java`
添加 import
```java
import com.xunhong.erp.turbo.api.biz.dto.cmd.{Entity}{Action}Cmd;
```
添加接口方法:
```java
@SaCheckLogin
@PostMapping("{action}{Entity}")
@Operation(summary = "{Comment}", method = "POST")
public SingleResponse<{Entity}VO> {action}{Entity}(
@RequestBody @Validated {Entity}{Action}Cmd cmd) {
return SingleResponse.of({entity}Service.{action}(cmd));
}
```
## 命名规范
| 类型 | 格式 | 示例 |
|-----|------|-----|
| 实体名 | PascalCase | Reconciliation, Order |
| 操作名 | PascalCase | Complete, Cancel, Approve |
| 方法名 | camelCase | complete, cancel, approve |
| 字段名 | camelCase | reconciliationId, orderId |
| 常量 | UPPER_SNAKE_CASE | RECONCILIATION, ORDER |
## 使用示例
### 示例 1对账完成
```
Entity: Reconciliation
Action: Complete
TargetState: RECONCILED
Comment: 对账完成
CurrentState: PENDING
```
生成接口:`POST /operation/completeReconciliation`
### 示例 2订单取消
```
Entity: Order
Action: Cancel
TargetState: CANCELLED
Comment: 取消订单
CurrentState: PENDING
```
生成接口:`POST /operation/cancelOrder`
### 示例 3审核通过
```
Entity: PaymentTask
Action: Approve
TargetState: APPROVED
Comment: 审核通过
CurrentState: PENDING_APPROVAL
```
生成接口:`POST /operation/approvePaymentTask`
## 输出示例
```
✅ 已完成状态流转接口添加
========================================
创建文件:
✅ api/.../cmd/{Entity}{Action}Cmd.java (erp-turbo-common)
✅ biz/.../executor/cmd/{Entity}{Action}CmdExe.java (erp-turbo-business)
更新文件:
✅ api/.../api/{Entity}ServiceI.java (erp-turbo-common)
✅ biz/.../gateway/{Entity}Gateway.java (erp-turbo-business)
✅ biz/.../gateway/{Entity}GatewayImpl.java (erp-turbo-business)
✅ biz/.../service/{Entity}ServiceImpl.java (erp-turbo-business)
✅ admin/.../controller/{Entity}Controller.java (erp-turbo-admin)
接口路径: POST /{basePath}/{action}{Entity}
请求体: {"{entityFieldName}Id": 123}
状态流转: {CURRENT_STATE} → {TARGET_STATE}
```
## 注意事项
1. **状态验证**
- Gateway 实现中应验证当前状态
- 使用 `BizException` 抛出业务异常
- 错误码格式:`B_BIZ_{ENTITY}_NOT_{STATE}`
2. **查询优化**
- 使用 `queryWrapper.select()` 只查询必要字段
- 减少数据传输和内存占用
3. **事务处理**
- 根据需要添加 `@Transactional` 注解
- 涉及多表操作时务必使用事务
4. **接口一致性**
- 使用 `@PostMapping`
- 使用 `@RequestBody @Validated` 接收参数
- 返回类型统一为 `SingleResponse<{Entity}VO>`
5. **权限控制**
- 所有接口添加 `@SaCheckLogin` 注解
- 根据需要添加其他权限验证
6. **Assemble 方法**
- 使用 `to{Entity}VO()` 而非 `toVO()`
- 保持命名一致性
## 文件搜索规则
使用 Glob 工具时,**限定在特定模块**
```bash
# ✅ 正确:限定路径
erp-turbo-business/**/{Entity}*.java
erp-turbo-common/**/{Entity}*.java
erp-turbo-admin/**/{Entity}*.java
# ❌ 错误:会匹配到 svc 模块
**/*{Entity}*.java
```
## 常见状态流转模式
| 业务场景 | 当前状态 | 目标状态 | 操作名 |
|---------|---------|---------|--------|
| 对账完成 | PENDING | RECONCILED | Complete |
| 部分开票 | RECONCILED | PARTIAL_INVOICE | PartialInvoice |
| 完成开票 | PARTIAL_INVOICE | INVOICED | Invoice |
| 部分回款 | INVOICED | PARTIAL_PAYMENT | PartialPayment |
| 完成回款 | PARTIAL_PAYMENT | PAID | Payment |
| 订单取消 | PENDING | CANCELLED | Cancel |
| 审核通过 | PENDING | APPROVED | Approve |
| 审核驳回 | PENDING | REJECTED | Reject |

View File

@ -6,6 +6,7 @@ import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
import com.xunhong.erp.turbo.api.biz.api.ReconciliationServiceI;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationUpdateCmd;
@ -69,6 +70,13 @@ public class ReconciliationController {
return SingleResponse.of(reconciliationService.update(reconciliationUpdateCmd));
}
@SaCheckLogin
@PostMapping("completeReconciliation")
@Operation(summary = "对账完成", method = "POST")
public SingleResponse<ReconciliationVO> completeReconciliation(@RequestBody @Validated ReconciliationCompleteCmd reconciliationCompleteCmd) {
return SingleResponse.of(reconciliationService.complete(reconciliationCompleteCmd));
}
@SaCheckLogin
@DeleteMapping("destroyReconciliation")
@Operation(summary = "对账删除", method = "DELETE")

View File

@ -0,0 +1,26 @@
package com.xunhong.erp.turbo.biz.app.executor.cmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.vo.ReconciliationVO;
import com.xunhong.erp.turbo.biz.app.assembler.ReconciliationAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Reconciliation;
import com.xunhong.erp.turbo.biz.domain.gateway.ReconciliationGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ReconciliationCompleteCmdExe {
private final ReconciliationGateway reconciliationGateway;
private final ReconciliationAssembler reconciliationAssembler;
public ReconciliationVO execute(ReconciliationCompleteCmd reconciliationCompleteCmd) {
Reconciliation reconciliation = reconciliationGateway.complete(reconciliationCompleteCmd);
return reconciliationAssembler.toReconciliationVO(reconciliation);
}
}

View File

@ -1,6 +1,7 @@
package com.xunhong.erp.turbo.biz.app.service;
import com.xunhong.erp.turbo.api.biz.api.ReconciliationServiceI;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationUpdateCmd;
@ -9,6 +10,7 @@ import com.xunhong.erp.turbo.api.biz.dto.qry.ReconciliationPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ReconciliationShowQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ReconciliationVO;
import com.xunhong.erp.turbo.base.dto.PageDTO;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ReconciliationCompleteCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ReconciliationCreateCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ReconciliationDestroyCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ReconciliationUpdateCmdExe;
@ -36,6 +38,7 @@ public class ReconciliationServiceImpl implements ReconciliationServiceI {
private final ReconciliationPageQryExe reconciliationPageQryExe;
private final ReconciliationListQryExe reconciliationListQryExe;
private final ReconciliationShowQryExe reconciliationShowQryExe;
private final ReconciliationCompleteCmdExe reconciliationCompleteCmdExe;
private final ReconciliationDestroyCmdExe reconciliationDestroyCmdExe;
@Override
@ -63,6 +66,11 @@ public class ReconciliationServiceImpl implements ReconciliationServiceI {
return reconciliationShowQryExe.execute(reconciliationShowQry);
}
@Override
public ReconciliationVO complete(ReconciliationCompleteCmd reconciliationCompleteCmd) {
return reconciliationCompleteCmdExe.execute(reconciliationCompleteCmd);
}
@Override
public void destroy(ReconciliationDestroyCmd reconciliationDestroyCmd) {
reconciliationDestroyCmdExe.execute(reconciliationDestroyCmd);

View File

@ -1,6 +1,7 @@
package com.xunhong.erp.turbo.biz.domain.gateway;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationUpdateCmd;
@ -25,6 +26,8 @@ public interface ReconciliationGateway {
Reconciliation show(ReconciliationShowQry reconciliationShowQry);
Reconciliation complete(ReconciliationCompleteCmd reconciliationCompleteCmd);
void destroy(ReconciliationDestroyCmd reconciliationDestroyCmd);
}

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationUpdateCmd;
@ -13,6 +14,8 @@ import com.xunhong.erp.turbo.api.biz.dto.enums.ReconciliationStateEnum;
import com.xunhong.erp.turbo.api.biz.dto.qry.ReconciliationListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ReconciliationPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ReconciliationShowQry;
import com.xunhong.erp.turbo.api.biz.exception.BizErrorCode;
import com.xunhong.erp.turbo.api.biz.exception.BizException;
import com.xunhong.erp.turbo.biz.domain.entity.Reconciliation;
import com.xunhong.erp.turbo.biz.domain.gateway.ReconciliationGateway;
import com.xunhong.erp.turbo.biz.infrastructure.convert.ReconciliationConvert;
@ -60,6 +63,8 @@ public class ReconciliationGatewayImpl implements ReconciliationGateway {
reconciliationDO.setReconciliationSn(reconciliationDO.generateReconciliationSn());
reconciliationDO.setState(ReconciliationStateEnum.PENDING);
reconciliationMapper.insert(reconciliationDO);
List<ReconciliationItemDO> reconciliationItemList = reconciliationItemConvert.toReconciliationItemList(reconciliationCreateCmd.getOrderShipVOList());
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
@ -71,10 +76,6 @@ public class ReconciliationGatewayImpl implements ReconciliationGateway {
sqlSession.commit();
sqlSession.close();
reconciliationMapper.insert(reconciliationDO);
return reconciliationConvert.toReconciliation(reconciliationDO);
}
@ -154,6 +155,25 @@ public class ReconciliationGatewayImpl implements ReconciliationGateway {
return reconciliationConvert.toReconciliation(reconciliationDO);
}
@Override
public Reconciliation complete(ReconciliationCompleteCmd reconciliationCompleteCmd) {
LambdaQueryWrapper<ReconciliationDO> queryWrapper = Wrappers.lambdaQuery(ReconciliationDO.class);
queryWrapper.eq(ReconciliationDO::getReconciliationId, reconciliationCompleteCmd.getReconciliationId());
queryWrapper.select(ReconciliationDO::getReconciliationId, ReconciliationDO::getState);
queryWrapper.last("limit 1");
ReconciliationDO reconciliationDO = reconciliationMapper.selectOne(queryWrapper);
if (!reconciliationDO.getState().equals(ReconciliationStateEnum.PENDING)) {
throw new BizException(BizErrorCode.B_BIZ_RECONCILIATION_NOT_PENDING);
}
reconciliationDO.setState(ReconciliationStateEnum.RECONCILED);
reconciliationMapper.updateById(reconciliationDO);
return reconciliationConvert.toReconciliation(reconciliationDO);
}
@Override
public void destroy(ReconciliationDestroyCmd reconciliationDestroyCmd) {
LambdaQueryWrapper<ReconciliationDO> queryWrapper = Wrappers.lambdaQuery(ReconciliationDO.class);

View File

@ -1,5 +1,6 @@
package com.xunhong.erp.turbo.api.biz.api;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCompleteCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ReconciliationUpdateCmd;
@ -25,6 +26,8 @@ public interface ReconciliationServiceI {
ReconciliationVO show(ReconciliationShowQry reconciliationShowQry);
ReconciliationVO complete(ReconciliationCompleteCmd reconciliationCompleteCmd);
void destroy(ReconciliationDestroyCmd reconciliationDestroyCmd);
}

View File

@ -0,0 +1,18 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "对账完成")
@EqualsAndHashCode(callSuper = true)
public class ReconciliationCompleteCmd extends Command {
@Schema(title = "对账ID", requiredMode = Schema.RequiredMode.REQUIRED, type = "string")
private Long reconciliationId;
}

View File

@ -45,7 +45,9 @@ public enum BizErrorCode implements ErrorCode {
/**
* 费用不存在
*/
B_BIZ_COST_NOT_FOUND("B_BIZ_COST_NOT_FOUND", "费用不存在");;
B_BIZ_COST_NOT_FOUND("B_BIZ_COST_NOT_FOUND", "费用不存在"),
B_BIZ_RECONCILIATION_NOT_PENDING("B_BIZ_RECONCILIATION_NOT_PENDING", "对账状态不是待对账,无法处理");;
private final String code;