Merge remote-tracking branch 'origin/main'

# Conflicts:
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java
This commit is contained in:
zhangji committed 2025-07-17 14:23:08 +08:00
commit b0d6e56c42
16 files changed
+211 -29

No files matched your search

@@ -68,6 +68,12 @@ public class ProductSkuRespDTO {
* 商品体积,单位:m^3 平米
*/
private Double volume;
/**
* 限购类型
*/
private String purchaseBizType;
/**
* 一级分销的佣金,单位:分
*/
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.product.enums.sku;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 限购类型
*/
@Getter
@AllArgsConstructor
public enum ProductSkuPurchaseBizTypeEnum implements ArrayValuable<String> {
NONE("0", "不限购"),
ONCE_A_DAY("1", "限购一天一次"),
;
public static final String[] ARRAYS = Arrays.stream(values()).map(ProductSkuPurchaseBizTypeEnum::getStatus).toArray(String[]::new);
/**
* 状态
*/
private final String status;
/**
* 状态名
*/
private final String name;
@Override
public String[] array() {
return ARRAYS;
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.product.controller.admin.spu.vo;
import cn.iocoder.yudao.module.product.enums.sku.ProductSkuPurchaseBizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -39,6 +40,9 @@ public class ProductSkuRespVO {
@Schema(description = "商品体积,单位:m^3 平米", example = "2.5")
private Double volume;
@Schema(description = "限购类型(不能为空)", example = "0")
private String purchaseBizType;
@Schema(description = "一级分销的佣金,单位:分", example = "199")
private Integer firstBrokeragePrice;
@@ -44,6 +44,9 @@ public class ProductSkuSaveReqVO {
@Schema(description = "商品体积,单位:m^3 平米", example = "2.5")
private Double volume;
@Schema(description = "限购类型(不能为空)", example = "0")
private String purchaseBizType;
@Schema(description = "一级分销的佣金,单位:分", example = "199")
private Integer firstBrokeragePrice;
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyDO;
import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyValueDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.enums.sku.ProductSkuPurchaseBizTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -35,7 +36,7 @@ public class ProductSkuDO extends BaseDO {
private Long id;
/**
* SPU 编号
*
* <p>
* 关联 {@link ProductSpuDO#getId()}
*/
private Long spuId;
@@ -103,6 +104,13 @@ public class ProductSkuDO extends BaseDO {
*/
private String cabinetType;
/**
* 限购类型(不能为空)
*
* @see ProductSkuPurchaseBizTypeEnum
*/
private String purchaseBizType;
/**
* 商品属性
*/
@@ -119,7 +127,7 @@ public class ProductSkuDO extends BaseDO {
/**
* 属性名字
* 冗余 {@link ProductPropertyDO#getName()}
*
* <p>
* 注意:每次属性名字发生变化时,需要更新该冗余
*/
private String propertyName;
@@ -132,7 +140,7 @@ public class ProductSkuDO extends BaseDO {
/**
* 属性值名字
* 冗余 {@link ProductPropertyValueDO#getName()}
*
* <p>
* 注意:每次属性值名字发生变化时,需要更新该冗余
*/
private String valueName;
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSkuSaveReqVO;
import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
@@ -11,6 +12,7 @@ import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyDO
import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyValueDO;
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
import cn.iocoder.yudao.module.product.dal.mysql.sku.ProductSkuMapper;
import cn.iocoder.yudao.module.product.enums.sku.ProductSkuPurchaseBizTypeEnum;
import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
@@ -39,6 +39,7 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_034, "交易订单自提失败,原因:你没有核销该门店订单的权限");
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_035, "交易订单创建失败,原因:用户积分不足");
ErrorCode ORDER_RECEIVE_FAIL_STATUS_NOT_OVERPAY = new ErrorCode(1_011_000_036, "提货柜订单完成失败,订单不是【已支付】状态");
ErrorCode ORDER_RECEIVE_FAIL_STATUS_PURCHASE = new ErrorCode(1_011_000_037, "您购买的商品:{},是{}");
// ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@@ -31,6 +31,7 @@ import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -63,6 +64,14 @@ public class AppTradeOrderController {
return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
}
@PostMapping("/judgePurchase")
@Operation(summary = "判断是否为限购商品")
public CommonResult<Boolean> judgePurchase(@RequestBody @Valid AppTradeOrderJudgePurchaseRespVo settlementReqVO) {
List<Long> skuList = settlementReqVO.getItems().stream().map(AppTradeOrderJudgePurchaseRespVo.Item::getSkuId).collect(Collectors.toList());
tradeOrderUpdateService.isSkuPurchaseLimit(getLoginUserId(), skuList);
return success(true);
}
@GetMapping("/settlement-product")
@Operation(summary = "获得商品结算信息", description = "用于商品列表、商品详情,获得参与活动后的价格信息")
@Parameter(name = "spuIds", description = "商品 SPU 编号数组")
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
public class AppTradeOrderJudgePurchaseRespVo {
@Schema(description = "商品项数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "商品不能为空")
private List<Item> items;
@Data
@Schema(description = "用户 App - 商品项")
@Valid
public static class Item {
@Schema(description = "商品 SKU 编号", example = "2048")
@NotNull(message = "商品 SKU 编号不能为空")
private Long skuId;
@Schema(description = "购买数量", example = "1")
@Min(value = 1, message = "购买数量最小值为 {value}")
private Integer count;
@Schema(description = "购物车项的编号", example = "1024")
private Long cartId;
@AssertTrue(message = "商品不正确")
@JsonIgnore
public boolean isValid() {
// 组合一:skuId + count 使用商品 SKU
if (skuId != null && count != null) {
return true;
}
// 组合二:cartId 使用购物车项
return cartId != null;
}
}
}
@@ -10,10 +10,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
@Mapper
public interface TradeOrderItemMapper extends BaseMapperX<TradeOrderItemDO> {
@@ -52,5 +49,4 @@ public interface TradeOrderItemMapper extends BaseMapperX<TradeOrderItemDO> {
// 获得数量
return CollUtil.getFirst(result) != null ? MapUtil.getInt(result.get(0), "sumCount") : 0;
}
}
@@ -23,6 +23,8 @@ public interface TradeOrderUpdateService {
// =================== Order ===================
void isSkuPurchaseLimit(Long payUserId, List<Long> skuIdList);
/**
* 获得订单结算信息
*
@@ -51,11 +53,11 @@ public interface TradeOrderUpdateService {
/**
* 同步订单的支付状态
*
* <p>
* 1. Quietly 表示,即使同步失败,也不会抛出异常
* 2. 什么时候回出现异常?因为是主动同步,可能和支付模块的回调通知 {@link #updateOrderPaid(Long, Long)} 存在并发冲突,导致抛出异常
*
* @param id 订单编号
* @param id 订单编号
* @param payOrderId 支付订单编号
*/
void syncOrderPayStatusQuietly(Long id, Long payOrderId);
@@ -130,14 +132,14 @@ public interface TradeOrderUpdateService {
* 【管理员】核销订单
*
* @param userId 管理员编号
* @param id 订单编号
* @param id 订单编号
*/
void pickUpOrderByAdmin(Long userId, Long id);
/**
* 【管理员】核销订单
*
* @param userId 管理员编号
* @param userId 管理员编号
* @param pickUpVerifyCode 自提核销码
*/
void pickUpOrderByAdmin(Long userId, String pickUpVerifyCode);
@@ -203,9 +205,9 @@ public interface TradeOrderUpdateService {
/**
* 取消支付订单
*
* @param userId 用户编号
* @param orderId 订单编号
* @param cancelType 取消类型
* @param userId 用户编号
* @param orderId 订单编号
* @param cancelType 取消类型
*/
void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.service.order;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@@ -13,6 +14,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
@@ -23,6 +25,9 @@ import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
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.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.enums.sku.ProductSkuPurchaseBizTypeEnum;
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
@@ -57,6 +62,7 @@ import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -64,11 +70,13 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -119,12 +127,58 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
public SocialClientApi socialClientApi;
@Resource
public PayRefundApi payRefundApi;
@Resource
public ProductSkuApi productSkuApi;
@Resource
private TradeOrderProperties tradeOrderProperties;
// =================== Order ===================
public List<TradeOrderItemDO> findUserSuccessOrderItem(Long payUserId, List<Long> skuIdList, ProductSkuPurchaseBizTypeEnum status) {
List<Integer> statusList = new ArrayList<>();
statusList.add(TradeOrderStatusEnum.UNPAID.getStatus());
statusList.add(TradeOrderStatusEnum.UNDELIVERED.getStatus());
statusList.add(TradeOrderStatusEnum.DELIVERED.getStatus());
statusList.add(TradeOrderStatusEnum.COMPLETED.getStatus());
// 查询用户订单
LambdaQueryWrapperX<TradeOrderDO> orderQuery = new LambdaQueryWrapperX<TradeOrderDO>().eq(TradeOrderDO::getUserId, payUserId).in(TradeOrderDO::getStatus, statusList);
// 限购一天一次
if (ObjectUtil.equals(status, ProductSkuPurchaseBizTypeEnum.ONCE_A_DAY)) {
orderQuery.between(TradeOrderDO::getCreateTime, LocalDate.now(), LocalDate.now().plusDays(1));
}
List<TradeOrderDO> orderList = tradeOrderMapper.selectList(orderQuery);
if (ObjectUtil.isEmpty(orderList)) return null;
List<Long> orderIds = orderList.stream().map(TradeOrderDO::getId).collect(Collectors.toList());
// 查询订单项
List<TradeOrderItemDO> orderItemList = tradeOrderItemMapper.selectList(new LambdaQueryWrapperX<TradeOrderItemDO>()
.eq(TradeOrderItemDO::getUserId, payUserId)
.in(TradeOrderItemDO::getOrderId, orderIds)
);
if (ObjectUtil.isEmpty(orderItemList)) return null;
orderItemList = orderItemList.stream().filter(v -> skuIdList.contains(v.getSkuId())).collect(Collectors.toList());
return orderItemList;
}
/**
* 判断sku是否限购
*/
@Override
public void isSkuPurchaseLimit(Long payUserId, List<Long> skuIdList) {
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIdList);
// 检查每天限购一次
ProductSkuPurchaseBizTypeEnum onceADayStatus = ProductSkuPurchaseBizTypeEnum.ONCE_A_DAY;
List<ProductSkuRespDTO> onceADay = skuList.stream().filter((v) -> ObjectUtil.equals(v.getPurchaseBizType(), onceADayStatus.getStatus())).collect(Collectors.toList());
if (ObjectUtil.isNotEmpty(onceADay)) {
List<Long> targetIdList = onceADay.stream().map(ProductSkuRespDTO::getId).collect(Collectors.toList());
List<TradeOrderItemDO> targetSkuList = findUserSuccessOrderItem(payUserId, targetIdList, onceADayStatus);
if (ObjectUtil.isNotEmpty(targetSkuList)) {
String spuName = targetSkuList.stream().map(TradeOrderItemDO::getSpuName).collect(Collectors.joining());
throw exception(ORDER_RECEIVE_FAIL_STATUS_PURCHASE, "【" + spuName + "】", onceADayStatus.getName());
}
}
}
@Override
public AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) {
// 1. 获得收货地址
@@ -133,6 +187,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
settlementReqVO.setAddressId(address.getId());
}
// TODO 新需求 查看商品有没有限购
List<Long> skuList = settlementReqVO.getItems().stream().map(AppTradeOrderSettlementReqVO.Item::getSkuId).collect(Collectors.toList());
isSkuPurchaseLimit(userId, skuList);
// 2. 计算价格
TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO);
@@ -251,7 +309,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
// 3. 生成预支付
// 特殊情况:积分兑换时,可能支付金额为零
if (order.getPayPrice() > 0) {
// TODO 特殊需求,价格可以为0
if (order.getPayPrice() >= 0) {
createPayOrder(order, orderItems);
}
@@ -53,7 +53,7 @@ public class PayOrderCreateReqDTO implements Serializable {
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
@DecimalMin(value = "0", inclusive = true, message = "支付金额必须大于零")
private Integer price;
/**
@@ -63,7 +63,7 @@ public class PayOrderUnifiedReqDTO {
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
@DecimalMin(value = "0", inclusive = true, message = "支付金额必须大于零")
private Integer price;
/**
@@ -79,8 +79,8 @@ spring:
redis:
host: 127.0.0.1 # 地址
port: 6379 # 端口
database: 0 # 数据库索引
password: 123456 # 密码,建议生产环境开启
database: 10 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
--- #################### 定时任务相关配置 ####################
@@ -209,8 +209,12 @@ wx:
# secret: 6f270509224a7ae1296bbf1c8cb97aed
# appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的)
# secret: 4a1a04e07f6a4a0751b39c3064a92c8b
appid: wx66186af0759f47c9 # 测试号(puhui 提供的)
secret: 3218bcbd112cbc614c7264ceb20144ac
# 测试
# appid: wx66186af0759f47c9
# secret: 3218bcbd112cbc614c7264ceb20144ac
# 正式
appid: wx741ef9b42243284a
secret: 3b211e4bf0eeaaf27c8a369bcd10c9fb
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
key-prefix: wa # Redis Key 的前缀
@@ -3,8 +3,8 @@ spring:
name: yudao-server
profiles:
active: prod
# active: local
# active: prod
active: local
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
@@ -71,9 +71,9 @@ mybatis-plus:
global-config:
db-config:
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false # 关闭控制台的 Banner 打印
@@ -219,13 +219,13 @@ yudao:
model: Baichuan4-Turbo
midjourney:
enable: true
# base-url: https://api.holdai.top/mj-relax/mj
# base-url: https://api.holdai.top/mj-relax/mj
base-url: https://api.holdai.top/mj
api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
suno:
enable: true
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
base-url: http://127.0.0.1:3001
--- #################### 芋道相关配置 ####################