提交
This commit is contained in:
+1
-7
@@ -34,11 +34,5 @@ public final class CargoDiscardRequests {
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String discardNo, String inboundCode, String overseasWarehouseId,
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo,
|
||||
LocalDateTime completedFrom, LocalDateTime completedTo) {
|
||||
/** 未筛完成时间的调用保持创建时间查询语义。 */
|
||||
public Query(long page, long size, String discardNo, String inboundCode, String overseasWarehouseId,
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
this(page, size, discardNo, inboundCode, overseasWarehouseId, status, createdFrom, createdTo, null, null);
|
||||
}
|
||||
}
|
||||
LocalDateTime completedFrom, LocalDateTime completedTo) {}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
/** 只读取废弃明细真实关联的本库托盘身份。 */
|
||||
@Mapper
|
||||
public interface CargoDiscardDimensionMapper {
|
||||
/** 无托盘事实以空串保留,避免把不适用维度伪造为零。 */
|
||||
@Select("""
|
||||
SELECT COALESCE(CASE WHEN i.return_carton_id IS NOT NULL THEN r.pallet_code
|
||||
WHEN i.inventory_position_id IS NOT NULL THEN p.pallet_no ELSE d.pallet_no END,'')
|
||||
FROM ll_cargo_discard_item i
|
||||
LEFT JOIN ll_return_inventory_carton r ON r.id=i.return_carton_id AND r.customer_id=i.customer_id
|
||||
LEFT JOIN ll_inventory_position p ON p.id=i.inventory_position_id AND p.customer_id=i.customer_id
|
||||
LEFT JOIN ll_dropship_order_profile d ON d.id=i.profile_id AND d.customer_id=i.customer_id
|
||||
WHERE i.customer_id=#{customerId} AND i.application_id=#{applicationId}
|
||||
""")
|
||||
List<String> palletNos(@Param("customerId") String customerId, @Param("applicationId") Long applicationId);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.discard.repository;
|
||||
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardDimensionMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 封装数量统计的明细与来源托盘读取。 */
|
||||
@Repository
|
||||
public class CargoDiscardDimensionRepository {
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardDimensionMapper dimensions;
|
||||
/** 读取一票客户隔离明细。 */
|
||||
public List<CargoDiscardItem> items(String customer, Long application) { return items.listApplication(customer, application); }
|
||||
/** 读取一票托盘身份,保留无事实行。 */
|
||||
public List<String> pallets(String customer, Long application) { return dimensions.palletNos(customer, application); }
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.lastleg.discard.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardApplicationMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.List;
|
||||
|
||||
/** 列表和全筛选统计共用同一客户及时间过滤条件。 */
|
||||
@Repository
|
||||
public class CargoDiscardQueryRepository {
|
||||
@Resource private CargoDiscardApplicationMapper applications;
|
||||
|
||||
/** 返回全部筛选单据,不受页面大小影响。 */
|
||||
public List<CargoDiscardApplication> list(String customerId, CargoDiscardRequests.Query query) {
|
||||
return applications.selectList(filter(customerId, query));
|
||||
}
|
||||
|
||||
/** 完成时间使用客户确认时间,与客户端完成时间展示保持一致。 */
|
||||
public static LambdaQueryWrapper<CargoDiscardApplication> filter(String customerId, CargoDiscardRequests.Query query) {
|
||||
return new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(StringUtils.hasText(customerId), CargoDiscardApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.discardNo()), CargoDiscardApplication::getDiscardNo, query.discardNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), CargoDiscardApplication::getInboundCodes, query.inboundCode())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), CargoDiscardApplication::getOverseasWarehouseId, query.overseasWarehouseId())
|
||||
.eq(StringUtils.hasText(query.status()), CargoDiscardApplication::getStatus, query.status())
|
||||
.ge(query.createdFrom() != null, CargoDiscardApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, CargoDiscardApplication::getCreatedAt, query.createdTo())
|
||||
.ge(query.completedFrom() != null, CargoDiscardApplication::getCustomerConfirmedAt, query.completedFrom())
|
||||
.le(query.completedTo() != null, CargoDiscardApplication::getCustomerConfirmedAt, query.completedTo())
|
||||
.orderByDesc(CargoDiscardApplication::getCreatedAt).orderByDesc(CargoDiscardApplication::getId);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardQuantities;
|
||||
import com.baiyue.lastleg.discard.repository.CargoDiscardQueryRepository;
|
||||
import com.baiyue.lastleg.discard.repository.CargoDiscardDimensionRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.HashSet;
|
||||
|
||||
/** 按全部筛选单据统计票、SKU、件、箱和真实来源托盘。 */
|
||||
@Service
|
||||
public class CargoDiscardStatisticsService {
|
||||
@Resource private CargoDiscardQueryRepository queries;
|
||||
@Resource private CargoDiscardDimensionRepository dimensions;
|
||||
|
||||
public record Statistics(long orderCount, int skuCount, long requestedPackages, Long actualPackages,
|
||||
Long requestedPieces, Long actualPieces, Integer palletCount) {}
|
||||
|
||||
/** 已确认实际数独立汇总;缺失件数或托盘事实返回空值,不从箱数换算。 */
|
||||
public Statistics statistics(String customer, CargoDiscardRequests.Query query) {
|
||||
var applications = queries.list(customer, query);
|
||||
var skus = new HashSet<String>(); var pallets = new HashSet<String>();
|
||||
long boxes = 0, actualBoxes = 0, pieces = 0, actualPieces = 0;
|
||||
boolean piecesKnown = true, actualPiecesKnown = true, actualKnown = false, palletsKnown = true;
|
||||
for (var application : applications) {
|
||||
var lines = dimensions.items(application.getCustomerId(), application.getId());
|
||||
boxes += lines.isEmpty() ? application.getTotalPackages() : CargoDiscardQuantities.boxes(lines, false);
|
||||
if (lines.isEmpty()) { piecesKnown = false; palletsKnown = false; }
|
||||
var confirmed = lines.stream().filter(i -> i.getActualDiscardPackages() != null).toList();
|
||||
if (!confirmed.isEmpty()) { actualKnown = true; actualBoxes += CargoDiscardQuantities.boxes(confirmed, true); }
|
||||
for (var line : lines) {
|
||||
if (line.getSkuCode() != null && !line.getSkuCode().isBlank()) skus.add(line.getSkuCode());
|
||||
if (line.getReturnCartonId() == null) {
|
||||
piecesKnown = false;
|
||||
if (line.getActualDiscardPackages() != null) actualPiecesKnown = false;
|
||||
} else {
|
||||
pieces += line.getDiscardPackages();
|
||||
if (line.getActualDiscardPackages() != null) actualPieces += line.getActualDiscardPackages();
|
||||
}
|
||||
}
|
||||
var sourcePallets = dimensions.pallets(application.getCustomerId(), application.getId());
|
||||
if (sourcePallets.isEmpty()) palletsKnown = false;
|
||||
for (var pallet : sourcePallets) {
|
||||
if (pallet == null || pallet.isBlank() || "散箱".equals(pallet)) palletsKnown = false;
|
||||
else pallets.add(application.getCustomerId() + ":" + application.getOverseasWarehouseId() + ":" + pallet);
|
||||
}
|
||||
}
|
||||
return new Statistics(applications.size(), skus.size(), boxes, actualKnown ? actualBoxes : null,
|
||||
!applications.isEmpty() && piecesKnown ? pieces : null,
|
||||
actualKnown && actualPiecesKnown ? actualPieces : null,
|
||||
!applications.isEmpty() && palletsKnown ? pallets.size() : null);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.relabel.model;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/** 列表与数量汇总共用客户范围及筛选条件,避免分页和汇总口径漂移。 */
|
||||
public final class RelabelQueryFilter {
|
||||
/** 禁止创建无状态筛选工具实例。 */
|
||||
private RelabelQueryFilter() { }
|
||||
|
||||
/** 构造完整筛选条件;同时间单据再按主键排序,保持分页稳定。 */
|
||||
public static LambdaQueryWrapper<RelabelApplication> build(String customerId, RelabelRequests.Query query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo()))
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
|
||||
var wrapper = new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(StringUtils.hasText(customerId), RelabelApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
|
||||
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), RelabelApplication::getOverseasWarehouseId, query.overseasWarehouseId())
|
||||
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
|
||||
.apply(StringUtils.hasText(query.skuCode()), "EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND (ri.sku_code LIKE CONCAT('%',{0},'%') OR JSON_SEARCH(CASE WHEN JSON_VALID(ri.sku_relabel_items_json) THEN ri.sku_relabel_items_json ELSE JSON_ARRAY() END, 'one', CONCAT('%',{0},'%'), NULL, '$[*].skuCode') IS NOT NULL))", query.skuCode())
|
||||
.orderByDesc(RelabelApplication::getCreatedAt).orderByDesc(RelabelApplication::getId);
|
||||
String status = query.status() == null ? "" : query.status().trim();
|
||||
if ("PENDING_PROCESSING".equals(status)) wrapper.eq(RelabelApplication::getStatus, status);
|
||||
else if ("COMPLETED".equals(status)) wrapper.in(RelabelApplication::getStatus, "READY_FOR_OUTBOUND", "COMPLETED");
|
||||
else if (StringUtils.hasText(status)) wrapper.eq(RelabelApplication::getStatus, status);
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.relabel.repository;
|
||||
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.model.RelabelQueryFilter;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/** 数量查询只读取本服务申请与箱任务,不跨服务读库存库。 */
|
||||
@Repository
|
||||
public class RelabelQuantityRepository {
|
||||
@Resource private RelabelApplicationMapper applications;
|
||||
@Resource private RelabelItemMapper items;
|
||||
|
||||
/** 读取全部筛选结果,不附加当前页限制。 */
|
||||
public List<RelabelApplication> filtered(String customerId, RelabelRequests.Query query) {
|
||||
return applications.selectList(RelabelQueryFilter.build(customerId, query));
|
||||
}
|
||||
|
||||
/** 明细同时限定客户与申请,避免跨客户聚合关联。 */
|
||||
public List<RelabelItem> items(RelabelApplication application) {
|
||||
return items.selectList(new LambdaQueryWrapper<RelabelItem>()
|
||||
.eq(RelabelItem::getApplicationId, application.getId())
|
||||
.eq(RelabelItem::getCustomerId, application.getCustomerId()));
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.repository.RelabelQuantityRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.*;
|
||||
|
||||
/** 分开统计贴标件数、真实箱任务和未知维度,不按固定箱规换算。 */
|
||||
@Service
|
||||
public class RelabelQuantityService {
|
||||
@Resource private RelabelQuantityRepository repository;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
public record Quantity(Long labelPieceCount, Long cartonCount, int skuCount, boolean duplicateCarton,
|
||||
String quantityUnit, Long sourcePalletCount, Long targetPalletCount) { }
|
||||
public record Totals(long orderCount, long skuCount, long knownLabelPieceCount, long unknownPieceOrders,
|
||||
long knownCartonCount, long unknownCartonOrders,
|
||||
Map<String, Quantity> orders) { }
|
||||
|
||||
/** 按全部筛选单据聚合;未知件数和箱数保留未知单数,不伪造为零。 */
|
||||
@Transactional(readOnly = true)
|
||||
public Totals totals(String customerId, RelabelRequests.Query query) {
|
||||
var dimensions = new LinkedHashMap<String, Quantity>();
|
||||
var skus = new HashSet<String>();
|
||||
long pieces = 0, cartons = 0, unknownPieces = 0, unknownCartons = 0;
|
||||
for (var application : repository.filtered(customerId, query)) {
|
||||
var detail = repository.items(application);
|
||||
Quantity quantity = measure(detail);
|
||||
dimensions.put(application.getRelabelNo(), quantity);
|
||||
detail.forEach(item -> collectSkus(item, skus));
|
||||
if (quantity.labelPieceCount() == null) unknownPieces++; else pieces = Math.addExact(pieces, quantity.labelPieceCount());
|
||||
if (quantity.cartonCount() == null) unknownCartons++; else cartons = Math.addExact(cartons, quantity.cartonCount());
|
||||
}
|
||||
return new Totals(dimensions.size(), skus.size(), pieces, unknownPieces, cartons, unknownCartons, dimensions);
|
||||
}
|
||||
|
||||
/** 箱级任务按箱唛去重;历史重复关联不重复增加件数并显式告警。 */
|
||||
public Quantity measure(List<RelabelItem> items) {
|
||||
if (items.isEmpty()) return new Quantity(null, null, 0, false, "UNKNOWN", null, null);
|
||||
long pieces = 0;
|
||||
boolean unknownPieces = false, bulk = false, duplicate = false, missingCarton = false;
|
||||
var cartons = new HashSet<String>();
|
||||
var piecesSeen = new HashSet<String>();
|
||||
var skus = new HashSet<String>();
|
||||
for (var item : items) {
|
||||
collectSkus(item, skus);
|
||||
String mark = item.getCartonMark() == null ? "" : item.getCartonMark().trim();
|
||||
if (mark.isEmpty()) missingCarton = true;
|
||||
else if (!cartons.add(mark) && !"RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) duplicate = true;
|
||||
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
|
||||
bulk = true;
|
||||
if (item.getNotifiedPackageCount() == null) unknownPieces = true;
|
||||
else if (piecesSeen.add(mark + "|" + item.getSkuCode() + "|" + item.getNewSkuCode())) pieces = Math.addExact(pieces, item.getNotifiedPackageCount());
|
||||
else duplicate = true;
|
||||
} else if ("CARTON_LABEL".equals(item.getOperationType())) {
|
||||
// 只换箱唛不能推导商品件数,保留未提供口径。
|
||||
unknownPieces = true;
|
||||
} else {
|
||||
try {
|
||||
var lines = objectMapper.readTree(Objects.requireNonNullElse(item.getSkuRelabelItemsJson(), "[]"));
|
||||
if (!lines.isArray() || lines.isEmpty()) unknownPieces = true;
|
||||
for (var line : lines) {
|
||||
String key = mark + "|" + line.path("skuCode").asText() + "|" + line.path("newSkuCode").asText();
|
||||
if (!line.path("quantity").canConvertToLong()) unknownPieces = true;
|
||||
else if (piecesSeen.add(key)) pieces = Math.addExact(pieces, line.path("quantity").asLong());
|
||||
}
|
||||
} catch (Exception exception) { unknownPieces = true; }
|
||||
}
|
||||
}
|
||||
// 按件退货来源可能是托盘或包裹标识,不能将标识数冒充真实箱数。
|
||||
return new Quantity(unknownPieces || duplicate ? null : pieces, bulk || missingCarton ? null : (long) cartons.size(),
|
||||
skus.size(), duplicate, bulk ? "PIECE" : "CARTON", null, null);
|
||||
}
|
||||
|
||||
/** SKU种数来自子项;旧快照无子项时才采用旧SKU汇总字段。 */
|
||||
private void collectSkus(RelabelItem item, Set<String> skus) {
|
||||
try {
|
||||
var lines = objectMapper.readTree(Objects.requireNonNullElse(item.getSkuRelabelItemsJson(), "[]"));
|
||||
if (lines.isArray() && !lines.isEmpty()) {
|
||||
for (var line : lines) if (!line.path("skuCode").asText().isBlank()) skus.add(line.path("skuCode").asText().trim());
|
||||
return;
|
||||
}
|
||||
} catch (Exception ignored) { /* 旧快照解析失败时只展示已有旧SKU,不推算件数。 */ }
|
||||
for (var sku : Objects.requireNonNullElse(item.getSkuCode(), "").split(",")) if (!sku.isBlank()) skus.add(sku.trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 贴标申请数量维度
|
||||
version: 1.0.0
|
||||
description: 全部筛选结果汇总,不受 page/size 影响。件、箱、SKU分别统计;未知不能解释为零,禁止固定箱规换算。
|
||||
paths:
|
||||
/api/platform/v1/last-leg/relabel-applications/quantity-summary:
|
||||
get:
|
||||
operationId: platformRelabelQuantitySummary
|
||||
description: 平台列表权限 platform:overseas-warehouse:relabel:list;范围遵循平台认证访问域。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RelabelNo'
|
||||
- $ref: '#/components/parameters/CustomerOrderNo'
|
||||
- $ref: '#/components/parameters/SkuCode'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
- $ref: '#/components/parameters/Warehouse'
|
||||
- $ref: '#/components/parameters/CreatedFrom'
|
||||
- $ref: '#/components/parameters/CreatedTo'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/Summary'
|
||||
/api/merchant/v1/last-leg/relabel-applications/quantity-summary:
|
||||
get:
|
||||
operationId: merchantRelabelQuantitySummary
|
||||
description: 仅认证客户范围;不接受请求参数覆盖客户身份。权限 merchant:warehouse:relabel:list。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RelabelNo'
|
||||
- $ref: '#/components/parameters/CustomerOrderNo'
|
||||
- $ref: '#/components/parameters/SkuCode'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
- $ref: '#/components/parameters/Warehouse'
|
||||
- $ref: '#/components/parameters/CreatedFrom'
|
||||
- $ref: '#/components/parameters/CreatedTo'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/Summary'
|
||||
components:
|
||||
parameters:
|
||||
RelabelNo: {name: relabelNo, in: query, schema: {type: string}, description: 单号模糊匹配}
|
||||
CustomerOrderNo: {name: customerOrderNo, in: query, schema: {type: string}}
|
||||
SkuCode: {name: skuCode, in: query, schema: {type: string}, description: 旧SKU字段或SKU子项匹配}
|
||||
Status: {name: status, in: query, schema: {type: string}, description: PENDING_PROCESSING精确匹配;COMPLETED包含READY_FOR_OUTBOUND与COMPLETED}
|
||||
Warehouse: {name: overseasWarehouseId, in: query, schema: {type: string}}
|
||||
CreatedFrom: {name: createdFrom, in: query, schema: {type: string, format: date-time}}
|
||||
CreatedTo: {name: createdTo, in: query, schema: {type: string, format: date-time}}
|
||||
responses:
|
||||
Summary:
|
||||
description: 标准ApiResponse;code为200时读取data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
requestId: {type: string}
|
||||
traceId: {type: string}
|
||||
data: {$ref: '#/components/schemas/Totals'}
|
||||
schemas:
|
||||
Totals:
|
||||
type: object
|
||||
required: [orderCount, skuCount, knownLabelPieceCount, unknownPieceOrders, knownCartonCount, unknownCartonOrders, orders]
|
||||
properties:
|
||||
orderCount: {type: integer, format: int64, minimum: 0}
|
||||
skuCount: {type: integer, format: int64, minimum: 0, description: 全部筛选结果的不同旧SKU数}
|
||||
knownLabelPieceCount: {type: integer, format: int64, minimum: 0}
|
||||
unknownPieceOrders: {type: integer, format: int64, minimum: 0}
|
||||
knownCartonCount: {type: integer, format: int64, minimum: 0}
|
||||
unknownCartonOrders: {type: integer, format: int64, minimum: 0}
|
||||
orders:
|
||||
type: object
|
||||
description: 以换标业务单号为键,覆盖全部筛选单据
|
||||
additionalProperties: {$ref: '#/components/schemas/Quantity'}
|
||||
Quantity:
|
||||
type: object
|
||||
properties:
|
||||
labelPieceCount: {type: integer, format: int64, nullable: true, description: 按件任务采用通知件数;箱任务采用SKU子项件数;重复关联或缺失为null}
|
||||
cartonCount: {type: integer, format: int64, nullable: true, description: 箱级任务按箱唛去重;按件来源标识不作为箱数}
|
||||
skuCount: {type: integer, minimum: 0}
|
||||
duplicateCarton: {type: boolean, description: 历史重复任务关联提示,不将同一来源不同SKU按件任务视为重复}
|
||||
quantityUnit: {type: string, enum: [PIECE, CARTON, UNKNOWN]}
|
||||
sourcePalletCount: {type: integer, format: int64, nullable: true, description: 缺少不可变原托快照时为null}
|
||||
targetPalletCount: {type: integer, format: int64, nullable: true, description: 未提供汇总,详情以本轮有效绑定历史为准}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** 使用真实 SQL 核对转运实收箱与库存仓位的关联及废弃状态流转。 */
|
||||
class CargoDiscardTransferCartonMapperTest {
|
||||
/** 同批多托盘、多 SKU 时仅返回本仓位实收箱,排除预报、短收、占用及其他客户的箱。 */
|
||||
@Test
|
||||
void receivedBoxesFollowTheirActualPalletAndSku() {
|
||||
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var db = new JdbcTemplate(source);
|
||||
db.execute("CREATE TABLE ll_inbound_plan_carton(id BIGINT,customer_id VARCHAR(20),order_id BIGINT,warehouse_pallet_id BIGINT,status VARCHAR(30),received_at TIMESTAMP,destination_warehouse_code VARCHAR(20),carton_mark VARCHAR(80),sku_code VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(20),inbound_order_id BIGINT,destination_warehouse_code VARCHAR(20),sku_code VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(20),balance_id BIGINT,pallet_id BIGINT,batch_no VARCHAR(80),overseas_warehouse_id VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT,customer_id VARCHAR(20),inbound_order_id BIGINT,transfer_no VARCHAR(80),operating_warehouse_id VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_warehouse_transfer_pallet(id BIGINT,customer_id VARCHAR(20),transfer_order_id BIGINT,warehouse_pallet_id BIGINT,status VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_warehouse_transfer_item(id BIGINT,customer_id VARCHAR(20),transfer_order_id BIGINT,sku_code VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_warehouse_transfer_carton(id BIGINT,customer_id VARCHAR(20),transfer_order_id BIGINT,transfer_item_id BIGINT,transfer_pallet_id BIGINT,status VARCHAR(30),carton_mark VARCHAR(80),updated_at TIMESTAMP)");
|
||||
db.update("INSERT INTO ll_inventory_balance VALUES(1,'C',10,'FBA','SKU'),(2,'C',10,'FBA','OTHER')");
|
||||
db.update("INSERT INTO ll_inventory_position VALUES(41,'C',1,100,'ZY-BYWL-261019194301','W'),(42,'C',1,200,'ZY-BYWL-261019194301','W'),(43,'C',2,100,'ZY-BYWL-261019194301','W')");
|
||||
db.update("INSERT INTO ll_warehouse_transfer_order VALUES(10,'C',10,'ZY-BYWL-261019194301','W')");
|
||||
db.update("INSERT INTO ll_warehouse_transfer_pallet VALUES(11,'C',10,100,'COMPLETED'),(12,'C',10,200,'COMPLETED'),(13,'C',10,100,'BOUND')");
|
||||
db.update("INSERT INTO ll_warehouse_transfer_item VALUES(21,'C',10,'SKU'),(22,'C',10,'OTHER')");
|
||||
db.update("INSERT INTO ll_warehouse_transfer_carton(id,customer_id,transfer_order_id,transfer_item_id,transfer_pallet_id,status,carton_mark) VALUES " +
|
||||
"(1,'C',10,21,11,'COUNTED','BOX-1'),(2,'C',10,21,11,'COUNTED','BOX-2')," +
|
||||
"(3,'C',10,21,12,'COUNTED','OTHER-PALLET'),(4,'C',10,22,11,'COUNTED','OTHER-SKU')," +
|
||||
"(5,'C',10,21,11,'EXPECTED','FORECAST'),(6,'C',10,21,11,'NOT_RECEIVED','SHORT')," +
|
||||
"(7,'C',10,21,11,'DISCARD_PENDING','HELD'),(8,'OTHER',10,21,11,'COUNTED','FOREIGN')," +
|
||||
"(9,'C',10,21,13,'COUNTED','NOT-STOCKED')");
|
||||
var config = new Configuration(new Environment("test", new JdbcTransactionFactory(), source));
|
||||
config.setMapUnderscoreToCamelCase(true);
|
||||
config.addMapper(CargoDiscardInventoryMapper.class);
|
||||
try (var session = new SqlSessionFactoryBuilder().build(config).openSession(true)) {
|
||||
var mapper = session.getMapper(CargoDiscardInventoryMapper.class);
|
||||
assertEquals(List.of("BOX-1", "BOX-2"), mapper.genericCartons("C", 41L).stream().map(c -> c.getCartonMark()).toList());
|
||||
assertEquals(List.of("OTHER-PALLET"), mapper.genericCartons("C", 42L).stream().map(c -> c.getCartonMark()).toList());
|
||||
assertEquals(List.of("OTHER-SKU"), mapper.genericCartons("C", 43L).stream().map(c -> c.getCartonMark()).toList());
|
||||
assertTrue(mapper.genericCartons("OTHER", 41L).isEmpty());
|
||||
assertEquals(1, mapper.changeTransferCartonStatus("C", 2L, "COUNTED", "DISCARD_PENDING"));
|
||||
assertEquals(1, mapper.genericCartons("C", 41L).size());
|
||||
assertEquals(0, mapper.changeTransferCartonStatus("OTHER", 2L, "DISCARD_PENDING", "COUNTED"));
|
||||
assertEquals(1, mapper.changeTransferCartonStatus("C", 2L, "DISCARD_PENDING", "COUNTED"));
|
||||
assertEquals(2, mapper.genericCartons("C", 41L).size());
|
||||
assertEquals(1, mapper.changeTransferCartonStatus("C", 2L, "COUNTED", "DISCARD_PENDING"));
|
||||
assertEquals(1, mapper.changeTransferCartonStatus("C", 2L, "DISCARD_PENDING", "DISCARDED"));
|
||||
assertEquals(1, mapper.genericCartons("C", 41L).size());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -34,7 +34,7 @@ class MerchantCargoDiscardControllerTest {
|
||||
private final CargoDiscardService service = mock(CargoDiscardService.class);
|
||||
private final MerchantCargoDiscardController controller = controller();
|
||||
private final CargoDiscardRequests.Query query = new CargoDiscardRequests.Query(
|
||||
1, 20, null, null, null, null, null, null);
|
||||
1, 20, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantQueriesUseOnlyAuthenticatedTenant() {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.repository.CargoDiscardQueryRepository;
|
||||
import com.baiyue.lastleg.discard.repository.CargoDiscardDimensionRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.List;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** 全筛选统计不能受当前页影响,混装退货箱和缺失维度必须分别处理。 */
|
||||
class CargoDiscardStatisticsServiceTest {
|
||||
private final CargoDiscardQueryRepository queries = mock(CargoDiscardQueryRepository.class);
|
||||
private final CargoDiscardDimensionRepository dimensions = mock(CargoDiscardDimensionRepository.class);
|
||||
private final CargoDiscardStatisticsService service = new CargoDiscardStatisticsService();
|
||||
private final CargoDiscardRequests.Query query = new CargoDiscardRequests.Query(1, 1, null, null, null, null, null, null, null, null);
|
||||
|
||||
/** 给当前服务注入隔离只读仓储。 */
|
||||
CargoDiscardStatisticsServiceTest() {
|
||||
ReflectionTestUtils.setField(service, "queries", queries);
|
||||
ReflectionTestUtils.setField(service, "dimensions", dimensions);
|
||||
}
|
||||
/** 合成两票同托盘退货,第一页大小为一仍汇总两票;同箱SKU不重复计箱。 */
|
||||
@Test void allFilteredOrdersDeduplicateMixedCartonsAndPallets() {
|
||||
var a = application(1L); var b = application(2L);
|
||||
when(queries.list("C", query)).thenReturn(List.of(a, b));
|
||||
when(dimensions.items("C", 1L)).thenReturn(List.of(item(10L, "A", 2, 2), item(10L, "B", 3, 3)));
|
||||
when(dimensions.items("C", 2L)).thenReturn(List.of(item(20L, "A", 7, null)));
|
||||
when(dimensions.pallets("C", 1L)).thenReturn(List.of("P", "P"));
|
||||
when(dimensions.pallets("C", 2L)).thenReturn(List.of("P"));
|
||||
var result = service.statistics("C", query);
|
||||
assertEquals(2, result.orderCount()); assertEquals(2, result.skuCount());
|
||||
assertEquals(2, result.requestedPackages()); assertEquals(1L, result.actualPackages());
|
||||
assertEquals(12L, result.requestedPieces()); assertEquals(5L, result.actualPieces()); assertEquals(1, result.palletCount());
|
||||
}
|
||||
/** 非退货箱级记录不包含件数,实际零与未作业空值必须区分。 */
|
||||
@Test void unknownPiecesAndLoosePalletNeverBecomeZero() {
|
||||
when(queries.list("C", query)).thenReturn(List.of(application(1L)));
|
||||
when(dimensions.items("C", 1L)).thenReturn(List.of(item(null, "A", 2, 0)));
|
||||
when(dimensions.pallets("C", 1L)).thenReturn(List.of(""));
|
||||
var result = service.statistics("C", query);
|
||||
assertEquals(2, result.requestedPackages()); assertEquals(0L, result.actualPackages());
|
||||
assertNull(result.requestedPieces()); assertNull(result.actualPieces()); assertNull(result.palletCount());
|
||||
}
|
||||
/** 空筛选有零票零申请箱,但不能伪造实际作业或商品、托盘事实。 */
|
||||
@Test void emptySelectionHasNoInventedActualFacts() {
|
||||
when(queries.list("C", query)).thenReturn(List.of());
|
||||
var result = service.statistics("C", query);
|
||||
assertEquals(0, result.orderCount()); assertEquals(0, result.requestedPackages());
|
||||
assertNull(result.actualPackages()); assertNull(result.requestedPieces()); assertNull(result.palletCount());
|
||||
verifyNoInteractions(dimensions);
|
||||
}
|
||||
/** 创建一票同客户同仓的隔离单据。 */
|
||||
private static CargoDiscardApplication application(long id) { var a = new CargoDiscardApplication(); a.setId(id); a.setCustomerId("C"); a.setOverseasWarehouseId("W"); return a; }
|
||||
/** 明确退货件数和实际值,不引入固定箱规。 */
|
||||
private static CargoDiscardItem item(Long carton, String sku, int planned, Integer actual) { var i = new CargoDiscardItem(); i.setReturnCartonId(carton); i.setSkuCode(sku); i.setDiscardPackages(planned); i.setActualDiscardPackages(actual); return i; }
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.repository.RelabelQuantityRepository;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class RelabelQuantityServiceTest {
|
||||
/** 以真实对象测试件数、箱数的不同业务口径。 */
|
||||
private RelabelQuantityService service() {
|
||||
var service = new RelabelQuantityService();
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
return service;
|
||||
}
|
||||
/** 构造单个箱任务,件数由SKU子项提供。 */
|
||||
private RelabelItem item(String type, String mark, int quantity) {
|
||||
var item = new RelabelItem();
|
||||
item.setOperationType(type); item.setCartonMark(mark); item.setSkuCode("A");
|
||||
item.setNotifiedPackageCount(quantity); item.setTotalPackageCount(quantity);
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"newSkuCode\":\"B\",\"quantity\":8}]");
|
||||
return item;
|
||||
}
|
||||
@Test void bulkPiecesNeverBecomeCartons() {
|
||||
var result = service().measure(List.of(item("RETURN_BULK_SKU_LABEL", "PALLET-1", 50)));
|
||||
assertEquals(50L, result.labelPieceCount()); assertNull(result.cartonCount());
|
||||
assertNull(result.sourcePalletCount()); assertNull(result.targetPalletCount());
|
||||
}
|
||||
@Test void skuPiecesAndCartonCountAreIndependent() {
|
||||
var result = service().measure(List.of(item("SKU_LABEL", "BOX-1", 1)));
|
||||
assertEquals(8L, result.labelPieceCount()); assertEquals(1L, result.cartonCount()); assertEquals(1, result.skuCount());
|
||||
}
|
||||
@Test void duplicateAssociationIsFlaggedAndCannotInflateCartonsOrGuessPieces() {
|
||||
var result = service().measure(List.of(item("SKU_LABEL", "BOX-1", 1), item("SKU_LABEL", "BOX-1", 1)));
|
||||
assertTrue(result.duplicateCarton()); assertEquals(1L, result.cartonCount()); assertNull(result.labelPieceCount());
|
||||
}
|
||||
@Test void cartonOnlyAndMissingDataDoNotInventZeroPieces() {
|
||||
var result = service().measure(List.of(item("CARTON_LABEL", "BOX-1", 1)));
|
||||
assertNull(result.labelPieceCount()); assertEquals(1L, result.cartonCount());
|
||||
assertNull(service().measure(List.of()).cartonCount());
|
||||
}
|
||||
@Test void mixedStagesDoNotDoubleCountTheBox() {
|
||||
var result = service().measure(List.of(item("CARTON_AND_SKU", "BOX-1", 1)));
|
||||
assertEquals(1L, result.cartonCount()); assertEquals(8L, result.labelPieceCount());
|
||||
}
|
||||
@Test void differentBulkSkusInOneSourceAreNotDuplicateTasks() {
|
||||
var first = item("RETURN_BULK_SKU_LABEL", "PALLET-1", 2);
|
||||
var second = item("RETURN_BULK_SKU_LABEL", "PALLET-1", 3);
|
||||
second.setSkuCode("C"); second.setSkuRelabelItemsJson("[]"); first.setSkuRelabelItemsJson("[]");
|
||||
var result = service().measure(List.of(first, second));
|
||||
assertFalse(result.duplicateCarton()); assertEquals(5L, result.labelPieceCount());
|
||||
assertNull(result.cartonCount()); assertEquals(2, result.skuCount());
|
||||
}
|
||||
@Test void totalsUseAllFilteredApplicationsAndPreserveUnknownDimensions() {
|
||||
var service = service(); var repository = mock(RelabelQuantityRepository.class);
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
var query = new RelabelRequests.Query(2, 1, null, null, "A", null, null, null, null);
|
||||
var first = new RelabelApplication(); first.setRelabelNo("R1"); first.setCustomerId("C1");
|
||||
var second = new RelabelApplication(); second.setRelabelNo("R2"); second.setCustomerId("C1");
|
||||
when(repository.filtered("C1", query)).thenReturn(List.of(first, second));
|
||||
when(repository.items(first)).thenReturn(List.of(item("RETURN_BULK_SKU_LABEL", "P1", 50)));
|
||||
when(repository.items(second)).thenReturn(List.of(item("CARTON_LABEL", "B1", 1)));
|
||||
var totals = service.totals("C1", query);
|
||||
assertEquals(2, totals.orderCount()); assertEquals(50, totals.knownLabelPieceCount());
|
||||
assertEquals(1, totals.unknownPieceOrders()); assertEquals(1, totals.knownCartonCount());
|
||||
assertEquals(1, totals.unknownCartonOrders()); assertEquals(1, totals.skuCount());
|
||||
verify(repository).filtered("C1", query);
|
||||
assertEquals(2, totals.orders().size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user