【功能新增】商城:积分商城
parent
6c325d99e7
commit
39ac641497
|
@ -0,0 +1,54 @@
|
|||
package cn.iocoder.yudao.module.promotion.api.point;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.enums.ApiConstants;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 秒杀活动")
|
||||
public interface PointActivityApi {
|
||||
|
||||
String PREFIX = ApiConstants.PREFIX + "/point-activity";
|
||||
|
||||
@GetMapping(PREFIX + "/validate-join")
|
||||
@Operation(summary = "【下单前】校验是否参与积分商城活动")
|
||||
@Parameters({
|
||||
@Parameter(name = "activityId", description = "活动编号", required = true, example = "1"),
|
||||
@Parameter(name = "skuId", description = "SKU 编号", required = true, example = "2"),
|
||||
@Parameter(name = "count", description = "数量", required = true, example = "3"),
|
||||
})
|
||||
CommonResult<PointValidateJoinRespDTO> validateJoinPointActivity(@RequestParam("activityId") Long activityId,
|
||||
@RequestParam("skuId") Long skuId,
|
||||
@RequestParam("count")Integer count);
|
||||
|
||||
@PutMapping(PREFIX + "/update-stock-decr")
|
||||
@Operation(summary = "更新积分商品库存(减少)")
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "活动编号", required = true, example = "1"),
|
||||
@Parameter(name = "skuId", description = "SKU 编号", required = true, example = "2"),
|
||||
@Parameter(name = "count", description = "数量", required = true, example = "3"),
|
||||
})
|
||||
CommonResult<Boolean> updatePointStockDecr(@RequestParam("id") Long id,
|
||||
@RequestParam("skuId") Long skuId,
|
||||
@RequestParam("count")Integer count);
|
||||
|
||||
@PutMapping(PREFIX + "/update-stock-incr")
|
||||
@Operation(summary = "更新积分商城商品库存(增加)")
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "活动编号", required = true, example = "1"),
|
||||
@Parameter(name = "skuId", description = "SKU 编号", required = true, example = "2"),
|
||||
@Parameter(name = "count", description = "数量", required = true, example = "3"),
|
||||
})
|
||||
CommonResult<Boolean> updatePointStockIncr(@RequestParam("id") Long id,
|
||||
@RequestParam("skuId") Long skuId,
|
||||
@RequestParam("count")Integer count);
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package cn.iocoder.yudao.module.promotion.api.point.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 校验参与积分商城 Response DTO
|
||||
*/
|
||||
@Data
|
||||
public class PointValidateJoinRespDTO {
|
||||
|
||||
/**
|
||||
* 可兑换次数
|
||||
*/
|
||||
private Integer count;
|
||||
/**
|
||||
* 所需兑换积分
|
||||
*/
|
||||
private Integer point;
|
||||
/**
|
||||
* 所需兑换金额,单位:分
|
||||
*/
|
||||
private Integer price;
|
||||
|
||||
}
|
|
@ -40,7 +40,7 @@ public interface SeckillActivityApi {
|
|||
@RequestParam("skuId") Long skuId,
|
||||
@RequestParam("count")Integer count);
|
||||
|
||||
@GetMapping("/validate-join")
|
||||
@GetMapping(PREFIX + "/validate-join")
|
||||
@Operation(summary = "【下单前】校验是否参与秒杀活动") // 如果校验失败,则抛出业务异常
|
||||
@Parameters({
|
||||
@Parameter(name = "activityId", description = "活动编号", required = true, example = "1"),
|
||||
|
|
|
@ -50,6 +50,10 @@ public interface ErrorCodeConstants {
|
|||
ErrorCode POINT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_002, "积分商城活动已关闭,不能修改");
|
||||
ErrorCode POINT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_007_003, "积分商城活动未关闭或未结束,不能删除");
|
||||
ErrorCode POINT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_004, "积分商城活动已关闭,不能重复关闭");
|
||||
ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED = new ErrorCode(1_013_007_005, "积分商品兑换失败,原因:积分商城活动已关闭");
|
||||
ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1_013_007_006, "积分商品兑换失败,原因:单次限购超出");
|
||||
ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS = new ErrorCode(1_013_007_007, "积分商品兑换失败,原因:商品不存在");
|
||||
ErrorCode POINT_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_007_008, "积分商品兑换失败,原因:积分商品库存不足");
|
||||
|
||||
// ========== 秒杀活动 1-013-008-000 ==========
|
||||
ErrorCode SECKILL_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_008_000, "秒杀活动不存在");
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package cn.iocoder.yudao.module.promotion.api.point;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.service.point.PointActivityService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 积分商城活动 Api 接口实现类
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@RestController // 提供 RESTful API 接口,给 Feign 调用
|
||||
@Validated
|
||||
public class PointActivityApiImpl implements PointActivityApi {
|
||||
|
||||
@Resource
|
||||
private PointActivityService pointActivityService;
|
||||
|
||||
@Override
|
||||
public CommonResult<PointValidateJoinRespDTO> validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
|
||||
return success(pointActivityService.validateJoinPointActivity(activityId, skuId, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> updatePointStockDecr(Long id, Long skuId, Integer count) {
|
||||
pointActivityService.updatePointStockDecr(id, skuId, count);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> updatePointStockIncr(Long id, Long skuId, Integer count) {
|
||||
pointActivityService.updatePointStockIncr(id, skuId, count);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -76,8 +76,11 @@ public class AppPointActivityController {
|
|||
|
||||
// 2. 拼接数据
|
||||
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(Collections.singletonList(id));
|
||||
AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class);
|
||||
respVO.setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class));
|
||||
PointProductDO minProduct = getMinObject(products, PointProductDO::getPoint);
|
||||
assert minProduct != null;
|
||||
AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class)
|
||||
.setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class))
|
||||
.setPoint(minProduct.getPoint()).setPrice(minProduct.getPrice());
|
||||
return success(respVO);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,14 @@ public class AppPointActivityDetailRespVO {
|
|||
@Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<Product> products;
|
||||
|
||||
//======================= 显示所需兑换积分最少的 SKU 信息 =======================
|
||||
|
||||
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer point;
|
||||
|
||||
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "商品信息")
|
||||
@Data
|
||||
public static class Product {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package cn.iocoder.yudao.module.promotion.dal.mysql.point;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
|
||||
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
|
@ -25,4 +27,33 @@ public interface PointActivityMapper extends BaseMapperX<PointActivityDO> {
|
|||
.orderByDesc(PointActivityDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活动库存(减少)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param count 扣减的库存数量(正数)
|
||||
* @return 影响的行数
|
||||
*/
|
||||
default int updateStockDecr(Long id, int count) {
|
||||
Assert.isTrue(count > 0);
|
||||
return update(null, new LambdaUpdateWrapper<PointActivityDO>()
|
||||
.eq(PointActivityDO::getId, id)
|
||||
.ge(PointActivityDO::getStock, count)
|
||||
.setSql("stock = stock - " + count));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活动库存(增加)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param count 增加的库存数量(正数)
|
||||
* @return 影响的行数
|
||||
*/
|
||||
default int updateStockIncr(Long id, int count) {
|
||||
Assert.isTrue(count > 0);
|
||||
return update(null, new LambdaUpdateWrapper<PointActivityDO>()
|
||||
.eq(PointActivityDO::getId, id)
|
||||
.setSql("stock = stock + " + count));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package cn.iocoder.yudao.module.promotion.dal.mysql.point;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
|
@ -29,4 +30,37 @@ public interface PointProductMapper extends BaseMapperX<PointProductDO> {
|
|||
.eq(PointProductDO::getActivityId, pointProductDO.getActivityId()));
|
||||
}
|
||||
|
||||
default PointProductDO selectListByActivityIdAndSkuId(Long activityId, Long skuId) {
|
||||
return selectOne(PointProductDO::getActivityId, activityId,
|
||||
PointProductDO::getSkuId, skuId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活动库存(减少)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param count 扣减的库存数量(减少库存)
|
||||
* @return 影响的行数
|
||||
*/
|
||||
default int updateStockDecr(Long id, int count) {
|
||||
Assert.isTrue(count > 0);
|
||||
return update(null, new LambdaUpdateWrapper<PointProductDO>()
|
||||
.eq(PointProductDO::getId, id)
|
||||
.ge(PointProductDO::getStock, count)
|
||||
.setSql("stock = stock - " + count));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活动库存(增加)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param count 需要增加的库存(增加库存)
|
||||
* @return 影响的行数
|
||||
*/
|
||||
default int updateStockIncr(Long id, int count) {
|
||||
Assert.isTrue(count > 0);
|
||||
return update(null, new LambdaUpdateWrapper<PointProductDO>()
|
||||
.eq(PointProductDO::getId, id)
|
||||
.setSql("stock = stock + " + count));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package cn.iocoder.yudao.module.promotion.service.point;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
|
||||
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
|
||||
|
@ -32,6 +33,24 @@ public interface PointActivityService {
|
|||
*/
|
||||
void updatePointActivity(@Valid PointActivitySaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 更新积分商城商品库存(减少)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param skuId sku 编号
|
||||
* @param count 数量(正数)
|
||||
*/
|
||||
void updatePointStockDecr(Long id, Long skuId, Integer count);
|
||||
|
||||
/**
|
||||
* 更新积分商城商品库存(增加)
|
||||
*
|
||||
* @param id 活动编号
|
||||
* @param skuId sku 编号
|
||||
* @param count 数量(正数)
|
||||
*/
|
||||
void updatePointStockIncr(Long id, Long skuId, Integer count);
|
||||
|
||||
/**
|
||||
* 关闭积分商城活动
|
||||
*
|
||||
|
@ -78,4 +97,16 @@ public interface PointActivityService {
|
|||
*/
|
||||
List<PointProductDO> getPointProductListByActivityIds(Collection<Long> activityIds);
|
||||
|
||||
/**
|
||||
* 【下单前】校验是否参与积分商城活动
|
||||
*
|
||||
* 如果校验失败,则抛出业务异常
|
||||
*
|
||||
* @param activityId 活动编号
|
||||
* @param skuId SKU 编号
|
||||
* @param count 数量
|
||||
* @return 积分商城商品信息
|
||||
*/
|
||||
PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count);
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
|
|||
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
|
||||
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.api.point.dto.PointValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
|
||||
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductSaveReqVO;
|
||||
|
@ -102,6 +103,43 @@ public class PointActivityServiceImpl implements PointActivityService {
|
|||
updateSeckillProduct(updateObj, updateReqVO.getProducts());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updatePointStockDecr(Long id, Long skuId, Integer count) {
|
||||
// 1.1 校验活动库存是否充足
|
||||
PointActivityDO activity = validatePointActivityExists(id);
|
||||
if (count > activity.getStock()) {
|
||||
throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
// 1.2 校验商品库存是否充足
|
||||
PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
|
||||
if (product == null || count > product.getStock()) {
|
||||
throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
|
||||
// 2.1 更新活动商品库存
|
||||
int updateCount = pointProductMapper.updateStockDecr(product.getId(), count);
|
||||
if (updateCount == 0) {
|
||||
throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
|
||||
// 2.2 更新活动库存
|
||||
updateCount = pointActivityMapper.updateStockDecr(id, count);
|
||||
if (updateCount == 0) {
|
||||
throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updatePointStockIncr(Long id, Long skuId, Integer count) {
|
||||
PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
|
||||
// 更新活动商品库存
|
||||
pointProductMapper.updateStockIncr(product.getId(), count);
|
||||
// 更新活动库存
|
||||
pointActivityMapper.updateStockIncr(id, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void closePointActivity(Long id) {
|
||||
|
@ -244,4 +282,28 @@ public class PointActivityServiceImpl implements PointActivityService {
|
|||
return pointProductMapper.selectListByActivityId(activityIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
|
||||
// 1. 校验积分商城活动是否存在
|
||||
PointActivityDO activity = validatePointActivityExists(activityId);
|
||||
if (CommonStatusEnum.isDisable(activity.getStatus())) {
|
||||
throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED);
|
||||
}
|
||||
|
||||
// 2.1 校验积分商城商品是否存在
|
||||
PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(activityId, skuId);
|
||||
if (product == null) {
|
||||
throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
// 2.2 超过单次购买限制
|
||||
if (count > product.getCount()) {
|
||||
throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED);
|
||||
}
|
||||
// 2.2 校验库存是否充足
|
||||
if (count > product.getStock()) {
|
||||
throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
return BeanUtils.toBean(product, PointValidateJoinRespDTO.class);
|
||||
}
|
||||
|
||||
}
|
|
@ -160,7 +160,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
|
|||
public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
|
||||
// 1.1 校验活动库存是否充足
|
||||
SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
|
||||
if (count > seckillActivity.getTotalStock()) {
|
||||
if (count > seckillActivity.getStock()) {
|
||||
throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
|
||||
}
|
||||
// 1.2 校验商品库存是否充足
|
||||
|
|
|
@ -58,11 +58,12 @@ public interface ErrorCodeConstants {
|
|||
|
||||
// ========== Price 相关 1-011-003-000 ============
|
||||
ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1_011_003_000, "支付价格计算异常,原因:价格小于等于 0");
|
||||
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
|
||||
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
|
||||
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
|
||||
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
|
||||
ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
|
||||
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_001, "计算快递运费异常,找不到对应的运费模板");
|
||||
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_002, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
|
||||
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_003, "参与秒杀的商品,超过了秒杀总限购数量");
|
||||
ErrorCode PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_004, "参与积分活动的商品,超过了积分活动商品总限购数量");
|
||||
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_005, "计算快递运费异常,配送方式不匹配");
|
||||
ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_006, "该优惠劵无法使用,原因:{}」");
|
||||
|
||||
// ========== 物流 Express 模块 1-011-004-000 ==========
|
||||
ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");
|
||||
|
|
|
@ -20,6 +20,7 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
|
|||
SECKILL(1, "秒杀订单"),
|
||||
BARGAIN(2, "砍价订单"),
|
||||
COMBINATION(3, "拼团订单"),
|
||||
POINT(4, "积分商城"),
|
||||
;
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderTypeEnum::getType).toArray();
|
||||
|
@ -54,4 +55,8 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
|
|||
return ObjectUtil.equal(type, COMBINATION.getType());
|
||||
}
|
||||
|
||||
public static boolean isPoint(Integer type) {
|
||||
return ObjectUtil.equal(type, POINT.getType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -62,6 +62,10 @@ public class AppTradeOrderSettlementReqVO {
|
|||
@Schema(description = "砍价记录编号", example = "123")
|
||||
private Long bargainRecordId;
|
||||
|
||||
// ========== 积分商城活动相关字段 ==========
|
||||
@Schema(description = "积分商城活动编号", example = "123")
|
||||
private Long pointActivityId;
|
||||
|
||||
@AssertTrue(message = "活动商品每次只能购买一种规格")
|
||||
@JsonIgnore
|
||||
public boolean isValidActivityItems() {
|
||||
|
|
|
@ -219,7 +219,8 @@ public interface TradeOrderConvert {
|
|||
.setSeckillActivityId(settlementReqVO.getSeckillActivityId())
|
||||
.setBargainRecordId(settlementReqVO.getBargainRecordId())
|
||||
.setCombinationActivityId(settlementReqVO.getCombinationActivityId())
|
||||
.setCombinationHeadId(settlementReqVO.getCombinationHeadId());
|
||||
.setCombinationHeadId(settlementReqVO.getCombinationHeadId())
|
||||
.setPointActivityId(settlementReqVO.getPointActivityId());
|
||||
// 商品项的构建
|
||||
Map<Long, CartDO> cartMap = convertMap(cartList, CartDO::getId);
|
||||
for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) {
|
||||
|
|
|
@ -353,4 +353,11 @@ public class TradeOrderDO extends BaseDO {
|
|||
*/
|
||||
private Long combinationRecordId;
|
||||
|
||||
/**
|
||||
* 积分商城活动的编号
|
||||
*
|
||||
* 关联 PointActivityDO 的 id 字段
|
||||
*/
|
||||
private Long pointActivityId;
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi;
|
|||
import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi;
|
||||
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
|
||||
|
@ -26,7 +27,7 @@ import org.springframework.context.annotation.Configuration;
|
|||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = {
|
||||
BargainActivityApi.class, BargainRecordApi.class, CombinationRecordApi.class,
|
||||
CouponApi.class, DiscountActivityApi.class, RewardActivityApi.class, SeckillActivityApi.class,
|
||||
CouponApi.class, DiscountActivityApi.class, RewardActivityApi.class, SeckillActivityApi.class, PointActivityApi.class,
|
||||
MemberUserApi.class, MemberPointApi.class, MemberLevelApi.class, MemberAddressApi.class, MemberConfigApi.class,
|
||||
ProductSpuApi.class, ProductSkuApi.class, ProductCommentApi.class, ProductCategoryApi.class,
|
||||
PayOrderApi.class, PayRefundApi.class,
|
||||
|
|
|
@ -245,7 +245,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
|
|||
}
|
||||
|
||||
// 3. 生成预支付
|
||||
// 特殊情况:积分兑换时,可能支付金额为零
|
||||
if (order.getPayPrice() > 0) {
|
||||
createPayOrder(order, orderItems);
|
||||
}
|
||||
|
||||
// 4. 插入订单日志
|
||||
TradeOrderLogUtils.setOrderInfo(order.getId(), null, order.getStatus());
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package cn.iocoder.yudao.module.trade.service.order.handler;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
|
||||
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 积分商城活动订单的 {@link TradeOrderHandler} 实现类
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
@Component
|
||||
public class TradePointOrderHandler implements TradeOrderHandler {
|
||||
|
||||
@Resource
|
||||
private PointActivityApi pointActivityApi;
|
||||
|
||||
@Override
|
||||
public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
|
||||
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
|
||||
return;
|
||||
}
|
||||
// 明确校验一下
|
||||
Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
|
||||
|
||||
// 扣减积分商城活动的库存
|
||||
pointActivityApi.updatePointStockDecr(order.getPointActivityId(),
|
||||
orderItems.get(0).getSkuId(), orderItems.get(0).getCount()).getCheckedData();
|
||||
|
||||
// 如果支付金额为 0,则直接设置为已支付
|
||||
if (Objects.equals(order.getPayPrice(), 0)) {
|
||||
order.setPayStatus(true).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
|
||||
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
|
||||
return;
|
||||
}
|
||||
// 明确校验一下
|
||||
Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
|
||||
|
||||
// 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
|
||||
orderItems = filterOrderItemListByNoneAfterSale(orderItems);
|
||||
if (CollUtil.isEmpty(orderItems)) {
|
||||
return;
|
||||
}
|
||||
afterCancelOrderItem(order, orderItems.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
|
||||
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
|
||||
return;
|
||||
}
|
||||
// 恢复积分商城活动的库存
|
||||
pointActivityApi.updatePointStockIncr(order.getPointActivityId(),
|
||||
orderItem.getSkuId(), orderItem.getCount()).getCheckedData();
|
||||
}
|
||||
|
||||
}
|
|
@ -69,7 +69,8 @@ public class TradePriceServiceImpl implements TradePriceService {
|
|||
.buildCalculateResp(calculateReqBO, spuList, skuList);
|
||||
priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO));
|
||||
// 2.2 如果最终支付金额小于等于 0,则抛出业务异常
|
||||
if (calculateRespBO.getPrice().getPayPrice() <= 0) {
|
||||
if (calculateReqBO.getPointActivityId() == null // 积分订单,允许支付金额为 0
|
||||
&& calculateRespBO.getPrice().getPayPrice() <= 0) {
|
||||
log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]",
|
||||
calculateReqBO, calculateRespBO);
|
||||
throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
|
||||
|
|
|
@ -84,6 +84,12 @@ public class TradePriceCalculateReqBO {
|
|||
*/
|
||||
private Long bargainRecordId;
|
||||
|
||||
// ========== 积分商城活动相关字段 ==========
|
||||
/**
|
||||
* 积分商城活动编号
|
||||
*/
|
||||
private Long pointActivityId;
|
||||
|
||||
/**
|
||||
* 商品 SKU
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package cn.iocoder.yudao.module.trade.service.price.calculator;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
|
||||
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
|
||||
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
|
||||
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT;
|
||||
|
||||
/**
|
||||
* 积分商城的 {@link TradePriceCalculator} 实现类
|
||||
*
|
||||
* @author owen
|
||||
*/
|
||||
@Component
|
||||
@Order(TradePriceCalculator.ORDER_POINT_ACTIVITY)
|
||||
@Slf4j
|
||||
public class TradePointActivityPriceCalculator implements TradePriceCalculator {
|
||||
|
||||
@Resource
|
||||
private PointActivityApi pointActivityApi;
|
||||
@Resource
|
||||
private MemberUserApi memberUserApi;
|
||||
|
||||
@Resource
|
||||
private TradeOrderQueryService tradeOrderQueryService;
|
||||
|
||||
@Override
|
||||
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
|
||||
// 1.1 判断订单类型是否为积分商城活动
|
||||
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
|
||||
return;
|
||||
}
|
||||
// 1.2 初始化积分
|
||||
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId()).getCheckedData();
|
||||
result.setTotalPoint(user.getPoint()).setUsePoint(0);
|
||||
|
||||
// 1.3 校验用户积分余额
|
||||
if (user.getPoint() == null || user.getPoint() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.isTrue(param.getItems().size() == 1, "积分商城兑换商品时,只允许选择一个商品");
|
||||
// 2. 校验是否可以参与积分商城活动
|
||||
TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0);
|
||||
PointValidateJoinRespDTO activity = validateJoinSeckill(
|
||||
param.getUserId(), param.getPointActivityId(),
|
||||
orderItem.getSkuId(), orderItem.getCount());
|
||||
|
||||
// 3.1 记录优惠明细
|
||||
int discountPrice = orderItem.getPayPrice(); // 情况一:单使用积分兑换
|
||||
Assert.isTrue(activity.getPoint() >= 1, "积分商城商品兑换积分必须大于 1");
|
||||
result.setUsePoint(activity.getPoint() * orderItem.getCount());
|
||||
orderItem.setUsePoint(activity.getPoint() * orderItem.getCount());
|
||||
if (activity.getPrice() != null && activity.getPrice() > 0) { // 情况二:积分 + 金额
|
||||
discountPrice = orderItem.getPayPrice() - activity.getPrice() * orderItem.getCount();
|
||||
}
|
||||
// 3.2 记录优惠明细
|
||||
TradePriceCalculatorHelper.addPromotion(result, orderItem,
|
||||
param.getPointActivityId(), "积分商城活动", PromotionTypeEnum.POINT.getType(),
|
||||
StrUtil.format("积分商城活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)),
|
||||
discountPrice);
|
||||
|
||||
// 3.3 更新 SKU 优惠金额
|
||||
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
|
||||
TradePriceCalculatorHelper.recountPayPrice(orderItem);
|
||||
TradePriceCalculatorHelper.recountAllPrice(result);
|
||||
}
|
||||
|
||||
private PointValidateJoinRespDTO validateJoinSeckill(Long userId, Long activityId, Long skuId, Integer count) {
|
||||
// 1. 校验是否可以参与积分商城活动
|
||||
PointValidateJoinRespDTO pointValidateJoinRespDTO = pointActivityApi.validateJoinPointActivity(activityId, skuId, count).getCheckedData();
|
||||
// 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用
|
||||
int activityProductCount = tradeOrderQueryService.getSeckillProductCount(userId, activityId);
|
||||
if (activityProductCount + count > pointValidateJoinRespDTO.getCount()) {
|
||||
throw exception(PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT);
|
||||
}
|
||||
return pointValidateJoinRespDTO;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
package cn.iocoder.yudao.module.trade.service.price.calculator;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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.promotion.enums.common.PromotionTypeEnum;
|
||||
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 jakarta.annotation.Resource;
|
||||
|
@ -37,6 +39,10 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
|
|||
|
||||
@Override
|
||||
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
|
||||
// 判断订单类型是否不为积分商城活动
|
||||
if (ObjectUtil.equal(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
|
||||
return;
|
||||
}
|
||||
// 0. 初始化积分
|
||||
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId()).getCheckedData();
|
||||
result.setTotalPoint(user.getPoint()).setUsePoint(0);
|
||||
|
|
|
@ -16,6 +16,7 @@ public interface TradePriceCalculator {
|
|||
int ORDER_SECKILL_ACTIVITY = 8;
|
||||
int ORDER_BARGAIN_ACTIVITY = 8;
|
||||
int ORDER_COMBINATION_ACTIVITY = 8;
|
||||
int ORDER_POINT_ACTIVITY = 8;
|
||||
|
||||
int ORDER_DISCOUNT_ACTIVITY = 10;
|
||||
int ORDER_REWARD_ACTIVITY = 20;
|
||||
|
|
|
@ -90,6 +90,9 @@ public class TradePriceCalculatorHelper {
|
|||
if (param.getBargainRecordId() != null) {
|
||||
return TradeOrderTypeEnum.BARGAIN.getType();
|
||||
}
|
||||
if (param.getPointActivityId() != null) {
|
||||
return TradeOrderTypeEnum.POINT.getType();
|
||||
}
|
||||
return TradeOrderTypeEnum.NORMAL.getType();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue