add /manual-in/batch for gao product
This commit is contained in:
@@ -9,6 +9,7 @@ import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockBatchRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
|
||||
@@ -93,6 +94,21 @@ public class GaoProductStockController {
|
||||
return stockService.manualIn(normalize(request));
|
||||
}
|
||||
|
||||
/// 批量手工入库
|
||||
///
|
||||
/// @param request 批量手工入库请求
|
||||
/// @return 更新后的库存列表
|
||||
/// @throws TimiException 请求或库存校验失败
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
|
||||
@PostMapping("/manual-in/batch")
|
||||
public List<GaoProductStock> manualInBatch(@RequestBody GaoProductStockManualInBatchRequest request) {
|
||||
TimiException.required(request, "not found stock batch request");
|
||||
request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
|
||||
return stockService.manualInBatch(request);
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
|
||||
|
||||
@@ -4,10 +4,12 @@ import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
|
||||
import java.util.List;
|
||||
@@ -36,6 +38,13 @@ public interface GaoProductStockService {
|
||||
/// 手工入库
|
||||
GaoProductStock manualIn(GaoProductStockChangeRequest request);
|
||||
|
||||
/// 批量手工入库
|
||||
///
|
||||
/// @param request 批量手工入库请求
|
||||
/// @return 更新后的库存列表
|
||||
/// @throws TimiException 请求或库存校验失败
|
||||
List<GaoProductStock> manualInBatch(GaoProductStockManualInBatchRequest request);
|
||||
|
||||
/// 手工出库
|
||||
GaoProductStock manualOut(GaoProductStockChangeRequest request);
|
||||
|
||||
|
||||
+139
-4
@@ -16,6 +16,7 @@ import com.imyeyu.api.modules.gao.service.GaoProductStockService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
|
||||
@@ -150,6 +151,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
return change(request, GaoProductStockRecord.OperationType.INITIAL_IN);
|
||||
}
|
||||
|
||||
/// 批量手工入库
|
||||
///
|
||||
/// @param request 批量手工入库请求
|
||||
/// @return 更新后的库存列表
|
||||
/// @throws TimiException 请求或库存校验失败
|
||||
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_IN)
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
@@ -157,6 +163,54 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
return change(request, GaoProductStockRecord.OperationType.MANUAL_IN);
|
||||
}
|
||||
|
||||
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_IN)
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public List<GaoProductStock> manualInBatch(GaoProductStockManualInBatchRequest request) {
|
||||
TimiException.required(request, "not found stock batch request");
|
||||
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
|
||||
String storeId = storeScopeService.resolveStoreId(request.getStoreId());
|
||||
TimiException.required(storeId, "not found storeId");
|
||||
String remark = request.getRemark();
|
||||
TimiException.requiredTrue(hasText(remark), "库存操作必须填写备注");
|
||||
TimiException.requiredTrue(request.getItemList() != null && !request.getItemList().isEmpty(), "批量入库至少需要一个 SKU");
|
||||
List<GaoProductStockManualInBatchRequest.Item> requestItemList = new ArrayList<>(request.getItemList());
|
||||
for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
|
||||
TimiException.required(item, "库存变更项不能为空");
|
||||
item.setSkuId(requiredText(item.getSkuId(), "not found skuId"));
|
||||
item.setSkuUnitId(requiredText(item.getSkuUnitId(), "not found skuUnitId"));
|
||||
}
|
||||
requestItemList.sort(Comparator.comparing(GaoProductStockManualInBatchRequest.Item::getSkuId));
|
||||
Set<String> skuIdSet = new HashSet<>();
|
||||
List<GaoStockChangeItem> changeItemList = new ArrayList<>();
|
||||
for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
|
||||
TimiException.requiredTrue(skuIdSet.add(item.getSkuId()), "同一库存操作不能重复 SKU");
|
||||
GaoProductSkuUnitSelection selection = skuService.getSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId(), true);
|
||||
GaoProductSku sku = selection.getSku();
|
||||
GaoProduct product = selection.getProduct();
|
||||
GaoProductSkuUnit unit = selection.getUnit();
|
||||
TimiException.requiredTrue(Boolean.TRUE.equals(sku.getStockEnabled()), "该 SKU 未启用库存管理");
|
||||
BigDecimal quantity = validateQuantity(sku, item.getQuantity());
|
||||
BigDecimal baseQuantity = quantity.multiply(unit.getBaseQuantityPerUnit());
|
||||
GaoStockChangeItem changeItem = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity);
|
||||
changeItem.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku));
|
||||
changeItemList.add(changeItem);
|
||||
}
|
||||
List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo);
|
||||
if (!existingList.isEmpty()) {
|
||||
verifyExistingManualInBatch(existingList, changeItemList, remark);
|
||||
List<GaoProductStock> result = new ArrayList<>();
|
||||
for (GaoStockChangeItem item : changeItemList) {
|
||||
result.add(detail(storeId, item.getSkuId()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Map<String, List<Attachment>> itemAttachmentMap = prepareManualInBatchAttachments(requestItemList, request.getAttachmentList());
|
||||
return executeInternal(storeId, changeItemList.getFirst().getSkuId(), changeItemList, requestNo,
|
||||
userLoginService.getRequireLoginUserId(), GaoProductStockRecord.OperationType.MANUAL_IN, Time.now(), remark, null,
|
||||
GaoProductStockRecord.BizType.PRODUCT_STOCK, itemAttachmentMap, true);
|
||||
}
|
||||
|
||||
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_OUT)
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
@@ -391,6 +445,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
|
||||
String operatorUserId, GaoProductStockRecord.OperationType operationType, Long occurredAt, String remark, List<Attachment> attachmentList,
|
||||
GaoProductStockRecord.BizType bizType) {
|
||||
return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, occurredAt, remark, attachmentList,
|
||||
bizType, null, false);
|
||||
}
|
||||
|
||||
private List<GaoProductStock> executeInternal(String storeId, String bizId, List<GaoStockChangeItem> sourceList, String requestNo,
|
||||
String operatorUserId, GaoProductStockRecord.OperationType operationType, Long occurredAt, String remark, List<Attachment> attachmentList,
|
||||
GaoProductStockRecord.BizType bizType, Map<String, List<Attachment>> itemAttachmentMap, boolean skuAsBizId) {
|
||||
TimiException.required(storeId, "not found storeId");
|
||||
TimiException.required(bizId, "not found stock bizId");
|
||||
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品");
|
||||
@@ -407,7 +468,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
}
|
||||
List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, normalizedRequestNo);
|
||||
if (!existingList.isEmpty()) {
|
||||
verifyExistingList(existingList, itemList, operationType, bizId);
|
||||
if (skuAsBizId) {
|
||||
verifyExistingManualInBatch(existingList, itemList, remark);
|
||||
} else {
|
||||
verifyExistingList(existingList, itemList, operationType, bizId);
|
||||
}
|
||||
List<GaoProductStock> result = new ArrayList<>();
|
||||
for (GaoStockChangeItem item : itemList) {
|
||||
result.add(detail(storeId, item.getSkuId()));
|
||||
@@ -447,14 +512,16 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
record.setReservedBefore(reservedBefore);
|
||||
record.setReservedAfter(reservedAfter);
|
||||
record.setBizType(bizType);
|
||||
record.setBizId(bizId);
|
||||
record.setBizId(skuAsBizId ? item.getSkuId() : bizId);
|
||||
record.setSourceRecordId(item.getSourceRecordId());
|
||||
record.setRequestNo(normalizedRequestNo);
|
||||
record.setOperatorUserId(operatorUserId);
|
||||
record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now()));
|
||||
record.setOccurredAt(occurredAt == null ? Time.now() : occurredAt);
|
||||
record.setRemark(remark);
|
||||
List<Attachment> recordAttachmentList = itemAttachmentMap == null
|
||||
? attachmentList : itemAttachmentMap.get(item.getSkuId());
|
||||
recordService.createOperationRecord(record,
|
||||
operationType == GaoProductStockRecord.OperationType.ORDER_OUT ? null : attachmentList);
|
||||
operationType == GaoProductStockRecord.OperationType.ORDER_OUT ? null : recordAttachmentList);
|
||||
if (operationType == GaoProductStockRecord.OperationType.ORDER_OUT) {
|
||||
copyAttachments(record.getId(), attachmentList);
|
||||
}
|
||||
@@ -562,6 +629,74 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验批量手工入库幂等请求
|
||||
///
|
||||
/// @param existingList 已存在的库存流水
|
||||
/// @param itemList 请求中的入库项
|
||||
/// @param remark 批次备注
|
||||
/// @throws TimiException 请求内容与已存在流水不一致
|
||||
private void verifyExistingManualInBatch(List<GaoProductStockRecord> existingList, List<GaoStockChangeItem> itemList, String remark) {
|
||||
TimiException.requiredTrue(existingList.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
|
||||
Map<String, GaoProductStockRecord> recordMap = new HashMap<>();
|
||||
for (GaoProductStockRecord record : existingList) {
|
||||
TimiException.requiredTrue(record.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_IN
|
||||
&& GaoProductStockRecord.BizType.PRODUCT_STOCK == record.getBizType()
|
||||
&& record.getSkuId() != null && record.getSkuId().equals(record.getBizId()), "requestNo 已被其他库存操作使用");
|
||||
TimiException.requiredTrue(recordMap.put(record.getSkuId(), record) == null, "requestNo 对应库存操作内容不一致");
|
||||
}
|
||||
TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
|
||||
for (GaoStockChangeItem item : itemList) {
|
||||
GaoProductStockRecord record = recordMap.get(item.getSkuId());
|
||||
TimiException.required(record, "requestNo 对应库存操作内容不一致");
|
||||
TimiException.requiredTrue(item.getSkuUnitId().equals(record.getSkuUnitId())
|
||||
&& record.getQuantity() != null && item.getQuantity().compareTo(record.getQuantity()) == 0
|
||||
&& record.getBaseQuantity() != null && item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0
|
||||
&& sameText(record.getRemark(), remark), "requestNo 对应库存操作内容不一致");
|
||||
}
|
||||
}
|
||||
|
||||
/// 准备批量入库附件并复制复用的全局临时附件
|
||||
///
|
||||
/// @param itemList 排序后的入库项
|
||||
/// @param globalAttachmentList 全局附件列表
|
||||
/// @return 按 SKU 分组的临时附件列表
|
||||
/// @throws TimiException 附件无效或缺失
|
||||
private Map<String, List<Attachment>> prepareManualInBatchAttachments(List<GaoProductStockManualInBatchRequest.Item> itemList,
|
||||
List<Attachment> globalAttachmentList) {
|
||||
Map<String, List<Attachment>> result = new HashMap<>();
|
||||
Set<String> usedTempFileIdSet = new HashSet<>();
|
||||
for (GaoProductStockManualInBatchRequest.Item item : itemList) {
|
||||
boolean useGlobalAttachments = item.getAttachmentList() == null || item.getAttachmentList().isEmpty();
|
||||
List<Attachment> sourceAttachmentList = useGlobalAttachments ? globalAttachmentList : item.getAttachmentList();
|
||||
TimiException.requiredTrue(sourceAttachmentList != null && !sourceAttachmentList.isEmpty(), "每个 SKU 至少需要一个附件");
|
||||
List<Attachment> attachmentList = new ArrayList<>();
|
||||
for (Attachment source : sourceAttachmentList) {
|
||||
TimiException.required(source, "入库附件不能为空");
|
||||
String tempFileId = requiredText(source.getTempFileId(), "入库附件 tempFileId 不能为空");
|
||||
Attachment tempFile = attachmentService.get(tempFileId);
|
||||
TimiException.required(tempFile, "not found temp file");
|
||||
TimiException.requiredTrue(tempFile.getBizType() == Attachment.BizType.TEMP_FILE, "入库附件必须是临时文件");
|
||||
if (!usedTempFileIdSet.add(tempFileId)) {
|
||||
TimiException.requiredTrue(useGlobalAttachments, "同一临时附件不能用于多个入库项");
|
||||
Attachment clone = attachmentService.clone(tempFileId);
|
||||
TimiException.required(clone, "not found cloned temp file");
|
||||
TimiException.requiredTrue(clone.getBizType() == Attachment.BizType.TEMP_FILE, "克隆附件必须是临时文件");
|
||||
tempFileId = requiredText(clone.getId(), "克隆附件 ID 不能为空");
|
||||
TimiException.requiredTrue(usedTempFileIdSet.add(tempFileId), "克隆附件 ID 已被使用");
|
||||
}
|
||||
Attachment attachment = new Attachment();
|
||||
attachment.setTempFileId(tempFileId);
|
||||
attachment.setAttachType(source.getAttachType());
|
||||
attachment.setName(source.getName());
|
||||
attachment.setMetadata(source.getMetadata());
|
||||
attachmentList.add(attachment);
|
||||
}
|
||||
TimiException.requiredTrue(!attachmentList.isEmpty(), "每个 SKU 至少需要一个附件");
|
||||
result.put(item.getSkuId(), attachmentList);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void fillBalance(GaoProductStock stock) {
|
||||
stock.setOnHandQuantity(decimal(stock.getOnHandQuantity()));
|
||||
stock.setReservedQuantity(decimal(stock.getReservedQuantity()));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.imyeyu.api.modules.gao.vo;
|
||||
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/// GAO 产品批量手工入库请求
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-10-13
|
||||
@Data
|
||||
public class GaoProductStockManualInBatchRequest {
|
||||
|
||||
/// 客户端幂等请求号
|
||||
private String requestNo;
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 批次备注
|
||||
private String remark;
|
||||
|
||||
/// 全局附件列表
|
||||
private List<Attachment> attachmentList;
|
||||
|
||||
/// 批量入库项
|
||||
private List<Item> itemList;
|
||||
|
||||
/// 批量手工入库项
|
||||
@Data
|
||||
public static class Item {
|
||||
|
||||
/// SKU ID
|
||||
private String skuId;
|
||||
|
||||
/// SKU 单位 ID
|
||||
private String skuUnitId;
|
||||
|
||||
/// 入库数量
|
||||
private BigDecimal quantity;
|
||||
|
||||
/// SKU 自有附件列表
|
||||
private List<Attachment> attachmentList;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user