feat(cost): 扩展费用类型并优化采购订单成本管理

- 新增费用类型:工头垫付和产地垫付选项
- 在OrderCost实体中增加requireQuantityAndPrice字段
- 为OrderSupplier添加产品ID和产品名称字段
- 更新CostItemGatewayImpl中的查询逻辑以支持新字段
- 优化PurchaseOrderGatewayImpl中的费用信息处理逻辑
- 在ApplicationConfigurer中添加MultipartResolver配置
- 完善OrderCostMapper和OrderSupplierMapper的XML映射
- 移除过时的API添加指南和设计文档
This commit is contained in:
shenyifei 2025-11-17 10:46:21 +08:00
parent 7e58fc8d62
commit 371fb91991
22 changed files with 141 additions and 202 deletions

View File

@ -1,156 +0,0 @@
---
trigger: manual
---
# 新增接口需要添加的组件清单
注意:
不用管 erp-turbo-svc 这个目录的代码
本文档描述了在本项目中添加一个新的API接口时需要修改或创建的组件。
## 1. API层 (接口定义层)
注意这个 biz 可以是其他的名user、goods、order等
### 1.1 DTO定义
- **查询参数类(Query)**
- 路径: `erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/dto/qry/`
- 命名规范: `功能名+CountQry``功能名+ListQry``功能名+PageQry`
- 继承: `Query` 基类
- 注解: `@Data`, `@Schema`, `@EqualsAndHashCode(callSuper = true)`
- **命令类(Command)**
- 路径: `erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/dto/cmd/`
- 命名规范: `功能名+CreateCmd``功能名+UpdateCmd``功能名+DestroyCmd`
- 继承: `Command` 基类
- 注解: `@Data`, `@Schema`, `@EqualsAndHashCode(callSuper = true)`
- **返回值类(Value Object)**
- 路径: `erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/dto/vo/`
- 命名规范: `功能名+VO`
- 注解: `@Data`, `@Schema`
### 1.2 服务接口定义
- **接口文件**
- 路径: `erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/api/`
- 文件命名: `功能名+ServiceI`
- 方法签名: 定义业务方法
## 2. 业务实现层 (Biz层)
### 2.1 服务实现
- **服务实现类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/app/service/`
- 文件命名: `功能名+ServiceImpl`
- 实现接口: 实现对应的ServiceI接口
- 注解: `@Service`, `@DubboService`, `@RequiredArgsConstructor`
- 依赖注入: 通过构造函数注入执行器
### 2.2 查询执行器
- **查询执行器类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/app/executor/query/`
- 文件命名: `功能名+QryExe`
- 注解: `@Component`, `@RequiredArgsConstructor`
- 方法: `execute()`方法处理具体查询逻辑
### 2.3 命令执行器
- **命令执行器类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/app/executor/cmd/`
- 文件命名: `功能名+CmdExe`
- 注解: `@Component`, `@RequiredArgsConstructor`
- 方法: `execute()`方法处理具体命令逻辑
### 2.4 网关接口
- **网关接口**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/domain/gateway/`
- 文件命名: `功能名+Gateway`
- 定义数据访问方法
### 2.5 领域实体
- **领域实体类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/domain/entity/`
- 文件命名: `功能名`
- 注解: `@Data`, `@ToString`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor`
### 2.6 数据实体
- **数据实体类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/infrastructure/entity/`
- 文件命名: `功能名+DO`
- 继承: `BaseDO`
- 注解: `@Data`, `@TableName`, `@EqualsAndHashCode(callSuper = true)`
### 2.7 数据转换器
- **转换器类**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/infrastructure/convert/`
- 文件命名: `功能名+Convert`
- 注解: `@Mapper`
- 方法: 定义DO与领域实体、DO与VO之间的转换方法
### 2.8 数据访问层
- **Mapper接口**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/java/com/xunhong/erp/turbo/biz/infrastructure/mapper/`
- 文件命名: `功能名+Mapper`
- 继承: `BaseMapper<实体类>`
- 注解: `@Mapper`
- **Mapper XML文件**
- 路径: `erp-turbo-business/erp-turbo-biz/src/main/resources/mapper/`
- 文件命名: `功能名+Mapper.xml`
## 3. 控制层 (Controller层)
### 3.1 控制器
- **控制器类**
- 路径: `erp-turbo-admin/src/main/java/com/xunhong/erp/turbo/admin/controller/`
- 文件命名: `功能名+Controller`
- 注解: `@RestController`, `@RequestMapping`, `@RequiredArgsConstructor`
- 方法注解: `@SaCheckLogin`, `@GetMapping`/`@PostMapping`, `@Operation`
- Dubbo引用: `@DubboReference` 注解引入服务接口
## 4. 枚举类 (如有需要)
### 4.1 枚举定义
- **枚举类**
- 路径: `erp-turbo-common/erp-turbo-api/src/main/java/com/xunhong/erp/turbo/api/biz/dto/enums/`
- 文件命名: `功能名+Enum`
- 实现接口: `DictEnum`
- 注解: `@Getter`, `@AllArgsConstructor`
## 5. 数据库变更 (如有需要)
### 5.1 数据库迁移脚本
- **SQL脚本**
- 路径: `db/migration/`
- 文件命名: `V版本号__变更描述.sql`
## 6. 测试用例 (建议)
### 6.1 单元测试
- **测试类**
- 路径: 对应模块的test目录下
- 文件命名: 被测试类+Test
## 7. 接口文档
### 7.1 Swagger注解
- 所有Controller方法都应添加Swagger注解包括:
- `@Tag` - 类级别,描述接口分类
- `@Operation` - 方法级别,描述接口功能
- `@Schema` - 类属性级别,描述字段含义
## 8. 权限控制
### 8.1 权限注解
- 使用`@SaCheckLogin`进行登录检查
- 使用`@SaCheckRole`进行角色检查
- 使用`@SaCheckPermission`进行权限检查(如需要)
## 9. 校验注解
### 9.1 参数校验
- 在Query和Command类的字段上使用校验注解如:
- `@NotNull` - 非空校验
- `@NotBlank` - 非空字符串校验
- `@Min`/`@Max` - 数值范围校验
- 在Controller方法参数中使用`@Validated`注解

View File

@ -1,5 +0,0 @@
---
trigger: manual
---
不用管 erp-turbo-svc 这个目录的代码

View File

@ -23,7 +23,7 @@ public class CostItem extends DTO {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
private CostItemCostTypeEnum costType;
@ -72,4 +72,4 @@ public class CostItem extends DTO {
*/
private BigDecimal sort;
}
}

View File

@ -29,4 +29,4 @@ public interface OrderSupplierConvert {
@Mapping(target = "createdBy", ignore = true)
@Mapping(target = "orderPackageList", source = "orderPackageDOList")
OrderSupplier toOrderSupplier(OrderSupplierDO orderSupplierDO);
}
}

View File

@ -26,7 +26,7 @@ public class CostItemDO extends BaseDO<CostItemDO> {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@TableField(value = "cost_type")
private CostItemCostTypeEnum costType;
@ -79,4 +79,4 @@ public class CostItemDO extends BaseDO<CostItemDO> {
@TableField(value = "sort")
private BigDecimal sort;
}
}

View File

@ -75,7 +75,13 @@ public class OrderCostDO extends BaseDO<OrderCostDO> {
private String principal;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 是否需要填写数量和单价
*/
@TableField(value = "require_quantity_and_price")
private Boolean requireQuantityAndPrice;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@TableField(value = "cost_type")
private CostItemCostTypeEnum costType;

View File

@ -158,7 +158,18 @@ public class OrderSupplierDO extends BaseDO<OrderSupplierDO> {
@TableField(value = "contract_img", typeHandler = JacksonTypeHandler.class)
private List<String> contractImg;
/**
* 产品ID
*/
@TableField(value = "product_id")
private Long productId;
/**
* 产品名称
*/
@TableField(value = "product_name")
private String productName;
@TableField(exist = false)
private List<OrderPackageDO> orderPackageDOList;
}
}

View File

@ -10,7 +10,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -60,11 +59,5 @@ public class ProductDO extends BaseDO<ProductDO> {
@TableField(exist = false)
private List<CostItemDO> costItemDOList;
/**
* 创建时间
*/
@TableField(value = "created_at")
private LocalDateTime createdAt;
}

View File

@ -49,6 +49,7 @@ public class CostItemGatewayImpl implements CostItemGateway {
queryWrapper.like(StrUtil.isNotBlank(costItemPageQry.getName()), CostItemDO::getName, costItemPageQry.getName());
queryWrapper.like(StrUtil.isNotBlank(costItemPageQry.getRemark()), CostItemDO::getRemark, costItemPageQry.getRemark());
queryWrapper.eq(Objects.nonNull(costItemPageQry.getStatus()), CostItemDO::getStatus, costItemPageQry.getStatus());
queryWrapper.eq(Objects.nonNull(costItemPageQry.getShowInEntry()), CostItemDO::getShowInEntry, costItemPageQry.getShowInEntry());
queryWrapper.eq(Objects.nonNull(costItemPageQry.getCostType()), CostItemDO::getCostType, costItemPageQry.getCostType());
queryWrapper.orderByAsc(CostItemDO::getSort);
queryWrapper.orderByDesc(CostItemDO::getCreatedAt);
@ -62,7 +63,7 @@ public class CostItemGatewayImpl implements CostItemGateway {
@Override
public List<CostItem> list(CostItemListQry costItemListQry) {
LambdaQueryWrapper<CostItemDO> queryWrapper = Wrappers.lambdaQuery(CostItemDO.class);
queryWrapper.eq(Objects.nonNull(costItemListQry.getShowInEntry()), CostItemDO::getStatus, costItemListQry.getShowInEntry());
queryWrapper.eq(Objects.nonNull(costItemListQry.getShowInEntry()), CostItemDO::getShowInEntry, costItemListQry.getShowInEntry());
queryWrapper.eq(Objects.nonNull(costItemListQry.getStatus()), CostItemDO::getStatus, costItemListQry.getStatus());
queryWrapper.eq(Objects.nonNull(costItemListQry.getCostType()), CostItemDO::getCostType, costItemListQry.getCostType());
queryWrapper.orderByAsc(CostItemDO::getSort);

View File

@ -106,6 +106,16 @@ public class ProductGatewayImpl implements ProductGateway {
queryWrapper.last("limit 1");
ProductDO productDO = productMapper.selectOne(queryWrapper);
if (Objects.nonNull(productDO)) {
List<Long> costItemIds = productDO.getCostItemIds();
if (CollUtil.isNotEmpty(costItemIds)) {
LambdaQueryWrapper<CostItemDO> queryWrapper1 = Wrappers.lambdaQuery(CostItemDO.class);
queryWrapper1.in(CostItemDO::getItemId, costItemIds);
productDO.setCostItemDOList(costItemMapper.selectList(queryWrapper1));
}
}
return productConvert.toProduct(productDO);
}

View File

@ -10,11 +10,7 @@ import com.xunhong.erp.turbo.api.biz.dto.cmd.*;
import com.xunhong.erp.turbo.api.biz.dto.common.*;
import com.xunhong.erp.turbo.api.biz.dto.enums.CostItemCostTypeEnum;
import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderStateEnum;
import com.xunhong.erp.turbo.api.biz.dto.qry.PurchaseOrderCountQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.LastVehicleNoQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.PurchaseOrderListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.PurchaseOrderPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.PurchaseOrderShowQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.*;
import com.xunhong.erp.turbo.biz.domain.entity.PurchaseOrder;
import com.xunhong.erp.turbo.biz.domain.gateway.PurchaseOrderGateway;
import com.xunhong.erp.turbo.biz.infrastructure.convert.*;
@ -802,6 +798,50 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
// 执行插入操作
suppliersToInsert.forEach(orderSupplierMapper::insert);
if (purchaseOrderStep2Cmd.getActive() == 3) {
// 更新费用信息精细化处理
// 获取现有的费用列表
LambdaQueryWrapper<OrderCostDO> costQueryWrapper = Wrappers.lambdaQuery(OrderCostDO.class);
costQueryWrapper.eq(OrderCostDO::getOrderId, orderId);
List<OrderCostDO> existingCosts = orderCostMapper.selectList(costQueryWrapper);
// 获取更新的费用列表
List<OrderCost> updatedCosts = purchaseOrderStep2Cmd.getOrderCostList().stream().toList();
// 将现有费用映射到ID字典中便于查找
Map<Long, OrderCostDO> existingCostMap = existingCosts.stream()
.collect(Collectors.toMap(OrderCostDO::getOrderCostId, Function.identity()));
// 收集需要更新和新增的费用
List<OrderCostDO> costsToInsert = new ArrayList<>();
List<OrderCostDO> costsToUpdate = new ArrayList<>();
for (OrderCost updatedCost : updatedCosts) {
updatedCost.setOrderId(orderId);
OrderCostDO costDO = orderCostConvert.toOrderCostDO(updatedCost);
if (updatedCost.getOrderCostId() != null && existingCostMap.containsKey(updatedCost.getOrderCostId())) {
// 更新已存在的费用
costDO.setOrderCostId(updatedCost.getOrderCostId());
costsToUpdate.add(costDO);
// 从现有映射中移除剩下的就是需要删除的
existingCostMap.remove(updatedCost.getOrderCostId());
} else {
// 新增费用
costsToInsert.add(costDO);
}
}
// 删除不再需要的费用
existingCostMap.values().forEach(cost -> cost.deleteById());
// 执行更新操作
costsToUpdate.forEach(orderCostMapper::updateById);
// 执行插入操作
costsToInsert.forEach(orderCostMapper::insert);
}
// 第三步处理包材
if (purchaseOrderStep2Cmd.getActive() == 4) {
// 处理包材信息对更新和新增的供应商
@ -887,7 +927,7 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
List<OrderCostDO> existingCosts = orderCostMapper.selectList(costQueryWrapper);
// 获取更新的费用列表
List<OrderCost> updatedCosts = purchaseOrderStep3Cmd.getOrderCostList();
List<OrderCost> updatedCosts = purchaseOrderStep3Cmd.getOrderCostList().stream().toList();
// 将现有费用映射到ID字典中便于查找
Map<Long, OrderCostDO> existingCostMap = existingCosts.stream()

View File

@ -15,6 +15,7 @@
<result property="payerType" column="payer_type"/>
<result property="principal" column="principal"/>
<result property="costType" column="cost_type"/>
<result property="requireQuantityAndPrice" column="require_quantity_and_price"/>
<result property="createdAt" column="created_at"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>

View File

@ -34,6 +34,7 @@
<result property="updatedAt" column="updated_at"/>
<result property="isDelete" column="is_delete"/>
<result property="version" column="version"/>
<result property="productId" column="product_id"/>
<result property="productName" column="product_name"/>
</resultMap>
</mapper>
</mapper>

View File

@ -23,9 +23,9 @@ public class CostItemCreateCmd extends Command {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付", requiredMode = Schema.RequiredMode.REQUIRED)
private CostItemCostTypeEnum costType;
/**
@ -70,4 +70,4 @@ public class CostItemCreateCmd extends Command {
@Schema(title = "状态1_启用0_禁用", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean status;
}
}

View File

@ -1,5 +1,6 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.api.biz.dto.common.OrderCost;
import com.xunhong.erp.turbo.api.biz.dto.common.OrderSupplier;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
@ -33,4 +34,10 @@ public class PurchaseOrderStep2Cmd extends Command {
*/
@Schema(title = "供应商信息", requiredMode = Schema.RequiredMode.REQUIRED)
private List<OrderSupplier> orderSupplierList;
/**
* 费用信息
*/
@Schema(title = "采购订单费用信息", requiredMode = Schema.RequiredMode.REQUIRED)
private List<OrderCost> orderCostList;
}

View File

@ -62,7 +62,7 @@ public class OrderCost extends Command {
/**
* 付款方类型:1-我方,2-对方
*/
@Schema(title = "付款方类型:1-我方,2-对方", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(title = "付款方类型:1-我方,2-对方")
private OrderCostPayerTypeEnum payerType;
/**
@ -72,9 +72,15 @@ public class OrderCost extends Command {
private String principal;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 是否需要数量和价格
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(title = "是否需要数量和价格")
private Boolean requireQuantityAndPrice;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付", requiredMode = Schema.RequiredMode.REQUIRED)
private CostItemCostTypeEnum costType;
}

View File

@ -154,10 +154,21 @@ public class OrderSupplier extends Command {
@Schema(title = "合同")
private List<String> contractImg;
/**
* 产品ID
*/
@Schema(title = "产品ID", type = "string")
private Long productId;
/**
* 产品名称
*/
@Schema(title = "产品名称")
private String productName;
/**
* 采购订单包装箱信息
*/
@Schema(title = "采购订单包装箱信息")
private List<OrderPackage> orderPackageList;
}
}

View File

@ -8,7 +8,7 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum CostItemCostTypeEnum {
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付5_工头垫付6_产地垫付
*/
PACKAGING_MATERIALS(1, "包装材料"),
HUMAN_COST(2, "人工费用"),

View File

@ -18,9 +18,9 @@ public class CostItemListQry extends Query {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用")
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付")
private CostItemCostTypeEnum costType;
/**

View File

@ -18,10 +18,15 @@ public class CostItemPageQry extends PageQuery {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用")
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付")
private CostItemCostTypeEnum costType;
/**
* 是否在录入时显示
*/
@Schema(title = "是否在录入时显示")
private Boolean showInEntry;
/**
* 项目名称

View File

@ -24,9 +24,9 @@ public class CostItemVO extends DTO {
private Long itemId;
/**
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用
* 费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付
*/
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(title = "费用类型1_包装材料2_人工费用3_其他费用4_固定费用5_工头垫付6_产地垫付", requiredMode = Schema.RequiredMode.REQUIRED)
private CostItemCostTypeEnum costType;
/**
@ -77,4 +77,4 @@ public class CostItemVO extends DTO {
@Schema(title = "创建时间")
private LocalDateTime createdAt;
}
}

View File

@ -26,6 +26,8 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -83,6 +85,12 @@ public class ApplicationConfigurer implements WebMvcConfigurer {
return new CacheControlHandlerInterceptor();
}
// 添加MultipartResolver bean
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()).addPathPatterns("/**");
@ -157,4 +165,4 @@ public class ApplicationConfigurer implements WebMvcConfigurer {
return validator;
}
}
}