feat(api): 引入文件上传工具类及优化OCR接口

- 新增FileUploadUtil工具类,用于安全处理文件名和生成对象名
- 优化OcrController中的文件上传逻辑,使用新的工具类处理文件名
- UserController中增加上传信息日志记录
- 引入SupplierPackageUsage和UploadFileItem实体类,支持更丰富的文件信息存储
- 修改OrderSupplierDO、OrderPackageDO等实体类,将字符串类型字段改为Long类型
- 调整PurchaseOrderDO及相关映射配置,移除冗余字段并优化结构
- 更新ShipOrderPackageDO中的boxSpecId为Long类型
- 在OrderSupplierMapper.xml中新增typeHandler配置以支持JSON序列化
This commit is contained in:
shenyifei 2025-12-12 14:27:09 +08:00
parent 633af42f5a
commit 56acc46b07
22 changed files with 298 additions and 441 deletions

1
.gitignore vendored
View File

@ -74,6 +74,7 @@ log/
# Claude
.claude/commands/openspec
.claude/skills/codegen
.spec-workflow
.bmad-core
.claude/commands/BMad

View File

@ -1,11 +1,11 @@
package com.xunhong.erp.turbo.admin.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.alibaba.cola.dto.SingleResponse;
import com.xunhong.erp.turbo.api.facade.api.WxMaServiceI;
import com.xunhong.erp.turbo.api.facade.dto.vo.WxMaOcrBankCardVO;
import com.xunhong.erp.turbo.api.facade.dto.vo.WxMaOcrIdCardVO;
import com.xunhong.erp.turbo.base.utils.FileUploadUtil;
import com.xunhong.erp.turbo.file.FileService;
import com.xunhong.erp.turbo.file.config.OssProperties;
import io.swagger.v3.oas.annotations.Operation;
@ -40,7 +40,8 @@ public class OcrController {
@PostMapping(value = "ocrIdCard", consumes = "multipart/form-data")
@Operation(summary = "OCR识别身份证", method = "POST", hidden = true)
public SingleResponse<WxMaOcrIdCardVO> ocrIdCard(@RequestParam @Schema(title = "图片文件", requiredMode = Schema.RequiredMode.REQUIRED, type = "file") MultipartFile file) throws IOException {
String objectName = "uploads/" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyMM/dd") + "/" + file.getOriginalFilename();
// 使用FileUploadUtil安全处理文件名
String objectName = FileUploadUtil.generateObjectName(file);
fileService.upload(objectName, file.getInputStream());
String ocrUrl = properties.getDomain() + objectName;
@ -51,7 +52,8 @@ public class OcrController {
@PostMapping(value = "ocrBankCard", consumes = "multipart/form-data")
@Operation(summary = "OCR识别银行卡", method = "POST", hidden = true)
public SingleResponse<WxMaOcrBankCardVO> ocrBankCard(@RequestParam @Schema(title = "图片文件", requiredMode = Schema.RequiredMode.REQUIRED, type = "file") MultipartFile file) throws IOException {
String objectName = "uploads/" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyMM/dd") + "/" + file.getOriginalFilename();
// 使用FileUploadUtil安全处理文件名
String objectName = FileUploadUtil.generateObjectName(file);
fileService.upload(objectName, file.getInputStream());
String ocrUrl = properties.getDomain() + objectName;

View File

@ -2,7 +2,6 @@ package com.xunhong.erp.turbo.auth.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.tree.Tree;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.dto.Response;
@ -30,6 +29,7 @@ import com.xunhong.erp.turbo.api.user.dto.vo.EmployeeVO;
import com.xunhong.erp.turbo.api.user.dto.vo.UserAuthVO;
import com.xunhong.erp.turbo.api.user.dto.vo.UserVO;
import com.xunhong.erp.turbo.base.dto.UserSession;
import com.xunhong.erp.turbo.base.utils.FileUploadUtil;
import com.xunhong.erp.turbo.file.FileService;
import com.xunhong.erp.turbo.file.config.OssProperties;
import io.swagger.v3.oas.annotations.Operation;
@ -191,7 +191,14 @@ public class UserController {
@PostMapping(value = "upload", consumes = "multipart/form-data")
@Operation(summary = "上传图片", method = "POST", hidden = true)
public SingleResponse<String> upload(@RequestParam @Schema(title = "图片文件", requiredMode = Schema.RequiredMode.REQUIRED, type = "file") MultipartFile file) throws IOException {
String objectName = "uploads/" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyMM/dd") + "/" + file.getOriginalFilename();
// 使用FileUploadUtil安全处理文件名
String objectName = FileUploadUtil.generateObjectName(file);
// 记录上传信息
System.out.println("上传文件: " + file.getOriginalFilename() +
", Content-Type: " + file.getContentType() +
", 生成对象名: " + objectName);
fileService.upload(objectName, file.getInputStream());
return SingleResponse.of(properties.getDomain() + objectName);
}

View File

@ -9,7 +9,6 @@ import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderStateEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@ -51,51 +50,6 @@ public class PurchaseOrder extends DTO {
*/
private PurchaseOrderPricingMethodEnum pricingMethod;
/**
* 销售金额
*/
private BigDecimal saleAmount;
/**
* 包装费
*/
private BigDecimal packageFee;
/**
* 平均单价(/)
*/
private BigDecimal avgUnitPrice;
/**
* 是否返点
*/
private Boolean rebate;
/**
* 毛重()
*/
private BigDecimal grossWeight;
/**
* 净重()
*/
private BigDecimal netWeight;
/**
* 成本合计
*/
private BigDecimal totalCost;
/**
* 运费
*/
private BigDecimal freightCharge;
/**
* 瓜农数量
*/
private Integer supplierCount;
/**
* 采购订单状态: 0_草稿1_审核中2_已完成3_已驳回4_已关闭
*/

View File

@ -27,18 +27,9 @@ public interface PurchaseOrderConvert {
@Mapping(target = "orderCostItemDOList", ignore = true)
@Mapping(target = "auditState", ignore = true)
@Mapping(target = "orderPackageDOList", ignore = true)
@Mapping(target = "totalCost", ignore = true)
@Mapping(target = "supplierCount", ignore = true)
@Mapping(target = "saleAmount", ignore = true)
@Mapping(target = "rebate", ignore = true)
@Mapping(target = "pricingMethod", ignore = true)
@Mapping(target = "packageFee", ignore = true)
@Mapping(target = "orderRebateDO", ignore = true)
@Mapping(target = "orderCompanyDO", ignore = true)
@Mapping(target = "netWeight", ignore = true)
@Mapping(target = "grossWeight", ignore = true)
@Mapping(target = "freightCharge", ignore = true)
@Mapping(target = "avgUnitPrice", ignore = true)
@Mapping(target = "orderDealerDO", ignore = true)
@Mapping(target = "orderCostDOList", ignore = true)
@Mapping(target = "orderSn", ignore = true)
@ -55,18 +46,9 @@ public interface PurchaseOrderConvert {
@Mapping(target = "orderCostItemDOList", ignore = true)
@Mapping(target = "auditState", ignore = true)
@Mapping(target = "orderPackageDOList", ignore = true)
@Mapping(target = "totalCost", ignore = true)
@Mapping(target = "supplierCount", ignore = true)
@Mapping(target = "saleAmount", ignore = true)
@Mapping(target = "rebate", ignore = true)
@Mapping(target = "pricingMethod", ignore = true)
@Mapping(target = "packageFee", ignore = true)
@Mapping(target = "orderRebateDO", ignore = true)
@Mapping(target = "orderCompanyDO", ignore = true)
@Mapping(target = "netWeight", ignore = true)
@Mapping(target = "grossWeight", ignore = true)
@Mapping(target = "freightCharge", ignore = true)
@Mapping(target = "avgUnitPrice", ignore = true)
@Mapping(target = "orderDealerDO", ignore = true)
@Mapping(target = "orderCostDOList", ignore = true)
@Mapping(target = "orderVehicleDO", ignore = true)
@ -93,14 +75,9 @@ public interface PurchaseOrderConvert {
@Mapping(target = "originPrincipal", source = "createdByName")
@Mapping(target = "version", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "totalCost", ignore = true)
@Mapping(target = "supplierCount", ignore = true)
@Mapping(target = "state", ignore = true)
@Mapping(target = "saleAmount", ignore = true)
@Mapping(target = "remark", ignore = true)
@Mapping(target = "rebate", ignore = true)
@Mapping(target = "pricingMethod", ignore = true)
@Mapping(target = "packageFee", ignore = true)
@Mapping(target = "orderVehicleDO", ignore = true)
@Mapping(target = "orderSupplierDOList", ignore = true)
@Mapping(target = "orderSn", ignore = true)
@ -108,12 +85,8 @@ public interface PurchaseOrderConvert {
@Mapping(target = "orderDealerDO", ignore = true)
@Mapping(target = "orderCostDOList", ignore = true)
@Mapping(target = "orderCompanyDO", ignore = true)
@Mapping(target = "netWeight", ignore = true)
@Mapping(target = "isDelete", ignore = true)
@Mapping(target = "grossWeight", ignore = true)
@Mapping(target = "freightCharge", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "avgUnitPrice", ignore = true)
PurchaseOrderDO toPurchaseOrderDO(PurchaseOrderStep1Cmd purchaseOrderStep1Cmd);
}

View File

@ -66,7 +66,7 @@ public class OrderPackageDO extends BaseDO<OrderPackageDO> {
* 箱子规格id
*/
@TableField(value = "box_spec_id")
private String boxSpecId;
private Long boxSpecId;
/**
* 箱子规格名称

View File

@ -5,6 +5,9 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.xunhong.erp.turbo.api.biz.dto.common.SupplierPackageUsage;
import com.xunhong.erp.turbo.api.biz.dto.common.UploadFileItem;
import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderPricingMethodEnum;
import com.xunhong.erp.turbo.datasource.domain.entity.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -110,12 +113,24 @@ public class OrderSupplierDO extends BaseDO<OrderSupplierDO> {
@TableField(value = "purchase_price")
private BigDecimal purchasePrice;
/**
* 箱子类型
*/
@TableField(value = "package_usage", typeHandler = JacksonTypeHandler.class)
private List<SupplierPackageUsage> packageUsage;
/**
* 销售单价(/)
*/
@TableField(value = "sale_price")
private BigDecimal salePrice;
/**
* 报价方式1_按毛重报价2_按净重报价
*/
@TableField(value = "pricing_method")
private PurchaseOrderPricingMethodEnum pricingMethod;
/**
* 发票金额
*/
@ -144,7 +159,7 @@ public class OrderSupplierDO extends BaseDO<OrderSupplierDO> {
* 发票
*/
@TableField(value = "invoice_img", typeHandler = JacksonTypeHandler.class)
private List<String> invoiceImg;
private List<UploadFileItem> invoiceImg;
/**
* 是否上传合同
@ -156,7 +171,7 @@ public class OrderSupplierDO extends BaseDO<OrderSupplierDO> {
* 合同
*/
@TableField(value = "contract_img", typeHandler = JacksonTypeHandler.class)
private List<String> contractImg;
private List<UploadFileItem> contractImg;
/**
* 产品ID

View File

@ -11,7 +11,6 @@ import com.xunhong.erp.turbo.datasource.domain.entity.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
/**
@ -58,60 +57,6 @@ public class PurchaseOrderDO extends BaseDO<PurchaseOrderDO> {
@TableField(value = "pricing_method")
private PurchaseOrderPricingMethodEnum pricingMethod;
/**
* 销售金额
*/
@TableField(value = "sale_amount")
private BigDecimal saleAmount;
/**
* 包装费
*/
@TableField(value = "package_fee")
private BigDecimal packageFee;
/**
* 平均单价(/)
*/
@TableField(value = "avg_unit_price")
private BigDecimal avgUnitPrice;
/**
* 是否返点
*/
@TableField(value = "rebate")
private Boolean rebate;
/**
* 毛重()
*/
@TableField(value = "gross_weight")
private BigDecimal grossWeight;
/**
* 净重()
*/
@TableField(value = "net_weight")
private BigDecimal netWeight;
/**
* 成本合计
*/
@TableField(value = "total_cost")
private BigDecimal totalCost;
/**
* 运费
*/
@TableField(value = "freight_charge")
private BigDecimal freightCharge;
/**
* 瓜农数量
*/
@TableField(value = "supplier_count")
private Integer supplierCount;
/**
* 采购订单状态: 0_草稿1_审核中2_已完成3_已驳回4_已关闭
*/

View File

@ -34,7 +34,7 @@ public class ShipOrderPackageDO extends BaseDO<ShipOrderPackageDO> {
* 箱型规格id
*/
@TableField(value = "box_spec_id")
private String boxSpecId;
private Long boxSpecId;
/**
* 箱型规格名称

View File

@ -8,7 +8,10 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.*;
import com.xunhong.erp.turbo.api.biz.dto.enums.OrderAuditStateEnum;
import com.xunhong.erp.turbo.api.biz.dto.enums.OrderAuditTypeEnum;
import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderAuditStateEnum;
import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderStateEnum;
import com.xunhong.erp.turbo.api.biz.dto.qry.*;
import com.xunhong.erp.turbo.api.biz.exception.BizErrorCode;
import com.xunhong.erp.turbo.api.biz.exception.BizException;
@ -21,8 +24,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@ -503,230 +504,6 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
throw new BizException(BizErrorCode.B_BIZ_ORDER_AUDIT_NOT_FOUND);
}
// 自动生成发货单 purchaseOrderDO 生成 shipOrder,orderPackage 变成 shipOrderPackage按照报价价格分组插入,orderSupplier 变成shipOrderItem按照规格分组插入
// 1. 创建发货单
ShipOrderDO shipOrderDO = new ShipOrderDO();
shipOrderDO.setPurchaseOrderId(purchaseOrderDO.getOrderId());
shipOrderDO.setOrderSn("FH" + generateShipOrderSn()); // 生成发货单编号
// 从车辆信息中获取相关信息
OrderVehicleDO orderVehicleDO = orderVehicleMapper.selectOne(
Wrappers.lambdaQuery(OrderVehicleDO.class)
.eq(OrderVehicleDO::getOrderId, purchaseOrderDO.getOrderId())
);
if (orderVehicleDO != null) {
shipOrderDO.setLicensePlate(orderVehicleDO.getPlate());
shipOrderDO.setDriverName(orderVehicleDO.getDriver());
shipOrderDO.setDriverPhone(orderVehicleDO.getPhone());
shipOrderDO.setShippingDate(orderVehicleDO.getDeliveryTime());
shipOrderDO.setShippingAddress(orderVehicleDO.getOrigin());
shipOrderDO.setReceivingAddress(orderVehicleDO.getDestination());
shipOrderDO.setFreightDebt(orderVehicleDO.getPrice());
if (orderVehicleDO.getOpenStrawCurtain()) {
shipOrderDO.setStrawMatDebt(orderVehicleDO.getStrawCurtainPrice());
}
shipOrderDO.setDealerId(orderVehicleDO.getDealerId());
shipOrderDO.setDealerName(orderVehicleDO.getDealerName());
shipOrderDO.setVehicleNo(orderVehicleDO.getVehicleNo());
}
// 从公司信息中获取相关信息
OrderCompanyDO orderCompanyDO = orderCompanyMapper.selectOne(
Wrappers.lambdaQuery(OrderCompanyDO.class)
.eq(OrderCompanyDO::getOrderId, purchaseOrderDO.getOrderId())
);
if (orderCompanyDO != null) {
shipOrderDO.setCompanyId(orderCompanyDO.getCompanyId());
shipOrderDO.setCompanyName(orderCompanyDO.getFullName());
}
// 从经销商信息中获取相关信息
OrderDealerDO orderDealerDO = orderDealerMapper.selectOne(
Wrappers.lambdaQuery(OrderDealerDO.class)
.eq(OrderDealerDO::getOrderId, purchaseOrderDO.getOrderId())
);
// 设置其他基本信息
shipOrderDO.setCreatedBy(purchaseOrderFinalApproveCmd.getCreatedBy());
shipOrderDO.setCreatedByName(purchaseOrderFinalApproveCmd.getCreatedByName());
// 4. 从OrderCostDO中提取人工费商标费等费用信息
List<OrderCostDO> orderCostDOList = orderCostMapper.selectList(
Wrappers.lambdaQuery(OrderCostDO.class)
.eq(OrderCostDO::getOrderId, purchaseOrderDO.getOrderId())
);
// 计算人工费总额
BigDecimal totalLaborFee = orderCostDOList.stream()
.filter(cost -> CostItemTypeEnum.ARTIFICIAL_TYPE.getType() == cost.getType().getType())
.map(cost -> {
if (cost.getPrice() != null && cost.getCount() != null) {
return cost.getPrice().multiply(BigDecimal.valueOf(cost.getCount()));
}
return BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderDO.setLaborFee(totalLaborFee);
// 计算商标费总额
BigDecimal totalTrademarkFee = orderCostDOList.stream()
.filter(cost -> "商标费".equals(cost.getName()))
.map(cost -> {
if (cost.getPrice() != null && cost.getCount() != null) {
return cost.getPrice().multiply(BigDecimal.valueOf(cost.getCount()));
}
return BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderDO.setTrademarkFee(totalTrademarkFee);
// 计算打码费总额
BigDecimal totalCodingFee = orderCostDOList.stream()
.filter(cost -> "打码费".equals(cost.getName()))
.map(cost -> {
if (cost.getPrice() != null && cost.getCount() != null) {
return cost.getPrice().multiply(BigDecimal.valueOf(cost.getCount()));
}
return BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderDO.setTrademarkFee(totalCodingFee);
shipOrderDO.setCodingFee(totalTrademarkFee);
// 2. 处理包材信息转换为发货单包材信息按照boxCategoryId和boxProductName分组
List<OrderSupplierDO> orderSupplierDOList = orderSupplierMapper.selectList(
Wrappers.lambdaQuery(OrderSupplierDO.class)
.eq(OrderSupplierDO::getOrderId, purchaseOrderDO.getOrderId())
);
// 收集所有的包材信息
List<Long> orderSupplierIdList = orderSupplierDOList.stream()
.map(OrderSupplierDO::getOrderSupplierId)
.toList();
List<OrderPackageDO> orderPackageDOList = new ArrayList<>();
if (CollUtil.isNotEmpty(orderSupplierIdList)) {
orderPackageDOList = orderPackageMapper.selectList(
Wrappers.lambdaQuery(OrderPackageDO.class)
.in(OrderPackageDO::getOrderSupplierId, orderSupplierIdList)
);
}
// 计算纸箱费总额
BigDecimal totalCartonFee = orderPackageDOList.stream()
.map(p -> {
if (p.getBoxSalePrice() != null && p.getBoxCount() != null) {
return p.getBoxSalePrice().multiply(BigDecimal.valueOf(p.getBoxCount()));
}
return BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderDO.setCartonFee(totalCartonFee);
// 插入发货单
shipOrderMapper.insert(shipOrderDO);
// 按boxCategoryId和boxProductName分组包材信息
Map<String, List<OrderPackageDO>> packageGroupByKey = orderPackageDOList.stream()
.collect(Collectors.groupingBy(p -> p.getBoxSpecId() + "_" + p.getBoxProductName()));
// 创建发货单包材信息
for (Map.Entry<String, List<OrderPackageDO>> entry : packageGroupByKey.entrySet()) {
List<OrderPackageDO> packages = entry.getValue();
// 合并相同类型的包材信息
ShipOrderPackageDO shipOrderPackageDO = new ShipOrderPackageDO();
shipOrderPackageDO.setShipOrderId(shipOrderDO.getShipOrderId());
// 使用第一个包材的信息作为基础
OrderPackageDO firstPackage = packages.get(0);
shipOrderPackageDO.setBoxSpecId(firstPackage.getBoxSpecId());
shipOrderPackageDO.setBoxSpecName(firstPackage.getBoxSpecName());
shipOrderPackageDO.setBoxProduct(firstPackage.getBoxProductName());
// 使用第一个包材的销售价格作为单价
shipOrderPackageDO.setUnitPrice(firstPackage.getBoxSalePrice());
// 计算总数量
int totalQuantity = packages.stream()
.mapToInt(OrderPackageDO::getBoxCount)
.sum();
shipOrderPackageDO.setQuantity(totalQuantity);
// 计算总金额
BigDecimal unitPrice = firstPackage.getBoxSalePrice() != null ? firstPackage.getBoxSalePrice() : BigDecimal.ZERO;
BigDecimal totalAmount = unitPrice.multiply(BigDecimal.valueOf(totalQuantity));
shipOrderPackageDO.setItemAmount(totalAmount);
// 计算总重量如果有
BigDecimal totalWeight = packages.stream()
.map(p -> {
if (p.getBoxProductWeight() != null && p.getBoxCount() != null) {
return p.getBoxProductWeight().multiply(BigDecimal.valueOf(p.getBoxCount()));
}
return BigDecimal.ZERO;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderPackageDO.setTotalWeight(totalWeight);
shipOrderPackageDO.setSingleWeight(shipOrderPackageDO.getTotalWeight().divide(shipOrderPackageDO.getUnitPrice(), 2, RoundingMode.HALF_UP));
shipOrderPackageMapper.insert(shipOrderPackageDO);
}
// 3. 处理供应商信息转换为发货单项信息按照规格分组
// 按销售单价分组供应商信息
Map<BigDecimal, List<OrderSupplierDO>> supplierGroupByPrice = orderSupplierDOList.stream()
.collect(Collectors.groupingBy(OrderSupplierDO::getSalePrice));
// 创建发货单项信息
for (Map.Entry<BigDecimal, List<OrderSupplierDO>> entry : supplierGroupByPrice.entrySet()) {
List<OrderSupplierDO> suppliers = entry.getValue();
// 合并相同价格的供应商信息
ShipOrderItemDO shipOrderItemDO = new ShipOrderItemDO();
shipOrderItemDO.setShipOrderId(shipOrderDO.getShipOrderId());
// 计算总毛重
BigDecimal totalGrossWeight = suppliers.stream()
.map(OrderSupplierDO::getGrossWeight)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderItemDO.setGrossWeight(totalGrossWeight);
// 计算总净重
BigDecimal totalNetWeight = suppliers.stream()
.map(OrderSupplierDO::getNetWeight)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
shipOrderItemDO.setNetWeight(totalNetWeight);
shipOrderItemDO.setBoxWeight(totalGrossWeight.subtract(totalNetWeight));
// 计算总箱数
Integer totalBoxCount = orderPackageDOList.stream()
.filter(p -> suppliers.stream().map(OrderSupplierDO::getOrderSupplierId).toList().contains(p.getOrderSupplierId()))
.map(OrderPackageDO::getBoxCount)
.reduce(0, Integer::sum);
shipOrderItemDO.setBoxCount(totalBoxCount);
// 设置单价
BigDecimal salePrice = entry.getKey();
shipOrderItemDO.setUnitPrice(salePrice);
// 计算总金额
if (salePrice != null) {
BigDecimal totalAmount = salePrice.multiply(totalNetWeight);
shipOrderItemDO.setTotalAmount(totalAmount);
}
shipOrderItemMapper.insert(shipOrderItemDO);
}
purchaseOrderMapper.updateById(purchaseOrderDO);
}
@ -807,6 +584,7 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
queryWrapper.last("limit 1");
purchaseOrderDO = purchaseOrderMapper.selectOne(queryWrapper);
purchaseOrderDO.setActive(purchaseOrderStep1Cmd.getActive());
purchaseOrderDO.setState(PurchaseOrderStateEnum.DRAFT);
purchaseOrderMapper.updateById(purchaseOrderDO);
}
@ -867,6 +645,7 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
queryWrapper.last("limit 1");
PurchaseOrderDO purchaseOrderDO = purchaseOrderMapper.selectOne(queryWrapper);
purchaseOrderDO.setActive(purchaseOrderStep2Cmd.getActive());
purchaseOrderDO.setState(PurchaseOrderStateEnum.DRAFT);
purchaseOrderMapper.updateById(purchaseOrderDO);
// 更新供应商信息精细化处理
@ -1019,6 +798,7 @@ public class PurchaseOrderGatewayImpl implements PurchaseOrderGateway {
PurchaseOrderDO purchaseOrderDO = purchaseOrderMapper.selectOne(queryWrapper);
purchaseOrderDO.setActive(purchaseOrderStep3Cmd.getActive());
purchaseOrderDO.setForeman(purchaseOrderStep3Cmd.getForeman());
purchaseOrderDO.setState(PurchaseOrderStateEnum.DRAFT);
purchaseOrderMapper.updateById(purchaseOrderDO);
saveOrderCostItem(orderId, purchaseOrderStep3Cmd.getOrderCostItemList().stream().toList());

View File

@ -20,10 +20,13 @@
<result property="grossWeight" column="gross_weight"/>
<result property="netWeight" column="net_weight"/>
<result property="purchasePrice" column="purchase_price"/>
<result property="packageUsage" column="package_usage"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result property="salePrice" column="sale_price"/>
<result property="pricingMethod" column="pricing_method"/>
<result property="invoiceAmount" column="invoice_amount"/>
<result property="emptyWeightImg" column="empty_photo"/>
<result property="totalWeightImg" column="total_photo"/>
<result property="emptyWeightImg" column="empty_weight_img"/>
<result property="totalWeightImg" column="total_Weight_img"/>
<result property="invoiceImg" column="invoice_img"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result property="contractImg" column="contract_img"

View File

@ -10,17 +10,7 @@
<result property="originPrincipal" column="origin_principal"/>
<result property="pricingMethod" column="pricing_method"/>
<result property="orderSn" column="order_sn"/>
<result property="saleAmount" column="sale_amount"/>
<result property="packageFee" column="package_fee"/>
<result property="avgUnitPrice" column="avg_unit_price"/>
<result property="rebate" column="rebate"/>
<result property="grossWeight" column="gross_weight"/>
<result property="netWeight" column="net_weight"/>
<result property="totalCost" column="total_cost"/>
<result property="freightCharge" column="freight_charge"/>
<result property="supplierCount" column="supplier_count"/>
<result property="state" column="state"/>
<result property="rejectReason" column="reject_reason"/>
<result property="foreman" column="foreman"/>
<result property="remark" column="remark"/>
<result property="createdBy" column="created_by"/>

View File

@ -1,6 +1,7 @@
package com.xunhong.erp.turbo.infra.infrastructure.gateway;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.xunhong.erp.turbo.base.utils.FileUploadUtil;
import com.xunhong.erp.turbo.file.FileService;
import com.xunhong.erp.turbo.file.config.OssProperties;
import com.xunhong.erp.turbo.infra.domain.entity.Credentials;
@ -37,7 +38,8 @@ public class OssGatewayImpl implements OssGateway {
@Override
public String upload(MultipartFile file) {
String objectName = "uploads/" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyMM/dd") + "/" + file.getOriginalFilename();
// 使用FileUploadUtil安全处理文件名
String objectName = FileUploadUtil.generateObjectName(file);
try {
fileService.upload(objectName, file.getInputStream());

View File

@ -1,5 +1,6 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.api.biz.dto.common.UploadFileItem;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -29,7 +30,7 @@ public class OrderSupplierUpdateCmd extends Command {
* 发票照片
*/
@Schema(title = "发票照片")
private List<String> invoiceImg;
private List<UploadFileItem> invoiceImg;
/**
* 是否上传合同
@ -41,7 +42,7 @@ public class OrderSupplierUpdateCmd extends Command {
* 合同照片
*/
@Schema(title = "合同照片")
private List<String> contractImg;
private List<UploadFileItem> contractImg;
/**
* 是否已付定金

View File

@ -62,8 +62,8 @@ public class OrderPackage extends Command {
/**
* 箱子规格ID
*/
@Schema(title = "箱子规格ID", requiredMode = Schema.RequiredMode.REQUIRED)
private String boxSpecId;
@Schema(title = "箱子规格ID", type = "string", requiredMode = Schema.RequiredMode.REQUIRED)
private Long boxSpecId;
/**
* 箱子规格名称

View File

@ -1,5 +1,6 @@
package com.xunhong.erp.turbo.api.biz.dto.common;
import com.xunhong.erp.turbo.api.biz.dto.enums.PurchaseOrderPricingMethodEnum;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -106,12 +107,24 @@ public class OrderSupplier extends Command {
@Schema(title = "采购单价(元/斤)", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal purchasePrice;
/**
* 箱子类型
*/
@Schema(title = "箱子类型", requiredMode = Schema.RequiredMode.REQUIRED)
private List<SupplierPackageUsage> packageUsage;
/**
* 销售单价(/)
*/
@Schema(title = "销售单价(元/斤)", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal salePrice;
/**
* 报价方式1_按毛重报价2_按净重报价
*/
@Schema(title = "报价方式1_按毛重报价2_按净重报价")
private PurchaseOrderPricingMethodEnum pricingMethod;
/**
* 发票金额
*/
@ -140,7 +153,7 @@ public class OrderSupplier extends Command {
* 发票
*/
@Schema(title = "发票")
private List<String> invoiceImg;
private List<UploadFileItem> invoiceImg;
/**
* 是否上传合同
@ -152,7 +165,7 @@ public class OrderSupplier extends Command {
* 合同
*/
@Schema(title = "合同")
private List<String> contractImg;
private List<UploadFileItem> contractImg;
/**
* 产品ID

View File

@ -31,7 +31,7 @@ public class ShipOrderPackage extends DTO {
/**
* 箱型ID
*/
@Schema(title = "箱型ID")
@Schema(title = "箱型ID", type = "string", requiredMode = Schema.RequiredMode.REQUIRED)
private Long boxSpecId;
/**

View File

@ -0,0 +1,22 @@
package com.xunhong.erp.turbo.api.biz.dto.common;
import com.alibaba.cola.dto.Command;
import com.xunhong.erp.turbo.api.biz.dto.enums.OrderPackageBoxTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "采购订单供应商纸箱使用情况")
@EqualsAndHashCode(callSuper = true)
public class SupplierPackageUsage extends Command {
@Schema(title = "箱子类型:1_本次使用2_额外运输3_已使用额外运输4_车上剩余5_瓜农纸箱6_空箱")
private OrderPackageBoxTypeEnum boxType;
@Schema(title = "是否使用:0_未回答1_使用2_未使用")
private Integer isUsed;
}

View File

@ -0,0 +1,26 @@
package com.xunhong.erp.turbo.api.biz.dto.common;
import com.alibaba.cola.dto.DTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "文件上传")
@EqualsAndHashCode(callSuper = true)
public class UploadFileItem extends DTO {
@Schema(title = "文件名")
private String fileName;
@Schema(title = "文件路径")
private String filePath;
@Schema(title = "文件大小")
private Long fileSize;
@Schema(title = "文件类型")
private String fileType;
}

View File

@ -2,6 +2,7 @@ package com.xunhong.erp.turbo.api.biz.dto.vo;
import com.alibaba.cola.dto.DTO;
import com.xunhong.erp.turbo.api.biz.dto.common.OrderVehicle;
import com.xunhong.erp.turbo.api.biz.dto.common.UploadFileItem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -172,7 +173,7 @@ public class OrderSupplierVO extends DTO {
* 发票照片
*/
@Schema(title = "发票照片")
private List<String> invoiceImg;
private List<UploadFileItem> invoiceImg;
/**
* 是否上传合同
@ -184,7 +185,7 @@ public class OrderSupplierVO extends DTO {
* 合同照片
*/
@Schema(title = "合同照片")
private List<String> contractImg;
private List<UploadFileItem> contractImg;
/**
* 创建时间

View File

@ -9,7 +9,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@ -63,60 +62,6 @@ public class PurchaseOrderVO extends DTO {
@Schema(title = "报价方式1_按毛重报价2_按净重报价")
private PurchaseOrderPricingMethodEnum pricingMethod;
/**
* 销售金额
*/
@Schema(title = "销售金额")
private BigDecimal saleAmount;
/**
* 包装费
*/
@Schema(title = "包装费")
private BigDecimal packageFee;
/**
* 平均单价(/)
*/
@Schema(title = "平均单价(元/斤)")
private BigDecimal avgUnitPrice;
/**
* 是否返点
*/
@Schema(title = "是否返点")
private Boolean rebate;
/**
* 毛重()
*/
@Schema(title = "毛重(斤)")
private BigDecimal grossWeight;
/**
* 净重()
*/
@Schema(title = "净重(斤)")
private BigDecimal netWeight;
/**
* 成本合计
*/
@Schema(title = "成本合计")
private BigDecimal totalCost;
/**
* 运费
*/
@Schema(title = "运费")
private BigDecimal freightCharge;
/**
* 瓜农数量
*/
@Schema(title = "瓜农数量")
private Integer supplierCount;
/**
* 采购订单状态: 0_草稿1_审核中2_已完成3_已驳回4_已关闭
*/

View File

@ -0,0 +1,177 @@
package com.xunhong.erp.turbo.base.utils;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 文件上传工具类
*
* @author system
*/
@Slf4j
public class FileUploadUtil {
/**
* 安全获取文件名处理后缀问题
*
* @param file 上传的文件
* @return 处理后的安全文件名
*/
public static String getSafeFileName(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
log.debug("原始文件名: {}, Content-Type: {}", originalFilename, file.getContentType());
// 如果原始文件名为空或无效生成默认文件名
if (StringUtils.isBlank(originalFilename)) {
String generatedName = generateDefaultFileName(file);
log.warn("原始文件名为空,生成文件名: {}", generatedName);
return generatedName;
}
// 检查文件名是否包含后缀
if (!containsFileExtension(originalFilename)) {
// 尝试从Content-Type推断文件类型
String extension = getExtensionFromContentType(file.getContentType());
if (extension != null) {
String fileNameWithExt = originalFilename + "." + extension;
log.info("文件名缺少后缀从Content-Type推断: {} -> {}", originalFilename, fileNameWithExt);
return fileNameWithExt;
} else {
log.warn("无法推断文件后缀,使用原始文件名: {}", originalFilename);
}
}
return originalFilename;
}
/**
* 生成唯一的文件对象名
*
* @param file 上传的文件
* @return 生成的对象名
*/
public static String generateObjectName(MultipartFile file) {
String safeFileName = getSafeFileName(file);
String datePath = LocalDateTimeUtil.format(LocalDateTime.now(), "yyMM/dd");
return "uploads/" + datePath + "/" + safeFileName;
}
/**
* 检查文件名是否包含后缀
*
* @param filename 文件名
* @return 是否包含后缀
*/
private static boolean containsFileExtension(String filename) {
if (filename == null) return false;
int lastDotIndex = filename.lastIndexOf('.');
return lastDotIndex > 0 && lastDotIndex < filename.length() - 1;
}
/**
* 从Content-Type推断文件扩展名
*
* @param contentType MIME类型
* @return 文件扩展名如果无法推断则返回null
*/
private static String getExtensionFromContentType(String contentType) {
if (contentType == null) return null;
switch (contentType.toLowerCase()) {
// 图片类型
case "image/jpeg":
return "jpeg";
case "image/jpg":
return "jpg";
case "image/png":
return "png";
case "image/gif":
return "gif";
case "image/webp":
return "webp";
case "image/bmp":
return "bmp";
case "image/svg+xml":
return "svg";
// 文档类型
case "application/pdf":
return "pdf";
case "application/msword":
return "doc";
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return "docx";
case "application/vnd.ms-excel":
return "xls";
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return "xlsx";
// 文本类型
case "text/plain":
return "txt";
case "text/csv":
return "csv";
case "application/json":
return "json";
case "application/xml":
return "xml";
// 压缩文件
case "application/zip":
return "zip";
case "application/x-rar-compressed":
return "rar";
default:
log.debug("未知的Content-Type: {}", contentType);
return null;
}
}
/**
* 生成默认文件名
*
* @param file 上传的文件
* @return 生成的文件名
*/
private static String generateDefaultFileName(MultipartFile file) {
String timestamp = LocalDateTimeUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss");
String randomId = UUID.randomUUID().toString().substring(0, 8);
String extension = getExtensionFromContentType(file.getContentType());
return extension != null ?
"file_" + timestamp + "_" + randomId + "." + extension :
"file_" + timestamp + "_" + randomId;
}
/**
* 验证文件类型是否允许上传
*
* @param file 上传的文件
* @param allowedExtensions 允许的文件扩展名数组
* @return 是否允许上传
*/
public static boolean isAllowedFileType(MultipartFile file, String[] allowedExtensions) {
String fileName = getSafeFileName(file);
if (fileName == null) return false;
int lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex == -1) return false;
String extension = fileName.substring(lastDotIndex + 1).toLowerCase();
for (String allowedExt : allowedExtensions) {
if (allowedExt.toLowerCase().equals(extension)) {
return true;
}
}
return false;
}
}