feat(quotation): 添加随机配件和交机服务功能

- 在PDF模板中添加表格样式和随机配件、交机服务表格展示
- 新增备件总价字段到购物车配件实体类
- 设置购物车VO默认系数为BigDecimal.ONE
- 调整价格计算逻辑,添加折扣率处理
- 保存购物车数据到数据库
- 添加币种和汇率一致性校验
- 更新PDF导出功能的文档设置方法
- 为配件和服务请求类添加数据验证注解
- 设置配件费和服务费默认值为零
- 修改币种验证从NotBlank为NotNull
- 为配件和服务列表添加@Valid注解进行嵌套验证
This commit is contained in:
曹鹏飞 2026-03-17 10:15:48 +08:00
parent 9a5fa93957
commit 531b3b2932
7 changed files with 95 additions and 27 deletions

View File

@ -173,20 +173,22 @@ public class ShoppingController extends ControllerBase {
.setTotalFee(modelPrice.getAmount()) .setTotalFee(modelPrice.getAmount())
.setActualFee(modelPrice.getAmount()); .setActualFee(modelPrice.getAmount());
log.debug("机型【{}】售价为{}", request.getModelNo(), modelPrice); log.debug("机型【{}】售价为{}", request.getModelNo(), modelPrice);
//系数
Pair<BigDecimal, BigDecimal> pair = getRatio(request.getModelId());
BigDecimal standardRatio = pair.getLeft(), optionalRatio = pair.getRight(), discountRatio = BigDecimal.ONE;
log.debug("机型【{}】标准配件系数为{},可选配件系数为{}", request.getModelNo(), standardRatio, optionalRatio);
if (AppUserUtil.isAgent()) { if (AppUserUtil.isAgent()) {
//代理商 //代理商
QuotationDiscountDTO discountDTO = discountService.getEffectiveForCustomer(request.getModelId(), request.getTargetId(), categoryId); QuotationDiscountDTO discountDTO = discountService.getEffectiveForCustomer(request.getModelId(), request.getTargetId(), categoryId);
if (Objects.nonNull(discountDTO)) { if (Objects.nonNull(discountDTO)) {
vo.setDiscountId(discountDTO.getDiscountId()); vo.setDiscountId(discountDTO.getDiscountId());
vo.setActualFee(vo.getTotalFee().multiply(discountDTO.getRatio())); // vo.setActualFee(vo.getTotalFee().multiply(discountDTO.getRatio()));
vo.setDiscount(vo.getTotalFee().subtract(vo.getActualFee())); // vo.setDiscount(vo.getTotalFee().subtract(vo.getActualFee()));
log.debug("机型【{}】打折后价格为{},优惠{}", request.getModelNo(), vo.getActualFee(), vo.getDiscount()); // log.debug("机型【{}】打折后价格为{},优惠{}", request.getModelNo(), vo.getActualFee(), vo.getDiscount());
discountRatio = discountDTO.getRatio();
log.debug("机型【{}】折扣为{}", request.getModelNo(), discountRatio);
} }
} }
//系数
Pair<BigDecimal, BigDecimal> pair = getRatio(request.getModelId());
BigDecimal standardRatio = pair.getLeft(), optionalRatio = pair.getRight();
log.debug("机型【{}】标准配件系数为{},可选配件系数为{}", request.getModelNo(), standardRatio, optionalRatio);
if (!request.getShowLowestPrice()) { if (!request.getShowLowestPrice()) {
//方案 //方案
QuotationUserPlanModelItem planModelItem = userPlanModelItemService.getEffectiveForUser(request.getModelId(), AppUserUtil.isAgent() ? 1 : 0, AppUserUtil.getUserId()); QuotationUserPlanModelItem planModelItem = userPlanModelItemService.getEffectiveForUser(request.getModelId(), AppUserUtil.isAgent() ? 1 : 0, AppUserUtil.getUserId());
@ -198,13 +200,16 @@ public class ShoppingController extends ControllerBase {
} else { } else {
QuotationUserPlanDefault userPlanDefault = userPlanDefaultService.getEffectiveForUser(AppUserUtil.isAgent() ? 1 : 0, AppUserUtil.getUserId()); QuotationUserPlanDefault userPlanDefault = userPlanDefaultService.getEffectiveForUser(AppUserUtil.isAgent() ? 1 : 0, AppUserUtil.getUserId());
if (Objects.nonNull(userPlanDefault)) { if (Objects.nonNull(userPlanDefault)) {
vo.setDefaultRatio(userPlanDefault.getRatio());
log.debug("用户方案默认系数为{}", userPlanDefault.getRatio()); log.debug("用户方案默认系数为{}", userPlanDefault.getRatio());
standardRatio = NumberUtil.multiply(standardRatio, userPlanDefault.getRatio()); standardRatio = NumberUtil.multiply(standardRatio, userPlanDefault.getRatio());
log.debug("机型【{}】标准配件系数为{},可选配件系数为{}", request.getModelNo(), standardRatio, optionalRatio); log.debug("机型【{}】标准配件系数为{},可选配件系数为{}", request.getModelNo(), standardRatio, optionalRatio);
} }
} }
} }
vo.setActualFee(vo.getTotalFee().subtract(vo.getDiscount()).multiply(standardRatio)); vo.setTotalFee(NumberUtil.multiply(vo.getTotalFee(), standardRatio));
vo.setActualFee(NumberUtil.multiply(vo.getTotalFee(), discountRatio));
vo.setDiscount(vo.getTotalFee().subtract(vo.getActualFee()));
log.debug("机型【{}】价格为{},优惠{}", request.getModelNo(), vo.getActualFee(), vo.getDiscount()); log.debug("机型【{}】价格为{},优惠{}", request.getModelNo(), vo.getActualFee(), vo.getDiscount());
//获取部件配置 //获取部件配置
List<ModelConfigEffectiveDTO> parts = modelConfigService.getEffectives(modelPrice.getPriceId(), categoryId, MultilingualUtil.getLanguage()); List<ModelConfigEffectiveDTO> parts = modelConfigService.getEffectives(modelPrice.getPriceId(), categoryId, MultilingualUtil.getLanguage());
@ -305,6 +310,7 @@ public class ShoppingController extends ControllerBase {
).collect(Collectors.toList()) ).collect(Collectors.toList())
); );
} }
shoppingCartService.save(cart);
return ApiResult.success(cartId); return ApiResult.success(cartId);
} }
@ -368,6 +374,10 @@ public class ShoppingController extends ControllerBase {
.throwMessage("客户名称不一致"); .throwMessage("客户名称不一致");
VUtils.trueThrowBusinessError(carts.stream().map(QuotationShoppingCart::getTargetId).collect(Collectors.toSet()).size() > 1) VUtils.trueThrowBusinessError(carts.stream().map(QuotationShoppingCart::getTargetId).collect(Collectors.toSet()).size() > 1)
.throwMessage("报价对象不一致"); .throwMessage("报价对象不一致");
VUtils.trueThrowBusinessError(carts.stream().map(QuotationShoppingCart::getCurrency).collect(Collectors.toSet()).size() > 1)
.throwMessage("币种不一致");
VUtils.trueThrowBusinessError(carts.stream().map(QuotationShoppingCart::getExchangeRate).collect(Collectors.toSet()).size() > 1)
.throwMessage("汇率不一致");
QuotationShoppingOrder order = new QuotationShoppingOrder() QuotationShoppingOrder order = new QuotationShoppingOrder()
.setCreateByType(AppUserUtil.isAgent() ? 1 : 0) .setCreateByType(AppUserUtil.isAgent() ? 1 : 0)
.setCreateBy(AppUserUtil.getUserName()) .setCreateBy(AppUserUtil.getUserName())
@ -538,23 +548,22 @@ public class ShoppingController extends ControllerBase {
// * 报价单-复制报价单 // * 报价单-复制报价单
// */ // */
// @PostMapping("/quotation/copy") // @PostMapping("/quotation/copy")
// public ApiResult<Void> copyQuotation(@Valid @RequestBody QuotationCopyRequest request){ // public ApiResult<Void> copyQuotation(@Valid @RequestBody QuotationCopyRequest request) {
// QuotationShoppingOrder order = shoppingOrderService.getById(request.getId()); // QuotationShoppingOrder order = shoppingOrderService.getById(request.getId());
// VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单"); // VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单");
// List<QuotationShoppingOrderItem> orderItems = shoppingOrderItemService.lambdaQuery() // List<QuotationShoppingOrderItem> orderItems = shoppingOrderItemService.lambdaQuery()
// .eq(QuotationShoppingOrderItem::getOrderId, request.getId()) // .eq(QuotationShoppingOrderItem::getOrderId, request.getId())
// .list(); // .list();
// List<QuotationShoppingCart> carts = shoppingCartService.lambdaQuery() // VUtils.trueThrowBusinessError(orderItems.size() > 1).throwMessage("多机型报价单不能复制");
// .in(QuotationShoppingCart::getId, orderItems.stream().map(QuotationShoppingOrderItem::getCartId).collect(Collectors.toList())) // QuotationShoppingCart cart = shoppingCartService.getById(orderItems.get(0).getCartId());
// .list();
// ShoppingCartVO vo = new ShoppingCartVO() // ShoppingCartVO vo = new ShoppingCartVO()
// .setModelId(request.getModelId()) // .setModelId(cart.getModelId())
// .setTargetId(request.getTargetId()) // .setTargetId(request.getTargetId())
// .setCustomerName(request.getCustomerName()) // .setCustomerName(request.getCustomerName())
// .setPriceId(modelPrice.getPriceId()) // .setPriceId(cart.getPriceId())
// .setConfigId(modelPrice.getConfigId()) // .setConfigId(cart.getConfigId())
// .setTotalFee(modelPrice.getAmount()) // .setTotalFee(cart.getAmount())
// .setActualFee(modelPrice.getAmount()); // .setActualFee(cart.getAmount());
// log.debug("机型【{}】售价为{}", request.getModelNo(), modelPrice); // log.debug("机型【{}】售价为{}", request.getModelNo(), modelPrice);
// if (AppUserUtil.isAgent()) { // if (AppUserUtil.isAgent()) {
// //代理商 // //代理商
@ -709,7 +718,7 @@ public class ShoppingController extends ControllerBase {
* 报价单-导出PDF * 报价单-导出PDF
*/ */
@GetMapping("/quotation/exportToPdf") @GetMapping("/quotation/exportToPdf")
public void exportToPdf(HttpServletResponse response, @RequestParam @NotNull(message = "报价单id不能为空") Long id){ public void exportToPdf(HttpServletResponse response, @RequestParam @NotNull(message = "报价单id不能为空") Long id) {
// QuotationShoppingOrder order = shoppingOrderService.getById(id); // QuotationShoppingOrder order = shoppingOrderService.getById(id);
// VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单"); // VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单");
Map<String, Object> order = new HashMap<>(); Map<String, Object> order = new HashMap<>();
@ -735,7 +744,7 @@ public class ShoppingController extends ControllerBase {
ITextRenderer renderer = new ITextRenderer(); ITextRenderer renderer = new ITextRenderer();
renderer.getFontResolver().addFont("fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); renderer.getFontResolver().addFont("fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
URL baseUrl = new ClassPathResource("templates/").getURL(); URL baseUrl = new ClassPathResource("templates/").getURL();
renderer.setDocumentFromString(html,baseUrl.toString()); renderer.setDocumentFromString(html, baseUrl.toString());
renderer.layout(); renderer.layout();
try (OutputStream outputStream = response.getOutputStream()) { try (OutputStream outputStream = response.getOutputStream()) {
renderer.createPDF(outputStream); renderer.createPDF(outputStream);

View File

@ -2,6 +2,8 @@ package com.nflg.mobilebroken.quotation.pojo.request;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
@Data @Data
@ -10,6 +12,7 @@ public class ShoppingSaveAccessoryRequest {
/** /**
* 物料编号 * 物料编号
*/ */
@NotBlank
private String materialNo; private String materialNo;
/** /**
@ -20,15 +23,18 @@ public class ShoppingSaveAccessoryRequest {
/** /**
* 备件数量 * 备件数量
*/ */
@NotNull
private Integer num; private Integer num;
/** /**
* 备件单价 * 备件单价
*/ */
@NotNull
private BigDecimal amount; private BigDecimal amount;
/** /**
* 备件总价 * 备件总价
*/ */
@NotNull
private BigDecimal totalAmount; private BigDecimal totalAmount;
} }

View File

@ -1,10 +1,13 @@
package com.nflg.mobilebroken.quotation.pojo.request; package com.nflg.mobilebroken.quotation.pojo.request;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
@ -100,14 +103,12 @@ public class ShoppingSaveRequest {
/** /**
* 随机配件总价 * 随机配件总价
*/ */
@NotNull private BigDecimal accessoryFee = BigDecimal.ZERO;
private BigDecimal accessoryFee;
/** /**
* 交机服务总价 * 交机服务总价
*/ */
@NotNull private BigDecimal serviceFee = BigDecimal.ZERO;
private BigDecimal serviceFee;
/** /**
* 交货方式字典id * 交货方式字典id
@ -147,7 +148,7 @@ public class ShoppingSaveRequest {
/** /**
* 币种字典id * 币种字典id
*/ */
@NotBlank @NotNull
private Long currency; private Long currency;
/** /**
@ -169,10 +170,12 @@ public class ShoppingSaveRequest {
/** /**
* 随机配件 * 随机配件
*/ */
@Valid
private List<ShoppingSaveAccessoryRequest> accessories; private List<ShoppingSaveAccessoryRequest> accessories;
/** /**
* 交机服务 * 交机服务
*/ */
@Valid
private List<ShoppingSaveServiceRequest> services; private List<ShoppingSaveServiceRequest> services;
} }

View File

@ -2,6 +2,7 @@ package com.nflg.mobilebroken.quotation.pojo.request;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
@Data @Data
@ -10,20 +11,24 @@ public class ShoppingSaveServiceRequest {
/** /**
* 工程师人数 * 工程师人数
*/ */
@NotNull
private Integer userNum; private Integer userNum;
/** /**
* 服务天数 * 服务天数
*/ */
@NotNull
private Integer days; private Integer days;
/** /**
* 单人天费用 * 单人天费用
*/ */
@NotNull
private BigDecimal fee; private BigDecimal fee;
/** /**
* 服务总费用 * 服务总费用
*/ */
@NotNull
private BigDecimal totalFee; private BigDecimal totalFee;
} }

View File

@ -41,7 +41,7 @@ public class ShoppingCartVO {
/** /**
* 默认系数没有方案时使用 * 默认系数没有方案时使用
*/ */
private BigDecimal defaultRatio; private BigDecimal defaultRatio = BigDecimal.ONE;
/** /**
* 客户名称 * 客户名称

View File

@ -44,6 +44,16 @@
.data1 { .data1 {
width: 100%; width: 100%;
} }
table, th, td {
border: black 1px solid;
border-collapse: collapse;
padding: 5px;
}
table {
width: 100%;
}
</style> </style>
</head> </head>
<body> <body>
@ -129,8 +139,38 @@
<div th:text="${info.no}">其他配置</div> <div th:text="${info.no}">其他配置</div>
<div th:text="${info.no}">油漆要求</div> <div th:text="${info.no}">油漆要求</div>
<div th:text="${info.no}">其它要求说明</div> <div th:text="${info.no}">其它要求说明</div>
<div th:text="${info.no}">其它要求费用</div> <div th:text="${info.no}">随机配件</div>
<table>
<tr>
<th>物料编号</th>
<th>备件名称</th>
<th>备件数量</th>
<th>备件单价</th>
<th>备件总金额</th>
</tr>
<tr th:each="item, itemStat : ${measure.items}" th:if="${not #lists.isEmpty(measure.items)}">
<td th:text="${itemStat.index + 1}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.superintendent}"></td>
<td th:text="${item.scheduleDate}"></td>
<td th:text="${item.confirmedDate}"></td>
</tr>
</table>
<div th:text="${info.no}">交机服务</div>
<table>
<tr>
<th>工程师人数</th>
<th>服务天数</th>
<th>单人天费用</th>
<th>服务总费用</th>
</tr>
<tr th:each="item, itemStat : ${measure.items}" th:if="${not #lists.isEmpty(measure.items)}">
<td th:text="${itemStat.index + 1}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.superintendent}"></td>
<td th:text="${item.scheduleDate}"></td>
</tr>
</table>
</div> </div>
</body> </body>
</html> </html>

View File

@ -50,6 +50,11 @@ public class QuotationShoppingCartAccessory implements Serializable {
*/ */
private BigDecimal amount; private BigDecimal amount;
/**
* 备件总价
*/
private BigDecimal totalAmount;
/** /**
* 优惠金额 * 优惠金额
*/ */