v1.0.58 #70

Merged
timi merged 6 commits from dev into master 2026-10-15 17:34:02 +00:00
8 changed files with 95 additions and 12 deletions
Showing only changes of commit a4abdc0632 - Show all commits
@@ -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.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage; 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.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -30,11 +30,15 @@ public class GaoPointRedeemSkuController {
private final GaoPointRedeemSkuService service; private final GaoPointRedeemSkuService service;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
/// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
/// @param page 分页与筛选条件
/// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
/// @throws TimiException 分页条件为空
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_REDEEM_SKU_MANAGE) @RequireGaoPermission(GaoPermissionCode.POINT_REDEEM_SKU_MANAGE)
@PostMapping("/list") @PostMapping("/list")
public PageResult<GaoPointRedeemSku> list(@RequestBody GaoPointRedeemSkuPage page) { public GaoPointRedeemSkuPageResult list(@RequestBody GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page"); TimiException.required(page, "not found redeem sku page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page); 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.entity.GaoPointRedemption;
import com.imyeyu.api.modules.gao.vo.GaoPointRedemptionPage; import com.imyeyu.api.modules.gao.vo.GaoPointRedemptionPage;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -32,4 +33,12 @@ public interface GaoPointRedemptionMapper extends BaseMapper<GaoPointRedemption,
/// 统计客户指定 SKU 的有效兑换数量 /// 统计客户指定 SKU 的有效兑换数量
BigDecimal sumValidQuantity(@Param("storeId") String storeId, @Param("customerId") String customerId, BigDecimal sumValidQuantity(@Param("storeId") String storeId, @Param("customerId") String customerId,
@Param("skuId") String skuId, @Param("beginAt") Long beginAt, @Param("endAt") Long endAt); @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.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage; 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.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.spring.bean.PageResult;
/// GAO 积分兑换 SKU 配置服务 /// GAO 积分兑换 SKU 配置服务
/// ///
@@ -11,8 +11,11 @@ import com.imyeyu.spring.bean.PageResult;
/// @since 2026-09-14 /// @since 2026-09-14
public interface GaoPointRedeemSkuService { public interface GaoPointRedeemSkuService {
/// 分页查询兑换 SKU 配置 /// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
PageResult<GaoPointRedeemSku> pageByQuery(GaoPointRedeemSkuPage page); /// @param page 分页与筛选条件
/// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
/// @throws TimiException 分页条件为空
GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page);
/// 查询兑换 SKU 配置详情 /// 查询兑换 SKU 配置详情
GaoPointRedeemSku detail(String storeId, String id); GaoPointRedeemSku detail(String storeId, String id);
@@ -168,18 +168,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) { private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) {
TimiException.required(request, "not found fulfillment request"); TimiException.required(request, "not found fulfillment request");
List<GaoOrderFulfillmentItemRequest> requestItems = filterZeroQuantityItems(request.getItemList());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId()); GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId());
GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo); GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo);
if (existing != null) { if (existing != null) {
verifyIdempotency(existing, request); verifyIdempotency(existing, request, requestItems);
return detail(existing.getId()); return detail(existing.getId());
} }
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片"); TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约"); TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约");
salesOrderService.checkFulfillmentPermission(order); 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); List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems); boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems);
String operatorUserId = userLoginService.getRequireLoginUserId(); 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) { private List<SourceEntry> loadSources(GaoSalesOrder order, List<GaoOrderFulfillmentItemRequest> requestItems, boolean checkAvailable) {
TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品"); TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品");
List<GaoOrderFulfillmentItemRequest> sortedRequestItems = new ArrayList<>(requestItems); 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()) TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
&& sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致"); && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致");
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId()); List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致"); TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>(); Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) { 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.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku; 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.mapper.GaoPointRedeemSkuMapper;
import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService; import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage; 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.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -35,15 +36,24 @@ import java.util.UUID;
public class GaoPointRedeemSkuServiceImplement implements GaoPointRedeemSkuService { public class GaoPointRedeemSkuServiceImplement implements GaoPointRedeemSkuService {
private final GaoPointRedeemSkuMapper mapper; private final GaoPointRedeemSkuMapper mapper;
private final GaoPointRedemptionMapper redemptionMapper;
private final GaoProductService productService; private final GaoProductService productService;
private final GaoProductSkuService skuService; private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
@Override @Override
public PageResult<GaoPointRedeemSku> pageByQuery(GaoPointRedeemSkuPage page) { public GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page"); TimiException.required(page, "not found redeem sku page");
PageResult<GaoPointRedeemSku> result = new PageResult<>(); GaoPointRedeemSkuPageResult result = new GaoPointRedeemSkuPageResult();
result.setTotal(mapper.countByQuery(page)); 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); List<GaoPointRedeemSku> list = mapper.selectByQuery(page);
fillTransient(list); fillTransient(list);
result.setList(list); result.setList(list);
@@ -182,6 +182,8 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
TimiException.requiredTrue(hasText(remark), "库存操作必须填写备注"); TimiException.requiredTrue(hasText(remark), "库存操作必须填写备注");
TimiException.requiredTrue(request.getItemList() != null && !request.getItemList().isEmpty(), "批量入库至少需要一个 SKU"); TimiException.requiredTrue(request.getItemList() != null && !request.getItemList().isEmpty(), "批量入库至少需要一个 SKU");
List<GaoProductStockManualInBatchRequest.Item> requestItemList = new ArrayList<>(request.getItemList()); 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) { for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
TimiException.required(item, "库存变更项不能为空"); TimiException.required(item, "库存变更项不能为空");
item.setSkuId(requiredText(item.getSkuId(), "not found skuId")); 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="beginAt != null">AND `redeemed_at` &gt;= #{beginAt}</if>
<if test="endAt != null">AND `redeemed_at` &lt;= #{endAt}</if> <if test="endAt != null">AND `redeemed_at` &lt;= #{endAt}</if>
</select> </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> </mapper>