add GaoPointRedeemSkuPageResult

This commit is contained in:
Timi
2026-10-16 00:22:37 +08:00
parent dbf7cb8b0f
commit a4abdc0632
8 changed files with 95 additions and 12 deletions
@@ -6,11 +6,11 @@ import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -30,11 +30,15 @@ public class GaoPointRedeemSkuController {
private final GaoPointRedeemSkuService service;
private final GaoStoreScopeService storeScopeService;
/// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
/// @param page 分页与筛选条件
/// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
/// @throws TimiException 分页条件为空
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_REDEEM_SKU_MANAGE)
@PostMapping("/list")
public PageResult<GaoPointRedeemSku> list(@RequestBody GaoPointRedeemSkuPage page) {
public GaoPointRedeemSkuPageResult list(@RequestBody GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page);
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointRedemption;
import com.imyeyu.api.modules.gao.vo.GaoPointRedemptionPage;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
@@ -32,4 +33,12 @@ public interface GaoPointRedemptionMapper extends BaseMapper<GaoPointRedemption,
/// 统计客户指定 SKU 的有效兑换数量
BigDecimal sumValidQuantity(@Param("storeId") String storeId, @Param("customerId") String customerId,
@Param("skuId") String skuId, @Param("beginAt") Long beginAt, @Param("endAt") Long endAt);
/// 按门店和 SKU 汇总兑换与退货的基础单位数量
/// @param storeId 门店 ID,可为空
/// @param skuId SKU ID
/// @return 兑换、退货及净兑换基础数量
GaoPointRedeemSkuPageResult selectSkuSummaryByStoreIdAndSkuId(
@Param("storeId") String storeId, @Param("skuId") String skuId
);
}
@@ -2,8 +2,8 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.spring.bean.PageResult;
/// GAO 积分兑换 SKU 配置服务
///
@@ -11,8 +11,11 @@ import com.imyeyu.spring.bean.PageResult;
/// @since 2026-09-14
public interface GaoPointRedeemSkuService {
/// 分页查询兑换 SKU 配置
PageResult<GaoPointRedeemSku> pageByQuery(GaoPointRedeemSkuPage page);
/// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
/// @param page 分页与筛选条件
/// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
/// @throws TimiException 分页条件为空
GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page);
/// 查询兑换 SKU 配置详情
GaoPointRedeemSku detail(String storeId, String id);
@@ -168,18 +168,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) {
TimiException.required(request, "not found fulfillment request");
List<GaoOrderFulfillmentItemRequest> requestItems = filterZeroQuantityItems(request.getItemList());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId());
GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
verifyIdempotency(existing, request);
verifyIdempotency(existing, request, requestItems);
return detail(existing.getId());
}
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约");
salesOrderService.checkFulfillmentPermission(order);
List<SourceEntry> sourceList = loadSources(order, request.getItemList(), true);
List<SourceEntry> sourceList = loadSources(order, requestItems, true);
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems);
String operatorUserId = userLoginService.getRequireLoginUserId();
@@ -443,6 +444,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
}
/// 过滤履约请求中数量为 0 的明细
///
/// @param requestItems 履约请求明细
/// @return 过滤后的履约请求明细,null 按原样返回
private List<GaoOrderFulfillmentItemRequest> filterZeroQuantityItems(List<GaoOrderFulfillmentItemRequest> requestItems) {
if (requestItems == null) {
return null;
}
return requestItems.stream()
.filter(item -> item == null || item.getQuantity() == null || 0 != item.getQuantity().signum())
.toList();
}
private List<SourceEntry> loadSources(GaoSalesOrder order, List<GaoOrderFulfillmentItemRequest> requestItems, boolean checkAvailable) {
TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品");
List<GaoOrderFulfillmentItemRequest> sortedRequestItems = new ArrayList<>(requestItems);
@@ -657,12 +671,12 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
}
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request,
List<GaoOrderFulfillmentItemRequest> requestItems) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
&& sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致");
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
@@ -5,16 +5,17 @@ import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.mapper.GaoPointRedemptionMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPointRedeemSkuMapper;
import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -35,15 +36,24 @@ import java.util.UUID;
public class GaoPointRedeemSkuServiceImplement implements GaoPointRedeemSkuService {
private final GaoPointRedeemSkuMapper mapper;
private final GaoPointRedemptionMapper redemptionMapper;
private final GaoProductService productService;
private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService;
@Override
public PageResult<GaoPointRedeemSku> pageByQuery(GaoPointRedeemSkuPage page) {
public GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page");
PageResult<GaoPointRedeemSku> result = new PageResult<>();
GaoPointRedeemSkuPageResult result = new GaoPointRedeemSkuPageResult();
result.setTotal(mapper.countByQuery(page));
if (page.getSkuId() != null && !page.getSkuId().isBlank()) {
GaoPointRedeemSkuPageResult summary = redemptionMapper.selectSkuSummaryByStoreIdAndSkuId(
page.getStoreId(), page.getSkuId()
);
result.setRedeemedBaseQuantityTotal(summary.getRedeemedBaseQuantityTotal());
result.setReturnedBaseQuantityTotal(summary.getReturnedBaseQuantityTotal());
result.setNetRedeemedBaseQuantityTotal(summary.getNetRedeemedBaseQuantityTotal());
}
List<GaoPointRedeemSku> list = mapper.selectByQuery(page);
fillTransient(list);
result.setList(list);
@@ -182,6 +182,8 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
TimiException.requiredTrue(hasText(remark), "库存操作必须填写备注");
TimiException.requiredTrue(request.getItemList() != null && !request.getItemList().isEmpty(), "批量入库至少需要一个 SKU");
List<GaoProductStockManualInBatchRequest.Item> requestItemList = new ArrayList<>(request.getItemList());
requestItemList.removeIf(item -> item != null && item.getQuantity() != null && item.getQuantity().signum() == 0);
TimiException.requiredTrue(!requestItemList.isEmpty(), "批量入库至少需要一个正数量 SKU");
for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
TimiException.required(item, "库存变更项不能为空");
item.setSkuId(requiredText(item.getSkuId(), "not found skuId"));
@@ -0,0 +1,26 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.spring.bean.PageResult;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/// GAO 积分兑换 SKU 配置分页查询结果
///
/// @author Codex
/// @since 2026-10-15
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPointRedeemSkuPageResult extends PageResult<GaoPointRedeemSku> {
/// SKU 筛选条件下的累计兑换基础数量
private BigDecimal redeemedBaseQuantityTotal;
/// SKU 筛选条件下的累计退货基础数量
private BigDecimal returnedBaseQuantityTotal;
/// SKU 筛选条件下的净兑换基础数量
private BigDecimal netRedeemedBaseQuantityTotal;
}
@@ -63,4 +63,19 @@
<if test="beginAt != null">AND `redeemed_at` &gt;= #{beginAt}</if>
<if test="endAt != null">AND `redeemed_at` &lt;= #{endAt}</if>
</select>
<select id="selectSkuSummaryByStoreIdAndSkuId" resultType="com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult">
SELECT COALESCE(SUM(`r`.`base_quantity`), 0) AS `redeemed_base_quantity_total`,
COALESCE(SUM(`return_summary`.`base_quantity_total`), 0) AS `returned_base_quantity_total`,
COALESCE(SUM(`r`.`base_quantity` - COALESCE(`return_summary`.`base_quantity_total`, 0)), 0) AS `net_redeemed_base_quantity_total`
FROM `gao_point_redemption` `r`
LEFT JOIN (
SELECT `redemption_id`, SUM(`base_quantity`) AS `base_quantity_total`
FROM `gao_point_redemption_return`
WHERE `deleted_at` IS NULL
GROUP BY `redemption_id`
) `return_summary` ON `return_summary`.`redemption_id` = `r`.`id`
WHERE `r`.`deleted_at` IS NULL
AND `r`.`sku_id_snapshot` = #{skuId}
<if test="storeId != null and storeId != ''">AND `r`.`store_id` = #{storeId}</if>
</select>
</mapper>