Merge pull request '新增积分商品选择、满减送优化' (#14) from zzw-one into master
All checks were successful
continuous-integration/drone Build is passing

Reviewed-on: #14
This commit is contained in:
root 2024-09-24 18:06:18 +08:00
commit 326dac91b5
17 changed files with 288 additions and 14 deletions

View File

@ -56,6 +56,8 @@ export interface Spu {
stock?: number // 商品库存
createTime?: Date // 商品创建时间
status?: number // 商品状态
pointsStatus?: number // 商品使用积分状态
usePointsMax?: number // 单商品积分使用上限
}
// 获得 Spu 列表

View File

@ -1,6 +1,20 @@
<!-- 商品发布 - 其它设置 -->
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
<el-form-item label="启用积分" prop="pointsStatus">
<el-radio-group v-model="formData.pointsStatus" class="w-80">
<el-radio :label="0" class="radio">禁用</el-radio>
<el-radio :label="1">启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.pointsStatus" label="最大抵扣" prop="usePointsMax">
<el-input-number
v-model="formData.usePointsMax"
:min="0"
placeholder="请输入最大抵扣积分"
class="w-80!"
/>
</el-form-item>
<el-form-item label="商品排序" prop="sort">
<el-input-number
v-model="formData.sort"
@ -50,6 +64,8 @@ const formRef = ref() // 表单Ref
const formData = ref<Spu>({
sort: 0, //
giveIntegral: 0, //
pointsStatus: 0, // 使
usePointsMax: 0, // 使
virtualSalesCount: 0 //
})
//

View File

@ -109,6 +109,8 @@ const formData = ref<ProductSpuApi.Spu>({
description: '', //
sort: 0, //
giveIntegral: 0, //
pointsStatus: 0, // 使
usePointsMax: 0, // 使
virtualSalesCount: 0 //
})

View File

@ -83,6 +83,16 @@ public class ProductSpuRespDTO {
*/
private Integer giveIntegral;
/**
* 商品使用积分状态
*/
private Integer pointsStatus;
/**
* 单商品积分使用上限
*/
private Integer usePointsMax;
// ========== 分销相关字段 =========
/**

View File

@ -109,6 +109,14 @@ public class ProductSpuRespVO {
@ExcelProperty("分销类型")
private Boolean subCommissionType;
@Schema(description = "商品使用积分状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
@ExcelProperty("商品使用积分状态")
private Integer pointsStatus;
@Schema(description = "单商品积分使用上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
@ExcelProperty("单商品积分使用上限")
private Integer usePointsMax;
// ========== 统计相关字段 =========
@Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000")

View File

@ -76,6 +76,12 @@ public class ProductSpuSaveReqVO {
@NotNull(message = "商品分销类型不能为空")
private Boolean subCommissionType;
@Schema(description = "商品使用积分状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Integer pointsStatus;
@Schema(description = "单商品积分使用上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Integer usePointsMax;
// ========== 统计相关字段 =========
@Schema(description = "虚拟销量", example = "66")

View File

@ -154,6 +154,16 @@ public class ProductSpuDO extends BaseDO {
*/
private Boolean subCommissionType;
/**
* 商品使用积分状态
*/
private Integer pointsStatus;
/**
* 单商品积分使用上限
*/
private Integer usePointsMax;
// ========== 统计相关字段 =========
/**

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.api.reward;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityDTO;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import java.util.Collection;
@ -21,4 +22,9 @@ public interface RewardActivityApi {
*/
List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds);
/**
*
*/
RewardActivityDTO getRewardActivityById(Long id);
}

View File

@ -0,0 +1,103 @@
package cn.iocoder.yudao.module.promotion.api.reward.dto;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 满减送活动 DO
*
* @author 芋道源码
*/
@Data
public class RewardActivityDTO {
/**
* 活动编号主键自增
*/
private Long id;
/**
* 活动标题
*/
private String name;
/**
* 状态
*
* 枚举 {@link PromotionActivityStatusEnum}
*/
private Integer status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 备注
*/
private String remark;
/**
* 条件类型
*
* 枚举 {@link PromotionConditionTypeEnum}
*/
private Integer conditionType;
/**
* 商品范围
*
* 枚举 {@link PromotionProductScopeEnum}
*/
private Integer productScope;
/**
* 商品 SPU 编号的数组
*/
private List<Long> productSpuIds;
/**
* 优惠规则的数组
*/
private List<Rule> rules;
/**
* 优惠规则
*/
@Data
public static class Rule implements Serializable {
/**
* 优惠门槛
*
* 1. N 单位
* 2. N
*/
private Integer limit;
/**
* 优惠价格单位
*/
private Integer discountPrice;
/**
* 是否包邮
*/
private Boolean freeDelivery;
/**
* 赠送的积分
*/
private Integer point;
/**
* 赠送的优惠劵编号的数组
*/
private List<Long> couponIds;
/**
* 赠送的优惠券数量的数组
*/
private List<Integer> couponCounts;
}
}

View File

@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.promotion.api.reward;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityDTO;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.app.reward.vo.AppRewardActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@ -26,4 +30,9 @@ public class RewardActivityApiImpl implements RewardActivityApi {
return rewardActivityService.getMatchRewardActivityList(spuIds);
}
@Override
public RewardActivityDTO getRewardActivityById(Long id) {
return BeanUtils.toBean(rewardActivityService.getRewardActivity(id), RewardActivityDTO.class);
}
}

View File

@ -5,6 +5,8 @@ import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.*;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
@ -57,12 +59,17 @@ public class AppTradeOrderController {
@Resource
private TradeOrderProperties tradeOrderProperties;
@Resource
private MemberUserApi memberUserApi;
@GetMapping("/settlement")
@Operation(summary = "获得订单结算信息")
@PreAuthenticated
public CommonResult<AppTradeOrderSettlementRespVO> settlementOrder(@Valid AppTradeOrderSettlementReqVO settlementReqVO) {
return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
AppTradeOrderSettlementRespVO order = tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO);
order.setTotalPoint(memberUserApi.getUser(getLoginUserId()).getPoint());
order.setUsedPoint(0);
return success(order);
}
@PostMapping("/create")

View File

@ -62,6 +62,10 @@ public class AppTradeOrderSettlementReqVO {
@Schema(description = "砍价记录编号", example = "123")
private Long bargainRecordId;
// ========== 优惠活动相关字段 ==========
@Schema(description = "满减送活动编号", example = "123")
private Long rewardId;
@AssertTrue(message = "活动商品每次只能购买一种规格")
@JsonIgnore
public boolean isValidActivityItems() {

View File

@ -31,6 +31,9 @@ public class AppTradeOrderSettlementRespVO {
@Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer totalPoint;
@Schema(description = "商品使用积分状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Integer pointsStatus;
@Schema(description = "购物项")
@Data
public static class Item {

View File

@ -21,6 +21,8 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
@ -116,6 +118,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Resource
private SocialUserApi socialUserApi;
@Resource
private ProductSpuApi productSpuApi;
// =================== Order ===================
@Override
@ -130,7 +135,21 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO);
// 3. 拼接返回
return TradeOrderConvert.INSTANCE.convert(calculateRespBO, address);
AppTradeOrderSettlementRespVO convert = TradeOrderConvert.INSTANCE.convert(calculateRespBO, address);
//默认禁用积分
convert.setPointsStatus(0);
//spu数组
ArrayList<Long> list = new ArrayList<>();
for (TradePriceCalculateRespBO.OrderItem item : calculateRespBO.getItems()) {
list.add(item.getSpuId());
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(list);
for (ProductSpuRespDTO respDTO : spuList) {
if (respDTO.getPointsStatus() == 1){
convert.setPointsStatus(1);
}
}
return convert;
}
/**

View File

@ -84,6 +84,12 @@ public class TradePriceCalculateReqBO {
*/
private Long bargainRecordId;
// ========== 满减送活动相关字段 ==========
/**
* 满减送活动编号
*/
private Long rewardId;
/**
* 商品 SKU
*/

View File

@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
@ -14,6 +17,8 @@ import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -34,6 +39,8 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
private MemberConfigApi memberConfigApi;
@Resource
private MemberUserApi memberUserApi;
@Resource
private ProductSpuApi productSpuApi;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
@ -46,17 +53,44 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
}
// 1.2 校验积分抵扣是否开启
MemberConfigRespDTO config = memberConfigApi.getConfig();
//全局
if (!isDeductPointEnable(config)) {
return;
}
//部分商品
ArrayList<Long> list = new ArrayList<>();
for (TradePriceCalculateRespBO.OrderItem item : result.getItems()) {
list.add(item.getSpuId());
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(list);
int status = 0;
int usePointMax = 0;
for (ProductSpuRespDTO respDTO : spuList) {
usePointMax += respDTO.getUsePointsMax();
if (respDTO.getPointsStatus() == 1){
status = 1;
}
}
if (status == 0){
return;
}
// 1.3 校验用户积分余额
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
if (user.getPoint() == null || user.getPoint() <= 0) {
return;
}
int calculatePointPricePar = 0;
if (user.getPoint() >= usePointMax){
calculatePointPricePar = usePointMax;
}else {
calculatePointPricePar = user.getPoint();
}
// 2.1 计算积分优惠金额
int pointPrice = calculatePointPrice(config, user.getPoint(), result);
int pointPrice = calculatePointPrice(config, calculatePointPricePar, result);
// 2.2 计算分摊的积分抵扣金额
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterList(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSelected);
List<Integer> dividePointPrices = TradePriceCalculatorHelper.dividePrice(orderItems, pointPrice);
@ -91,18 +125,18 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
// TODO @疯狂这里应该是抵扣到只剩下 0.01
// 积分优惠金额
int pointPrice = usePoint * config.getPointTradeDeductUnitPrice();
if (result.getPrice().getPayPrice() <= pointPrice) {
// 禁止 0 元购
throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
}
// // 允许0 元购!!!用户积分比较多时积分可以抵扣的金额要大于支付金额这时需要根据支付金额反推使用多少积分
// if (result.getPrice().getPayPrice() < pointPrice) {
// pointPrice = result.getPrice().getPayPrice();
// // 反推需要扣除的积分
// usePoint = NumberUtil.toBigDecimal(pointPrice)
// .divide(NumberUtil.toBigDecimal(config.getPointTradeDeductUnitPrice()), 0, RoundingMode.HALF_UP)
// .intValue();
// if (result.getPrice().getPayPrice() <= pointPrice) {
// // 禁止 0 元购
// throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
// }
// // 允许0 元购!!!用户积分比较多时积分可以抵扣的金额要大于支付金额这时需要根据支付金额反推使用多少积分
if (result.getPrice().getPayPrice() <= pointPrice) {
pointPrice = result.getPrice().getPayPrice() - 1;
// 反推需要扣除的积分
usePoint = NumberUtil.toBigDecimal(pointPrice)
.divide(NumberUtil.toBigDecimal(config.getPointTradeDeductUnitPrice()), 0, RoundingMode.HALF_UP)
.intValue();
}
// 记录使用的积分
result.setUsePoint(usePoint);
return pointPrice;

View File

@ -4,11 +4,14 @@ import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -25,6 +28,8 @@ import static java.util.Collections.singletonList;
* @author 芋道源码
*/
public class TradePriceCalculatorHelper {
@Resource
private static RewardActivityApi rewardActivityApi;
public static TradePriceCalculateRespBO buildCalculateResp(TradePriceCalculateReqBO param,
List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
@ -70,6 +75,30 @@ public class TradePriceCalculatorHelper {
result.setPrice(new TradePriceCalculateRespBO.Price());
recountAllPrice(result);
recountAllGivePoint(result);
// param.setRewardId(1L);
//满减送活动优惠价格计算
// if (param.getRewardId() != null){
// RewardActivityDTO activityDTO = rewardActivityApi.getRewardActivityById(param.getRewardId());
// TradePriceCalculateRespBO.Price price = result.getPrice();
//// List<TradePriceCalculateRespBO.OrderItem> items = result.getItems();
// int limitPrice = 0;
// int discountPrice = 0;
// Boolean deliveryPrice = null;
// for (RewardActivityDTO.Rule rule : activityDTO.getRules()) {
// if (price.getTotalPrice() >= rule.getLimit() && rule.getLimit() > limitPrice){
// limitPrice = rule.getLimit();
// discountPrice = rule.getDiscountPrice();
// deliveryPrice = rule.getFreeDelivery();
// }
// }
// price.setDiscountPrice(price.getDiscountPrice() + discountPrice);
// if (deliveryPrice != null && deliveryPrice){
// price.setDeliveryPrice(0);
// }
// price.setPayPrice(price.getPayPrice() - discountPrice);
// }
return result;
}