refactor: 合并 v2 版本业务代码更新

This commit is contained in:
郝先瑞 2023-09-27 18:21:37 +08:00
parent 8b22ffdef0
commit de86041995
131 changed files with 2159 additions and 1371 deletions

View File

@ -1,12 +0,0 @@
package com.youlai.mall.oms.dto;
import lombok.Data;
@Data
public class OrderInfoDTO {
private String orderSn;
private Integer status;
}

View File

@ -1,31 +0,0 @@
package com.youlai.mall.oms.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeataOrderDTO {
/**
* 会员ID
*/
private Long memberId;
/**
* 商品ID
*/
private Long skuId;
/**
* 订单金额
*/
private Long amount;
private Boolean openEx;
}

View File

@ -0,0 +1,28 @@
package com.youlai.mall.oms.constant;
/**
* 订单相关常量
*
* <p>该接口定义了与订单相关的常量</p>
*
* @author haoxr
* @since 2.0.0
*/
public interface OrderConstants {
/**
* 会员购物车缓存键前缀
*/
String MEMBER_CART_PREFIX = "order:cart:";
/**
* 订单防重提交令牌缓存键前缀
*/
String ORDER_TOKEN_PREFIX = "order:token:";
/**
* 订单锁缓存键前缀
*/
String ORDER_LOCK_PREFIX = "order:lock";
}

View File

@ -8,11 +8,12 @@ import com.youlai.mall.oms.model.dto.OrderDTO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.service.OrderItemService;
import com.youlai.mall.oms.model.vo.OmsOrderPageVO;
import com.youlai.mall.oms.service.admin.OmsOrderService;
import com.youlai.mall.oms.service.app.OrderItemService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -24,12 +25,12 @@ import java.util.List;
import java.util.Optional;
/**
* 管理端订单控制层
* 管理端-订单控制层
*
* @author huawei
* @since 2020/12/30
* @since 2.3.0
*/
@Tag(name = "「管理端」订单管理")
@Tag(name = "「管理端」订单管理")
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
@ -41,15 +42,15 @@ public class OmsOrderController {
@Operation(summary ="订单分页列表")
@GetMapping
public PageResult listOrderPages(OrderPageQuery queryParams) {
IPage<OmsOrder> result = orderService.listOrderPages(queryParams);
return PageResult.success(result);
public PageResult<OmsOrderPageVO> getOrderPage(OrderPageQuery queryParams) {
IPage<OmsOrderPageVO> page = orderService.getOrderPage(queryParams);
return PageResult.success(page);
}
@Operation(summary= "订单详情")
@Operation(summary = "订单详情")
@GetMapping("/{orderId}")
public Result getOrderDetail(
@Parameter(name = "订单ID") @PathVariable Long orderId
@Parameter(name ="订单ID") @PathVariable Long orderId
) {
OrderDTO orderDTO = new OrderDTO();
// 订单
@ -64,5 +65,4 @@ public class OmsOrderController {
orderDTO.setOrder(order).setOrderItems(orderItems);
return Result.success(orderDTO);
}
}

View File

@ -3,7 +3,7 @@ package com.youlai.mall.oms.controller.app;
import com.youlai.common.result.Result;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.mall.oms.model.dto.CartItemDTO;
import com.youlai.mall.oms.service.CartService;
import com.youlai.mall.oms.service.app.CartService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -16,11 +16,10 @@ import java.util.List;
* 移动端购物车接口
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
*/
@Tag(name = "「移动端」购物车接口")
@Tag(name = "「移动端」购物车接口")
@RestController
@RequestMapping("/app-api/v1/carts")
@RequiredArgsConstructor
@ -28,28 +27,28 @@ public class CartController {
private final CartService cartService;
@Operation(summary= "查询购物车")
@Operation(summary = "查询购物车")
@GetMapping
public <T> Result<T> getCart() {
List<CartItemDTO> result = cartService.listCartItems(SecurityUtils.getMemberId());
return Result.success((T) result);
}
@Operation(summary= "删除购物车")
@Operation(summary = "删除购物车")
@DeleteMapping
public <T> Result<T> deleteCart() {
boolean result = cartService.deleteCart();
return Result.judge(result);
}
@Operation(summary= "添加购物车商品")
@Operation(summary = "添加购物车商品")
@PostMapping
public <T> Result<T> addCartItem(@RequestParam Long skuId) {
cartService.addCartItem(skuId);
return Result.success();
}
@Operation(summary= "更新购物车商品")
@Operation(summary = "更新购物车商品")
@PutMapping("/skuId/{skuId}")
public <T> Result<T> updateCartItem(
@PathVariable Long skuId,
@ -60,17 +59,17 @@ public class CartController {
return Result.judge(result);
}
@Operation(summary= "删除购物车商品")
@Operation(summary = "删除购物车商品")
@DeleteMapping("/skuId/{skuId}")
public <T> Result<T> removeCartItem(@PathVariable Long skuId) {
boolean result = cartService.removeCartItem(skuId);
return Result.judge(result);
}
@Operation(summary= "全选/全不选购物车商品")
@Operation(summary = "全选/全不选购物车商品")
@PatchMapping("/_check")
public <T> Result<T> check(
@Parameter(name = "全选/全不选") boolean checked
@Parameter(name ="全选/全不选") boolean checked
) {
boolean result = cartService.checkAll(checked);
return Result.judge(result);

View File

@ -3,74 +3,68 @@ package com.youlai.mall.oms.controller.app;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.common.result.PageResult;
import com.youlai.common.result.Result;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.form.OrderPaymentForm;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OrderConfirmVO;
import com.youlai.mall.oms.model.vo.OrderSubmitResultVO;
import com.youlai.mall.oms.service.OrderService;
import com.youlai.mall.oms.model.vo.OrderPageVO;
import com.youlai.mall.oms.service.app.OrderService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 移动端订单控制层
* APP-订单控制层
*
* @author huawei
* @since 2020/12/30
*/
@Tag(name = "「移动端」订单接口")
@Tag(name = "APP-订单接口")
@RestController
@RequestMapping("/app-api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
final OrderService orderService;
private final OrderService orderService;
@Operation(summary ="分页列表")
@Operation(summary ="订单分页列表")
@GetMapping
public PageResult listOrderPages(OrderPageQuery queryParams) {
IPage<OmsOrder> result = orderService.listOrderPages(queryParams);
public PageResult<OrderPageVO> getOrderPage(OrderPageQuery queryParams) {
IPage<OrderPageVO> result = orderService.getOrderPage(queryParams);
return PageResult.success(result);
}
/**
* 订单确认 进入创建订单页面
* <p>
* 获取购买商品明细用户默认收货地址防重提交唯一token
* 进入订单创建页面有两个入口1立即购买2购物车结算
*
* @param skuId 直接购买必填购物车结算不填
* @return
*/
@Operation(summary ="订单确认")
@PostMapping("/_confirm")
public Result<OrderConfirmVO> confirmOrder(@RequestParam(required = false) Long skuId) {
@Operation(summary = "订单确认", description = "进入订单确认页面有两个入口1立即购买2购物车结算")
@PostMapping("/confirm")
public Result<OrderConfirmVO> confirmOrder(
@Parameter(name ="立即购买必填,购物车结算不填") @RequestParam(required = false) Long skuId
) {
OrderConfirmVO result = orderService.confirmOrder(skuId);
return Result.success(result);
}
@Operation(summary ="订单提交")
@PostMapping("/_submit")
public Result submitOrder(@RequestBody @Validated OrderSubmitForm orderSubmitForm) {
OrderSubmitResultVO result = orderService.submitOrder(orderSubmitForm);
return Result.success(result);
@PostMapping("/submit")
public Result<String> submitOrder(@Validated @RequestBody OrderSubmitForm submitForm) {
String orderSn = orderService.submitOrder(submitForm);
return Result.success(orderSn);
}
@Operation(summary ="订单支付")
@PostMapping("/{orderId}/_pay")
public Result payOrder(@PathVariable Long orderId) {
boolean result = orderService.payOrder(orderId);
@PostMapping("/payment")
public Result payOrder(@Validated @RequestBody OrderPaymentForm paymentForm) {
boolean result = orderService.payOrder(paymentForm);
return Result.judge(result);
}
@Operation(summary ="订单删除")
@DeleteMapping("/{orderId}")
public Result deleteOrder(@PathVariable Long orderId) {
boolean result = orderService.deleteOrder(orderId);
return Result.judge(result);
boolean deleted = orderService.deleteOrder(orderId);
return Result.judge(deleted);
}
}

View File

@ -4,7 +4,7 @@ import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.youlai.mall.oms.model.vo.WxPayResponseVO;
import com.youlai.mall.oms.service.OrderService;
import com.youlai.mall.oms.service.app.OrderService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@ -0,0 +1,24 @@
package com.youlai.mall.oms.converter;
import com.youlai.mall.oms.model.dto.CartItemDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
/**
* 购物车对象转化器
*
* @author haoxr
* @since 2.0.0
*/
@Mapper(componentModel = "spring")
public interface CartConverter {
@Mappings({
@Mapping(target = "skuId", source = "id"),
})
CartItemDTO sku2CartItem(SkuInfoDTO skuInfo);
}

View File

@ -1,25 +1,81 @@
package com.youlai.mall.oms.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.mall.oms.model.bo.OrderBO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import com.youlai.mall.oms.model.vo.OmsOrderPageVO;
import com.youlai.mall.oms.model.vo.OrderPageVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
/**
* 订单转化器
* 订单对象转化器
*
* @author haoxr
* @since 2022/12/21
* @since 2.0.0
*/
@Mapper(componentModel = "spring")
public interface OrderConverter {
@Mappings({
@Mapping(target = "orderSn", source = "orderToken"),
@Mapping(target = "totalQuantity", expression = "java(orderSubmitForm.getOrderItems().stream().map(OrderItemDTO::getCount).reduce(0, Integer::sum))"),
@Mapping(target = "totalAmount", expression = "java(orderSubmitForm.getOrderItems().stream().map(item -> item.getPrice() * item.getCount()).reduce(0L, Long::sum))"),
@Mapping(target = "totalQuantity",
expression = "java(orderSubmitForm.getOrderItems().stream().map(OrderSubmitForm.OrderItem::getQuantity).reduce(0, Integer::sum))"),
@Mapping(target = "totalAmount",
expression = "java(orderSubmitForm.getOrderItems().stream().map(item -> item.getPrice() * item.getQuantity()).reduce(0L, Long::sum))"),
@Mapping(target = "source", expression = "java(orderSubmitForm.getOrderSource().getValue())"),
})
OmsOrder submitForm2Entity(OrderSubmitForm orderSubmitForm);
OmsOrder form2Entity(OrderSubmitForm orderSubmitForm);
@Mappings({
@Mapping(
target = "paymentMethodLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getPaymentMethod(), com.youlai.mall.oms.enums.PaymentMethodEnum.class))"
),
@Mapping(
target = "sourceLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getSource(), com.youlai.mall.oms.enums.OrderSourceEnum.class))"
),
@Mapping(
target = "statusLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getStatus(), com.youlai.mall.oms.enums.OrderStatusEnum.class))"
),
@Mapping(
target = "orderItems",
source = "orderItems"
)
})
OmsOrderPageVO toVoPage(OrderBO bo);
Page<OmsOrderPageVO> toVoPage(Page<OrderBO> boPage);
OmsOrderPageVO.OrderItem toVoPageOrderItem(OrderBO.OrderItem orderItem);
@Mappings({
@Mapping(
target = "paymentMethodLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getPaymentMethod(), com.youlai.mall.oms.enums.PaymentMethodEnum.class))"
),
@Mapping(
target = "sourceLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getSource(), com.youlai.mall.oms.enums.OrderSourceEnum.class))"
),
@Mapping(
target = "statusLabel",
expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getStatus(), com.youlai.mall.oms.enums.OrderStatusEnum.class))"
),
@Mapping(
target = "orderItems",
source = "orderItems"
)
})
OrderPageVO toVoPageForApp(OrderBO bo);
Page<OrderPageVO> toVoPageForApp(Page<OrderBO> boPage);
OrderPageVO.OrderItem toVoPageOrderItemForApp(OrderBO.OrderItem orderItem);
}

View File

@ -1,40 +1,29 @@
package com.youlai.mall.oms.converter;
import cn.hutool.core.collection.CollectionUtil;
import com.youlai.mall.oms.model.dto.OrderItemDTO;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 订单对象转化器
* 订单商品明细对象转化器
*
* @author haoxr
* @since 2022/12/21
* @since 2.0.0
*/
@Mapper(componentModel = "spring")
public interface OrderItemConverter {
@Mappings({
@Mapping(target = "totalAmount", expression = "java(dto.getPrice() * dto.getCount())"),
@Mapping(target = "orderId", source = "orderId"),
@Mapping(target = "totalAmount", expression = "java(item.getPrice() * item.getQuantity())"),
})
OmsOrderItem dto2Entity(Long orderId, OrderItemDTO dto);
OmsOrderItem item2Entity(OrderSubmitForm.OrderItem item);
List<OmsOrderItem> item2Entity(List<OrderSubmitForm.OrderItem> list);
default List<OmsOrderItem> dto2Entity(Long orderId, List<OrderItemDTO> list) {
if (CollectionUtil.isNotEmpty(list)) {
List<OmsOrderItem> entities = list.stream().map(dto -> dto2Entity(orderId, dto))
.collect(Collectors.toList());
return entities;
}
return Collections.EMPTY_LIST;
}
}

View File

@ -4,20 +4,19 @@ import com.youlai.common.base.IBaseEnum;
import lombok.Getter;
/**
* 订单来源类型枚举
* 订单来源枚举
*
* @author huawei
* @email huawei_code@163.com
* @since 2021/1/16
*/
public enum OrderSourceTypeEnum implements IBaseEnum<Integer> {
public enum OrderSourceEnum implements IBaseEnum<Integer> {
APP(1, "APP"), // APP订单
PC(2, "PC"), // PC订单
WEB(2, "WEB"), // 网页
;
OrderSourceTypeEnum(Integer value, String label) {
OrderSourceEnum(Integer value, String label) {
this.value = value;
this.label = label;
}

View File

@ -7,7 +7,7 @@ import lombok.Getter;
* 订单状态枚举
*
* @author haoxr
* @since 2022/11/28
* @since 2.0.0
*/
public enum OrderStatusEnum implements IBaseEnum<Integer> {
@ -34,8 +34,7 @@ public enum OrderStatusEnum implements IBaseEnum<Integer> {
/**
* 售后中
*/
SERVICING(5, "售后中")
;
SERVICING(5, "售后中");
OrderStatusEnum(Integer value, String label) {
this.value = value;

View File

@ -5,20 +5,19 @@ import com.youlai.common.base.IBaseEnum;
import lombok.Getter;
/**
* 订单支付类型枚举
* 订单支付方式枚举
*
* @author huawei
* @email huawei_code@163.com
* @since 2021/1/16
* @since 2.0.0
*/
public enum PayTypeEnum implements IBaseEnum<Integer> {
public enum PaymentMethodEnum implements IBaseEnum<Integer> {
WX_JSAPI(1, "微信JSAPI支付"),
ALIPAY(2, "支付宝支付"),
BALANCE(3, "会员余额支付"),
WX_APP(4, "微信APP支付");
PayTypeEnum(int value, String label) {
PaymentMethodEnum(int value, String label) {
this.value = value;
this.label = label;
}

View File

@ -0,0 +1,92 @@
package com.youlai.mall.oms.listener;
import com.rabbitmq.client.Channel;
import com.youlai.mall.oms.service.app.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import java.io.IOException;
/**
* 订单超时未支付取消
*
* @author haoxr
* @since 2.0.0
*/
//@Component // 注解绑定死信队列无效暂不使用
@RequiredArgsConstructor
@Slf4j
public class OmsCloseListener {
private final OrderService orderService;
// 延迟队列
private static final String ORDER_CLOSE_DELAY_QUEUE = "order.close.delay.queue";
private static final String ORDER_EXCHANGE = "order.exchange";
private static final String ORDER_CLOSE_DELAY_ROUTING_KEY = "order.close.delay";
// 关单队列
private static final String ORDER_ClOSE_QUEUE = "order.close.queue";
private static final String ORDER_DLX_EXCHANGE = "order.dlx.exchange";
private static final String ORDER_ClOSE_ROUTING_KEY = "order.close";
/**
* 延迟队列
* <p>
* 超过 x-message-ttl 设定时间未被消费转发到死信交换机
*/
@RabbitListener(bindings =
{
@QueueBinding(
value = @Queue(value = ORDER_CLOSE_DELAY_QUEUE,
arguments =
{
@Argument(name = "x-dead-letter-exchange", value = ORDER_DLX_EXCHANGE),
@Argument(name = "x-dead-letter-routing-key", value = ORDER_ClOSE_ROUTING_KEY),
@Argument(name = "x-message-ttl", value = "10000", type = "java.lang.Long") // 超时10s
}),
exchange = @Exchange(value = ORDER_EXCHANGE),
key = {ORDER_CLOSE_DELAY_ROUTING_KEY}
)
}, ackMode = "MANUAL" // 手动ACK
)
public void handleOrderCloseDelay(String orderSn, Message message, Channel channel) throws IOException {
log.info("订单({})延时队列10s内如果未支付将路由到关单队列", orderSn);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
/**
* @param deliveryTag 消息序号
* @param multiple 是否批量处理true:批量拒绝所有小于deliveryTag的消息false:只处理当前消息
* @param requeue 拒绝是否重新入队列 true:消息重新入队false:禁止消息重新入队
*/
//channel.basicReject(deliveryTag, false); // 等于 channel.basicReject(deliveryTag, false);
}
/**
* 关单队列
*/
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = ORDER_ClOSE_QUEUE, durable = "true"),
exchange = @Exchange(value = ORDER_DLX_EXCHANGE),
key = {ORDER_ClOSE_ROUTING_KEY}
)
}, ackMode = "MANUAL" // 手动ACK
)
@RabbitListener(queues = "order.close.queue")
public void handleOrderClose(String orderSn, Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息序号
log.info("订单({})超时未支付,系统自动关闭订单", orderSn);
try {
orderService.closeOrder(orderSn);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
// TODO 关单失败入定时任务表
channel.basicReject(deliveryTag, false);
}
}
}

View File

@ -1,92 +1,54 @@
package com.youlai.mall.oms.listener;
import com.rabbitmq.client.Channel;
import com.youlai.mall.oms.service.OrderService;
import com.youlai.mall.oms.service.app.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 订单超时未支付取消
* 订单超时未支付系统自动取消监听器
*
* @author haoxr
* @since 2022/12/19
*/
//@Component
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderCloseListener {
private final OrderService orderService;
private final RabbitTemplate rabbitTemplate;
// 延迟队列
private static final String ORDER_CLOSE_DELAY_QUEUE = "order.close.delay.queue";
private static final String ORDER_EXCHANGE = "order.exchange";
private static final String ORDER_CLOSE_DELAY_ROUTING_KEY = "order.close.delay.routing.key";
// 关单队列
private static final String ORDER_ClOSE_QUEUE = "order.close.queue";
private static final String ORDER_DLX_EXCHANGE = "order.dlx.exchange";
private static final String ORDER_ClOSE_ROUTING_KEY = "order.close.routing.key";
/**
* 延迟队列
* <p>
* 超过 x-message-ttl 设定时间未被消费转发到死信交换机
*/
@RabbitListener(bindings =
{
@QueueBinding(
value = @Queue(value = ORDER_CLOSE_DELAY_QUEUE,
arguments =
{
@Argument(name = "x-dead-letter-exchange", value = ORDER_DLX_EXCHANGE),
@Argument(name = "x-dead-letter-routing-key", value = ORDER_ClOSE_ROUTING_KEY),
@Argument(name = "x-message-ttl", value = "5000", type = "java.lang.Long") // 超时10s
}),
exchange = @Exchange(value = ORDER_EXCHANGE),
key = {ORDER_CLOSE_DELAY_ROUTING_KEY}
)
}, ackMode = "MANUAL" // 手动ACK
)
public void handleOrderCloseDelay(String orderSn, Message message, Channel channel) throws IOException {
log.info("订单【{}】延时队列10s内如果未支付将路由到关单队列", orderSn);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
/**
* @param deliveryTag 消息序号
* @param multiple 是否批量处理true:批量拒绝所有小于deliveryTag的消息false:只处理当前消息
* @param requeue 拒绝是否重新入队列 true:消息重新入队false:禁止消息重新入队
*/
//channel.basicReject(deliveryTag, false); // 等于 channel.basicReject(deliveryTag, false);
}
/**
* 关单队列
*/
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = ORDER_ClOSE_QUEUE, durable = "true"),
exchange = @Exchange(value = ORDER_DLX_EXCHANGE),
key = {ORDER_ClOSE_ROUTING_KEY}
)
}, ackMode = "MANUAL" // 手动ACK
)
@RabbitListener(queues = "order.close.queue")
public void handleOrderClose(String orderSn, Message message, Channel channel) throws IOException {
public void closeOrder(String orderSn, Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息序号
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息序号消息队列中的位置
log.info("订单 【{}】 超时未支付,系统自动关闭订单", orderSn);
log.info("订单({})超时未支付,系统自动关闭订单", orderSn);
try {
orderService.closeOrder(orderSn);
channel.basicAck(deliveryTag, false);
boolean closeOrderResult = orderService.closeOrder(orderSn);
log.info("关单结果:{}", closeOrderResult);
if (closeOrderResult) {
// 关单成功释放库存
rabbitTemplate.convertAndSend("stock.exchange", "stock.unlock", orderSn);
} else {
// 关单失败订单已被关闭手动ACK确认并从队列移除消息
channel.basicAck(deliveryTag, false); // false: 不批量确认仅确认当前单个消息
}
} catch (Exception e) {
// 关单异常拒绝消息并重新入队
try {
channel.basicReject(deliveryTag, true); // true: 重新放回队列
// channel.basicReject(deliveryTag, false); // false: 直接丢弃消息 (TODO 定时任务补偿)
} catch (IOException ex) {
log.error("订单({})关闭失败,原因:{}", orderSn, ex.getMessage());
}
// TODO 关单失败入定时任务表
channel.basicReject(deliveryTag, false);
}
}
}

View File

@ -1,11 +1,15 @@
package com.youlai.mall.oms.mapper;
import com.youlai.mall.oms.model.entity.OmsOrderDelivery;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.oms.model.entity.OmsOrderDelivery;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单物流记录表
*
* @author huawei
* @email huawei_code@163.com
* @date 2020-12-30 22:31:10
*/
@Mapper
public interface OrderDeliveryMapper extends BaseMapper<OmsOrderDelivery> {

View File

@ -1,11 +1,15 @@
package com.youlai.mall.oms.mapper;
import com.youlai.mall.oms.model.entity.OmsOrderLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.oms.model.entity.OmsOrderLog;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单操作历史记录
*
* @author huawei
* @email huawei_code@163.com
* @date 2020-12-30 22:31:10
*/
@Mapper
public interface OrderLogMapper extends BaseMapper<OmsOrderLog> {

View File

@ -2,14 +2,16 @@ package com.youlai.mall.oms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.mall.oms.model.bo.OrderBO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import org.apache.ibatis.annotations.*;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单表
* 订单数据访问层
*
* @author huawei
* @since 2020-12-30 22:31:10
*/
@Mapper
public interface OrderMapper extends BaseMapper<OmsOrder> {
@ -21,5 +23,5 @@ public interface OrderMapper extends BaseMapper<OmsOrder> {
* @param queryParams
* @return
*/
List<OmsOrder> listOrderPages(Page<OmsOrder> page, OrderPageQuery queryParams);
Page<OrderBO> getOrderPage(Page<OrderBO> page, OrderPageQuery queryParams);
}

View File

@ -1,11 +1,15 @@
package com.youlai.mall.oms.mapper;
import com.youlai.mall.oms.model.entity.OmsOrderPay;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.oms.model.entity.OmsOrderPay;
import org.apache.ibatis.annotations.Mapper;
/**
* 支付信息表
*
* @author huawei
* @email huawei_code@163.com
* @date 2020-12-30 22:31:10
*/
@Mapper
public interface OrderPayMapper extends BaseMapper<OmsOrderPay> {

View File

@ -1,11 +1,15 @@
package com.youlai.mall.oms.mapper;
import com.youlai.mall.oms.model.entity.OmsOrderSetting;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.oms.model.entity.OmsOrderSetting;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单配置信息
*
* @author huawei
* @email huawei_code@163.com
* @date 2020-12-30 22:31:10
*/
@Mapper
public interface OrderSettingMapper extends BaseMapper<OmsOrderSetting> {

View File

@ -0,0 +1,123 @@
package com.youlai.mall.oms.model.bo;
import com.youlai.common.base.BaseEntity;
import com.youlai.mall.oms.enums.OrderSourceEnum;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.enums.PaymentMethodEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单业务对象
*
* @author huawei
* @since 2.0.0
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class OrderBO extends BaseEntity {
/**
* 订单ID
*/
private Long id;
/**
* 订单号
*/
private String orderSn;
/**
* 订单总额
*/
private Long totalAmount;
/**
* 商品总数
*/
private Integer totalQuantity;
/**
* 订单来源 {@link OrderSourceEnum}
*/
private Integer source;
/**
* 订单状态 {@link OrderStatusEnum}
*/
private Integer status;
/**
* 应付总额
*/
private Long paymentAmount;
/**
* 支付方式 {@link PaymentMethodEnum}
*/
private Integer paymentMethod;
/**
* 订单创建时间
*/
private LocalDateTime createTime;
/**
* 订单备注
*/
private String remark;
/**
* 订单商品明细列表
*/
private List<OrderItem> orderItems;
@Data
public static class OrderItem{
private Long id;
/**
* 订单ID
*/
private Long orderId;
/**
* 规格ID
*/
private Long skuId;
/**
* SKU编号
*/
private String skuSn;
/**
* 商品名称
*/
private String skuName;
/**
* 商品sku图片
*/
private String picUrl;
/**
* 商品单价(单位)
*/
private Long price;
/**
* 商品数量
*/
private Integer quantity;
/**
* 商品总金额(单位)(单价*数量)
*/
private Long totalAmount;
}
}

View File

@ -1,6 +1,6 @@
package com.youlai.mall.oms.model.dto;
import lombok.*;
import lombok.Data;
import java.io.Serializable;

View File

@ -2,18 +2,11 @@ package com.youlai.mall.oms.model.dto;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
import com.youlai.mall.ums.dto.MemberRegisterDto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* @author huawei
* @desc
* @email huawei_code@163.com
* @since 2021/1/19
*/
@Data
@Accessors(chain = true)
public class OrderDTO {
@ -22,6 +15,4 @@ public class OrderDTO {
private List<OmsOrderItem> orderItems;
private MemberRegisterDto member;
}

View File

@ -1,12 +1,12 @@
package com.youlai.mall.oms.model.dto;
import lombok.*;
import lombok.Data;
/**
* 订单商品
*
* @author haoxr
* @since 2022/12/21
* @since 2.0.0
*/
@Data
public class OrderItemDTO {
@ -44,5 +44,5 @@ public class OrderItemDTO {
/**
* 订单商品数量
*/
private Integer count;
private Integer quantity;
}

View File

@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.youlai.common.base.BaseEntity;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
@ -15,16 +15,13 @@ import java.util.List;
* 订单详情表
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @since 2.0.0
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class OmsOrder extends BaseEntity {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
@ -42,7 +39,7 @@ public class OmsOrder extends BaseEntity {
/**
* 订单来源(0-PC订单1-app订单)
*/
private Integer sourceType;
private Integer source;
/**
* 订单状态(1-待付款;2-待发货;3-已发货;4-已完成;5-已关闭;6-已取消;)
@ -71,15 +68,15 @@ public class OmsOrder extends BaseEntity {
/**
* 应付总额
*/
private Long payAmount;
private Long paymentAmount;
/**
* 支付时间
*/
private Date payTime;
private Date paymentTime;
/**
* 支付方式1->微信jsapi2->支付宝3->余额 4->微信app
* 支付方式1->微信jsapi2->支付宝3->余额4->微信app
*/
private Integer payType;
private Integer paymentMethod;
/**
* 商户订单号
*/

View File

@ -13,7 +13,7 @@ import java.util.Date;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
@Data
@Builder

View File

@ -4,17 +4,16 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.youlai.common.base.BaseEntity;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.EqualsAndHashCode;
/**
* 订单明细
* 订单商品明细
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @since 2020-12-30
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class OmsOrderItem extends BaseEntity {
@TableId(type = IdType.AUTO)
@ -36,7 +35,7 @@ public class OmsOrderItem extends BaseEntity {
private Long skuId;
/**
* SKU编号
* SKU 编号
*/
private String skuSn;
@ -58,7 +57,7 @@ public class OmsOrderItem extends BaseEntity {
/**
* 商品数量
*/
private Integer count;
private Integer quantity;
/**
* 商品总金额(单位)

View File

@ -10,7 +10,7 @@ import lombok.Data;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
@Data
public class OmsOrderLog extends BaseEntity {

View File

@ -13,7 +13,7 @@ import java.util.Date;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
@Data
@Builder

View File

@ -10,7 +10,7 @@ import lombok.Data;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
@Data
public class OmsOrderSetting extends BaseEntity {

View File

@ -0,0 +1,27 @@
package com.youlai.mall.oms.model.form;
import com.youlai.mall.oms.enums.PaymentMethodEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 订单支付表单对象
*
* @author haoxr
* @since 2.3.0
*/
@Data
@Schema(description ="订单支付表单对象")
public class OrderPaymentForm {
@Schema(description="订单编号")
private String orderSn;
@Schema(description="小程序 AppId")
String appId;
@Schema(description="支付方式")
private PaymentMethodEnum paymentMethod;
}

View File

@ -1,55 +1,99 @@
package com.youlai.mall.oms.model.form;
import com.youlai.mall.oms.enums.OrderSourceTypeEnum;
import com.youlai.mall.oms.model.dto.OrderItemDTO;
import com.youlai.mall.ums.dto.MemberAddressDTO;
import com.youlai.mall.oms.enums.OrderSourceEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
/**
* 订单提交表单对象
*
* @author huawei
* @email huawei_code@163.com
* @since 2021/1/16
* @author haoxr
* @since 2.0.0
*/
@Data
@ToString
public class OrderSubmitForm {
/**
* 订单来源
*
* @see OrderSourceTypeEnum
*/
@Schema(description="订单来源")
private Integer sourceType;
@Schema(description="提交订单确认页面签发的令牌(防止订单重复提交,订单提交成功转为订单编号)")
@Schema(description="订单确认页面签发的令牌(防止重复提交)")
@NotBlank(message = "订单令牌不能为空")
private String orderToken;
@Schema(description="订单总金额-用于验价(单位:分)")
private Long totalAmount;
@Schema(description="订单来源")
@NotNull(message = "订单来源不能为空")
private OrderSourceEnum orderSource;
@Schema(description="支付金额(单位:分)")
private Long payAmount;
@Schema(description="订单商品明细")
@NotEmpty(message = "订单商品不能为空")
private List<OrderItem> orderItems;
@Schema(description="订单的商品明细")
private List<OrderItemDTO> orderItems;
@Schema(description="应付金额(单位:分)")
@NotNull(message = "应付金额不能为空")
private Long paymentAmount;
@Schema(description="收获地址")
@NotNull(message = "收货地址不能为空")
private ShippingAddress shippingAddress;
@Schema(description="订单备注")
@Size(max = 500, message = "订单备注长度不能超过500")
private String remark;
@Schema(description="优惠券ID")
private String couponId;
@Schema(description ="收获地址")
@Data
public static class ShippingAddress {
@Schema(description="收货人姓名")
private String consigneeName;
@Schema(description="收货人手机号")
private String consigneeMobile;
@Schema(description="省份")
private String province;
@Schema(description="城市")
private String city;
@Schema(description="区域")
private String district;
@Schema(description="详细地址")
private String detailAddress;
}
@Schema(description ="订单商品")
@Data
public static class OrderItem {
@Schema(description = "SKU ID")
private Long skuId;
@Schema(description = "SKU 编号")
private String skuSn;
@Schema(description = "SKU 名称")
private String skuName;
@Schema(description = "商品图片URL")
private String picUrl;
@Schema(description = "商品价格(单位:分)")
private Long price;
@Schema(description = "商品名称")
private String spuName;
@Schema(description = "商品数量")
private Integer quantity;
}
@Schema(description="收获地址")
private MemberAddressDTO deliveryAddress;
}

View File

@ -1,31 +1,49 @@
package com.youlai.mall.oms.model.query;
import com.youlai.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* 订单分页查询对象
*
* @author haoxr
* @since 2022/2/1 19:14
* @since 2.3.0
*/
@EqualsAndHashCode(callSuper = true)
@Schema(description ="订单分页查询对象")
@Data
@Schema(description = "订单分页查询对象")
public class OrderPageQuery extends BasePageQuery {
/**
* 关键字(订单编号/商品名称/会员姓名/会员手机号)
*/
@Schema(description="关键字(订单编号/商品名称/会员姓名/会员手机号)")
private String keywords;
/**
* 订单状态
*/
@Schema(description="订单状态")
private Integer status;
@Schema(description="会员ID")
private Long memberId;
/**
* 开始时间
*/
@Schema(description = "开始时间(yyyy-MM-dd)",example = "2023-10-01")
@DateTimeFormat(pattern = "yyyy-MM-dd 00:00:00") // DateTimeFormat 用于将查询参数或表单参数转换为日期类型
private Date beginDate;
@Schema(description="订单编号")
private String orderSn;
@Schema(description = "开始时间(格式yyyy-MM-dd)")
private String beginDate;
@Schema(description = "截止时间(格式yyyy-MM-dd)")
private String endDate;
/**
* 截止时间
*/
@Schema(description = "截止时间(yyyy-MM-dd)",example = "2025-10-01")
@DateTimeFormat(pattern = "yyyy-MM-dd 23:59:59")
private Date endDate;
}

View File

@ -0,0 +1,83 @@
package com.youlai.mall.oms.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* Admin-订单分页视图对象
*
* @author haoxr
* @since 2.3.0
*/
@Schema(description ="Admin-订单分页视图对象")
@Data
public class OmsOrderPageVO {
@Schema(description="订单ID")
private Long id;
@Schema(description="订单编号")
private String orderSn;
@Schema(description="订单总金额(分)")
private BigDecimal totalAmount;
@Schema(description="订单总金额(分)")
private Long paymentAmount;
@Schema(description="支付方式标签")
private String paymentMethodLabel;
@Schema(description="订单状态")
private Integer status;
@Schema(description="订单状态标签")
private String statusLabel;
@Schema(description="商品总数")
private Integer totalQuantity;
@Schema(description="订单创建时间")
@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date createTime;
@Schema(description="订单来源标签")
private String sourceLabel;
@Schema(description="订单备注")
private String remark;
@Schema(description="订单商品集合")
private List<OrderItem> orderItems;
@Schema(description ="订单商品明细")
@Data
public static class OrderItem {
@Schema(description="商品ID")
private Long skuId;
@Schema(description="商品规格名称")
private String skuName;
@Schema(description="图片地址")
private String picUrl;
@Schema(description="商品价格")
private Long price;
@Schema(description="商品数量")
private Integer quantity;
@Schema(description="商品总金额(单位:分)")
private Long totalAmount;
}
}

View File

@ -2,22 +2,34 @@ package com.youlai.mall.oms.model.vo;
import com.youlai.mall.oms.model.dto.OrderItemDTO;
import com.youlai.mall.ums.dto.MemberAddressDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "订单确认视图层对象")
/**
* 订单确认响应对象
*/
@Schema(description ="订单确认响应对象")
@Data
public class OrderConfirmVO {
@Schema(description="订单token")
/**
* 订单防重提交令牌
*/
@Schema(description="订单防重提交令牌")
private String orderToken;
@Schema(description="订单明细")
/**
* 订单商品
*/
@Schema(description="订单商品")
private List<OrderItemDTO> orderItems;
/**
* 会员收货地址列表
*/
@Schema(description="会员收获地址列表")
private List<MemberAddressDTO> addresses;

View File

@ -1,62 +1,83 @@
package com.youlai.mall.oms.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 订单分页视图对象
* App-订单分页视图对象
*
* @author haoxr
* @since 2022/2/1 20:58
* @since 2.3.0
*/
@Schema(description ="App-订单分页视图对象")
@Data
public class OrderPageVO {
@Schema(description="订单ID")
private Long id;
@Schema(description="订单编号")
private String orderSn;
private Long totalAmount;
@Schema(description="订单总金额(分)")
private BigDecimal totalAmount;
private Long payAmount;
@Schema(description="订单总金额(分)")
private Long paymentAmount;
private Integer payType;
@Schema(description="支付方式标签")
private String paymentMethodLabel;
@Schema(description="订单状态")
private Integer status;
@Schema(description="订单状态标签")
private String statusLabel;
@Schema(description="商品总数")
private Integer totalQuantity;
@Schema(description="订单创建时间")
@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date createTime;
private Long memberId;
@Schema(description="订单来源标签")
private String sourceLabel;
private Integer sourceType;
@Schema(description="订单备注")
private String remark;
private List<OrderItem> orderItems;
@Schema(description="订单商品集合")
private List<OmsOrderPageVO.OrderItem> orderItems;
@Schema(description ="订单商品明细")
@Data
public static class OrderItem {
private Long id;
private Long orderId;
@Schema(description="商品ID")
private Long skuId;
@Schema(description="商品规格名称")
private String skuName;
@Schema(description="图片地址")
private String picUrl;
@Schema(description="商品价格")
private Long price;
private Integer count;
@Schema(description="商品数量")
private Integer quantity;
@Schema(description="商品总金额(单位:分)")
private Long totalAmount;
private String spuName;
}
}

View File

@ -1,26 +0,0 @@
package com.youlai.mall.oms.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 订单提交结果
*
* @author huawei
* @since 2021/1/21
*/
@Schema(description = "订单提交结果")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderSubmitResultVO {
@Schema(description="订单ID")
private Long orderId;
@Schema(description="订单编号,进入支付页面显示")
private String orderSn;
}

View File

@ -2,24 +2,24 @@ package com.youlai.mall.oms.service.admin;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.oms.dto.SeataOrderDTO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OmsOrderPageVO;
/**
* 管理端订单业务接口
* Admin-订单业务接口
*
* @author haoxr
* @since 2020/12/30
* @since 2.3.0
*/
public interface OmsOrderService extends IService<OmsOrder> {
/**
* 订单分页列表
*
* @param queryParams
* @param queryParams {@link OrderPageQuery}
* @return
*/
IPage<OmsOrder> listOrderPages(OrderPageQuery queryParams);
IPage<OmsOrderPageVO> getOrderPage(OrderPageQuery queryParams);
}

View File

@ -3,37 +3,42 @@ package com.youlai.mall.oms.service.admin.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.oms.converter.OrderConverter;
import com.youlai.mall.oms.mapper.OrderMapper;
import com.youlai.mall.oms.model.bo.OrderBO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OmsOrderPageVO;
import com.youlai.mall.oms.service.admin.OmsOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 管理端订单业务实现类
* Admin-订单业务实现类
*
* @author haoxr
* @since 2022/2/12
* @since 2.3.0
*/
@Service
@RequiredArgsConstructor
public class OmsOrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> implements OmsOrderService {
private final OrderConverter orderConverter;
/**
* 订单分页列表
* Admin-订单分页列表
*
* @param queryParams
* @return
* @param queryParams {@link OrderPageQuery}
* @return {@link OmsOrderPageVO}
*/
@Override
public IPage<OmsOrder> listOrderPages(OrderPageQuery queryParams) {
Page<OmsOrder> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
List<OmsOrder> list = this.baseMapper.listOrderPages(page, queryParams);
page.setRecords(list);
return page;
public IPage<OmsOrderPageVO> getOrderPage(OrderPageQuery queryParams) {
Page<OrderBO> boPage = this.baseMapper.getOrderPage(
new Page<>(queryParams.getPageNum(), queryParams.getPageSize()),
queryParams);
return orderConverter.toVoPage(boPage);
}
}

View File

@ -1,4 +1,4 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.youlai.mall.oms.model.dto.CartItemDTO;
@ -8,7 +8,7 @@ import java.util.List;
* 购物车业务接口
*
* @author haoxr
* @since 2022/11/13
* @date 2022/11/13
*/
public interface CartService {

View File

@ -1,7 +1,6 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.oms.model.entity.OmsOrderDelivery;
/**
@ -9,7 +8,7 @@ import com.youlai.mall.oms.model.entity.OmsOrderDelivery;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
public interface OrderDeliveryService extends IService<OmsOrderDelivery> {
}

View File

@ -1,4 +1,4 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
@ -9,7 +9,7 @@ import com.youlai.mall.oms.model.entity.OmsOrderItem;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
public interface OrderItemService extends IService<OmsOrderItem> {

View File

@ -1,7 +1,6 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.oms.model.entity.OmsOrderLog;
/**
@ -9,7 +8,7 @@ import com.youlai.mall.oms.model.entity.OmsOrderLog;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
public interface OrderLogService extends IService<OmsOrderLog> {

View File

@ -1,21 +1,20 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.form.OrderPaymentForm;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OrderConfirmVO;
import com.youlai.mall.oms.model.vo.OrderSubmitResultVO;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import com.youlai.mall.oms.model.vo.OrderPageVO;
/**
* 订单业务接口
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
*/
public interface OrderService extends IService<OmsOrder> {
@ -27,19 +26,22 @@ public interface OrderService extends IService<OmsOrder> {
* 进入订单创建页面有两个入口1立即购买2购物车结算
*
* @param skuId 直接购买必填购物车结算不填
* @return
* @return {@link OrderConfirmVO}
*/
OrderConfirmVO confirmOrder(Long skuId);
/**
* 订单提交
*
* @param orderSubmitForm {@link OrderSubmitForm}
* @return 订单编号
*/
OrderSubmitResultVO submitOrder(OrderSubmitForm orderSubmitForm);
String submitOrder(OrderSubmitForm orderSubmitForm);
/**
* 订单支付
*/
boolean payOrder(Long orderId);
<T> T payOrder(OrderPaymentForm paymentForm);
/**
* 系统关闭订单
@ -72,10 +74,10 @@ public interface OrderService extends IService<OmsOrder> {
/**
* 订单分页列表
*
* @param queryParams
* @return
* @param queryParams 订单分页查询参数
* @return {@link OrderPageVO}
*/
IPage<OmsOrder> listOrderPages(OrderPageQuery queryParams);
IPage<OrderPageVO> getOrderPage(OrderPageQuery queryParams);
}

View File

@ -1,7 +1,6 @@
package com.youlai.mall.oms.service;
package com.youlai.mall.oms.service.app;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.oms.model.entity.OmsOrderSetting;
/**
@ -9,7 +8,7 @@ import com.youlai.mall.oms.model.entity.OmsOrderSetting;
*
* @author huawei
* @email huawei_code@163.com
* @since 2020-12-30 22:31:10
* @date 2020-12-30 22:31:10
*/
public interface OrderSettingService extends IService<OmsOrderSetting> {
}

View File

@ -1,16 +1,15 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import com.youlai.common.result.ResultCode;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.common.web.exception.BizException;
import com.youlai.common.constant.OrderConstants;
import com.youlai.mall.oms.constant.OrderConstants;
import com.youlai.mall.oms.converter.CartConverter;
import com.youlai.mall.oms.model.dto.CartItemDTO;
import com.youlai.mall.oms.service.CartService;
import com.youlai.mall.oms.service.app.CartService;
import com.youlai.mall.pms.api.SkuFeignClient;
import com.youlai.mall.pms.model.dto.SkuDTO;
import lombok.AllArgsConstructor;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
@ -18,7 +17,6 @@ import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
@ -26,18 +24,19 @@ import java.util.concurrent.CompletableFuture;
* <p>
* 核心技术BoundHashOperations
* 数据格式
* -- key <----> 购物车
* -- hKey:value <----> 商品1
* -- hKey:value <----> 商品2
* -- hKey:value <----> 商品3
* -- key <--> 商品列表
* ---- hKey:value <--> skuId 商品1
* ---- hKey:value <--> 商品2
* ---- hKey:value <--> 商品3
*/
@Service
@Slf4j
@AllArgsConstructor
@RequiredArgsConstructor
public class CartServiceImpl implements CartService {
private RedisTemplate redisTemplate;
private SkuFeignClient skuFeignService;
private final RedisTemplate redisTemplate;
private final SkuFeignClient skuFeignService;
private final CartConverter cartConverter;
@Override
public List<CartItemDTO> listCartItems(Long memberId) {
@ -65,35 +64,24 @@ public class CartServiceImpl implements CartService {
@Override
public boolean addCartItem(Long skuId) {
Long memberId = SecurityUtils.getMemberId();
if (memberId == null) {
throw new BizException(ResultCode.INVALID_TOKEN);
}
BoundHashOperations cartHashOperations = getCartHashOperations(memberId);
BoundHashOperations<String, String, CartItemDTO> cartHashOperations = getCartHashOperations(memberId);
String hKey = String.valueOf(skuId);
CartItemDTO cartItem;
// 购物车已存在该商品更新商品数量
if (cartHashOperations.get(hKey) != null) {
cartItem = (CartItemDTO) cartHashOperations.get(hKey);
CartItemDTO cartItem = cartHashOperations.get(hKey);
if (cartItem != null) {
// 购物车已存在该商品更新商品数量
cartItem.setCount(cartItem.getCount() + 1); // 点击一次加入购物车数量+1
cartItem.setChecked(true);
cartHashOperations.put(hKey, cartItem);
return true;
}
// 购物车不存在该商品添加商品至购物车
cartItem = new CartItemDTO();
CompletableFuture<Void> cartItemCompletableFuture = CompletableFuture.runAsync(() -> {
SkuDTO skuInfo = skuFeignService.getSkuInfo(skuId).getData();
} else {
// 购物车中不存在该商品新增商品到购物车
SkuInfoDTO skuInfo = skuFeignService.getSkuInfo(skuId);
if (skuInfo != null) {
BeanUtil.copyProperties(skuInfo, cartItem);
cartItem.setStock(skuInfo.getStockNum());
cartItem = cartConverter.sku2CartItem(skuInfo);
cartItem.setCount(1);
cartItem.setChecked(true);
}
});
CompletableFuture.allOf(cartItemCompletableFuture).join();
Assert.isTrue(cartItem.getSkuId() != null, "商品不存在");
}
cartHashOperations.put(hKey, cartItem);
return true;
}

View File

@ -1,9 +1,9 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.oms.mapper.OrderDeliveryMapper;
import com.youlai.mall.oms.model.entity.OmsOrderDelivery;
import com.youlai.mall.oms.service.OrderDeliveryService;
import com.youlai.mall.oms.service.app.OrderDeliveryService;
import org.springframework.stereotype.Service;
@Service("orderDeliveryService")

View File

@ -1,9 +1,9 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.oms.mapper.OrderItemMapper;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
import com.youlai.mall.oms.service.OrderItemService;
import com.youlai.mall.oms.service.app.OrderItemService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@ -1,10 +1,10 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.mall.oms.mapper.OrderLogMapper;
import com.youlai.mall.oms.model.entity.OmsOrderLog;
import com.youlai.mall.oms.service.OrderLogService;
import com.youlai.mall.oms.service.app.OrderLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@ -1,7 +1,6 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
@ -21,31 +20,32 @@ import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.youlai.common.redis.BusinessSnGenerator;
import com.youlai.common.result.Result;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.common.web.exception.BizException;
import com.youlai.mall.oms.config.WxPayProperties;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.enums.PayTypeEnum;
import com.youlai.mall.oms.constant.OrderConstants;
import com.youlai.mall.oms.converter.OrderConverter;
import com.youlai.mall.oms.converter.OrderItemConverter;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.enums.PaymentMethodEnum;
import com.youlai.mall.oms.mapper.OrderMapper;
import com.youlai.mall.oms.model.bo.OrderBO;
import com.youlai.mall.oms.model.dto.CartItemDTO;
import com.youlai.mall.oms.model.dto.OrderItemDTO;
import com.youlai.mall.oms.model.entity.OmsOrder;
import com.youlai.mall.oms.model.entity.OmsOrderItem;
import com.youlai.mall.oms.model.form.OrderPaymentForm;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OrderConfirmVO;
import com.youlai.mall.oms.model.vo.OrderSubmitResultVO;
import com.youlai.mall.oms.service.CartService;
import com.youlai.mall.oms.service.OrderItemService;
import com.youlai.mall.oms.service.OrderService;
import com.youlai.mall.oms.model.vo.OrderPageVO;
import com.youlai.mall.oms.service.app.CartService;
import com.youlai.mall.oms.service.app.OrderItemService;
import com.youlai.mall.oms.service.app.OrderService;
import com.youlai.mall.pms.api.SkuFeignClient;
import com.youlai.mall.pms.model.dto.CheckPriceDTO;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.LockStockDTO;
import com.youlai.mall.pms.model.dto.LockedSkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.ums.api.MemberFeignClient;
import com.youlai.mall.ums.dto.MemberAddressDTO;
import io.seata.spring.annotation.GlobalTransactional;
@ -68,13 +68,12 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import static com.youlai.common.constant.OrderConstants.*;
/**
* 订单业务实现类
*
* @author haoxr
* @since 2022/2/12
* @since 2.0.0
*/
@Service
@RequiredArgsConstructor
@ -88,10 +87,9 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
private final StringRedisTemplate redisTemplate;
private final ThreadPoolExecutor threadPoolExecutor;
private final MemberFeignClient memberFeignClient;
private final BusinessSnGenerator businessSnGenerator;
private final SkuFeignClient skuFeignClient;
private final RedissonClient redissonClient;
private final WxPayService wxPayService;
private final RedissonClient redissonClient;
private final OrderConverter orderConverter;
private final OrderItemConverter orderItemConverter;
@ -99,11 +97,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
* 订单分页列表
*/
@Override
public IPage<OmsOrder> listOrderPages(OrderPageQuery queryParams) {
Page<OmsOrder> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
List<OmsOrder> list = this.baseMapper.listOrderPages(page, queryParams);
page.setRecords(list);
return page;
public IPage<OrderPageVO> getOrderPage(OrderPageQuery queryParams) {
Page<OrderBO> boPage = this.baseMapper.getOrderPage(
new Page<>(queryParams.getPageNum(), queryParams.getPageSize()),
queryParams);
return orderConverter.toVoPageForApp(boPage);
}
/**
@ -112,140 +110,166 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
* 获取购买商品明细用户默认收货地址防重提交唯一token
* 进入订单创建页面有两个入口1立即购买2购物车结算
*
* @param skuId 直接购买必填购物车结算不填
* @return
* @param skuId 商品ID(直接购买传值)
* @return {@link OrderConfirmVO}
*/
@Override
public OrderConfirmVO confirmOrder(Long skuId) {
OrderConfirmVO orderConfirmVO = new OrderConfirmVO();
// 获取原请求线程的参数
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
Long memberId = SecurityUtils.getMemberId();
// 获取订单的商品明细信息
CompletableFuture<Void> getOrderItemsFuture = CompletableFuture.runAsync(() -> {
// 请求参数传递给子线程
RequestContextHolder.setRequestAttributes(attributes);
List<OrderItemDTO> orderItems = this.getOrderItems(skuId, memberId);
orderConfirmVO.setOrderItems(orderItems);
}, threadPoolExecutor);
// 获取会员收获地址
CompletableFuture<Void> getMemberAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
// 解决子线程无法获取HttpServletRequest请求对象中数据的问题
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(attributes, true);
// 获取订单商品
CompletableFuture<List<OrderItemDTO>> getOrderItemsFuture = CompletableFuture.supplyAsync(
() -> this.getOrderItems(skuId, memberId), threadPoolExecutor)
.exceptionally(ex -> {
log.error("Failed to get order items: {}", ex.toString());
return null;
});
// 用户收货地址
CompletableFuture<List<MemberAddressDTO>> getMemberAddressFuture = CompletableFuture.supplyAsync(() -> {
Result<List<MemberAddressDTO>> getMemberAddressResult = memberFeignClient.listMemberAddresses(memberId);
List<MemberAddressDTO> memberAddresses;
if (Result.isSuccess(getMemberAddressResult) && (memberAddresses = getMemberAddressResult.getData()) != null) {
orderConfirmVO.setAddresses(memberAddresses);
} else {
orderConfirmVO.setAddresses(Collections.EMPTY_LIST);
if (Result.isSuccess(getMemberAddressResult)) {
return getMemberAddressResult.getData();
}
}, threadPoolExecutor);
return null;
}, threadPoolExecutor).exceptionally(ex -> {
log.error("Failed to get addresses for memberId {} : {}", memberId, ex.toString());
return null;
});
// 进入订单确认页面生成唯一token,订单提交根据此token判断是否重复提交
CompletableFuture<Void> getOrderTokenFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
String orderToken = businessSnGenerator.generateSerialNo("ORDER");
orderConfirmVO.setOrderToken(orderToken);
redisTemplate.opsForValue().set(ORDER_RESUBMIT_LOCK_PREFIX + orderToken, orderToken);
}, threadPoolExecutor);
// 生成唯一令牌,防止重复提交(原理提交会消耗令牌令牌被消耗无法再次提交)
CompletableFuture<String> generateOrderTokenFuture = CompletableFuture.supplyAsync(() -> {
String orderToken = this.generateTradeNo(memberId);
redisTemplate.opsForValue().set(OrderConstants.ORDER_TOKEN_PREFIX + orderToken, orderToken);
return orderToken;
}, threadPoolExecutor).exceptionally(ex -> {
log.error("Failed to generate order token .");
return null;
});
CompletableFuture.allOf(getOrderItemsFuture, getMemberAddressFuture, getOrderTokenFuture).join();
log.info("订单确认响应:{}", orderConfirmVO);
CompletableFuture.allOf(getOrderItemsFuture, getMemberAddressFuture, generateOrderTokenFuture).join();
OrderConfirmVO orderConfirmVO = new OrderConfirmVO();
orderConfirmVO.setOrderItems(getOrderItemsFuture.join());
orderConfirmVO.setAddresses(getMemberAddressFuture.join());
orderConfirmVO.setOrderToken(generateOrderTokenFuture.join());
log.info("Order confirm response for skuId {}: {}", skuId, orderConfirmVO);
return orderConfirmVO;
}
/**
* 订单提交
*
* @param submitForm {@link OrderSubmitForm}
* @return 订单编号
*/
@Override
@GlobalTransactional
public OrderSubmitResultVO submitOrder(OrderSubmitForm orderSubmitForm) {
log.info("订单提交数据:{}", JSONUtil.toJsonStr(orderSubmitForm));
// 订单基础信息校验
List<OrderItemDTO> orderItems = orderSubmitForm.getOrderItems();
Assert.isTrue(CollectionUtil.isNotEmpty(orderItems), "订单没有商品");
public String submitOrder(OrderSubmitForm submitForm) {
log.info("订单提交参数:{}", JSONUtil.toJsonStr(submitForm));
String orderToken = submitForm.getOrderToken();
// 订单重复提交校验
String orderSn = orderSubmitForm.getOrderToken();
Long releaseLockResult = this.redisTemplate.execute(
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class
),
Collections.singletonList(ORDER_RESUBMIT_LOCK_PREFIX + orderSn),
orderSn
); // 释放锁成功则返回1
Assert.isTrue(releaseLockResult.equals(1l), "订单重复提交,请刷新页面后重试");
// 1. 判断订单是否重复提交(LUA脚本保证获取和删除的原子性成功返回1否则返回0)
String lockAcquireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long lockAcquired = this.redisTemplate.execute(
new DefaultRedisScript<>(lockAcquireScript, Long.class),
Collections.singletonList(OrderConstants.ORDER_TOKEN_PREFIX + orderToken),
orderToken
);
Assert.isTrue(lockAcquired != null && lockAcquired.equals(1L), "订单重复提交,请刷新页面后重试");
// 订单验价
List<CheckPriceDTO.OrderSku> orderSkus = orderItems.stream()
.map(orderItem -> new CheckPriceDTO.OrderSku(orderItem.getSkuId(), orderItem.getCount())
).collect(Collectors.toList());
CheckPriceDTO checkPriceDTO = new CheckPriceDTO(orderSn, orderSubmitForm.getTotalAmount(), orderSkus);
Result<Boolean> checkPriceResult = skuFeignClient.checkPrice(checkPriceDTO);
Assert.isTrue(Result.isSuccess(checkPriceResult) && Boolean.TRUE.equals(checkPriceResult.getData()),
"当前页面已过期,请重新刷新页面再提交");
// 锁定订单商品的库存
List<LockStockDTO.LockedSku> lockedSkus = orderItems.stream()
.map(orderItem -> new LockStockDTO.LockedSku(orderItem.getSkuId(), orderItem.getCount()))
// 2. 订单商品校验 (PS校验进入订单确认页面到提交过程商品(价格上架状态)变化)
List<OrderSubmitForm.OrderItem> orderItems = submitForm.getOrderItems();
List<Long> skuIds = orderItems.stream()
.map(OrderSubmitForm.OrderItem::getSkuId)
.collect(Collectors.toList());
LockStockDTO lockStockDTO = new LockStockDTO(orderSn, lockedSkus);
Result lockStockResult = skuFeignClient.lockStock(lockStockDTO);
Assert.isTrue(Result.isSuccess(lockStockResult), "订单提交失败:锁定商品库存失败!");
// 创建订单
OmsOrder orderEntity = orderConverter.submitForm2Entity(orderSubmitForm);
orderEntity.setStatus(OrderStatusEnum.UNPAID.getValue());
orderEntity.setMemberId(SecurityUtils.getMemberId());
boolean result = this.save(orderEntity);
// 添加订单明细
Long orderId = orderEntity.getId();
if (result) {
List<OmsOrderItem> itemEntities = orderItemConverter.dto2Entity(orderId, orderItems);
result = orderItemService.saveBatch(itemEntities);
if (result) {
// 订单超时未支付关单
rabbitTemplate.convertAndSend("order.exchange", "order.close.delay.routing.key", orderSn);
}
List<SkuInfoDTO> skuList = skuFeignClient.getSkuInfoList(skuIds);
for (OrderSubmitForm.OrderItem item : orderItems) {
SkuInfoDTO skuInfo = skuList.stream().filter(sku -> sku.getId().equals(item.getSkuId()))
.findFirst()
.orElse(null);
Assert.isTrue(skuInfo != null, "商品({})已下架或删除");
Assert.isTrue(item.getPrice().compareTo(skuInfo.getPrice()) == 0, "商品({})价格发生变动,请刷新页面", item.getSkuName());
}
Assert.isTrue(result, "订单提交失败");
// 成功响应返回值构建
OrderSubmitResultVO submitVO = new OrderSubmitResultVO(orderId, orderEntity.getOrderSn());
return submitVO;
// 3. 校验库存并锁定库存
List<LockedSkuDTO> lockedSkuList = orderItems.stream()
.map(item -> new LockedSkuDTO(item.getSkuId(), item.getQuantity(), item.getSkuSn()))
.collect(Collectors.toList());
boolean lockStockResult = skuFeignClient.lockStock(orderToken, lockedSkuList);
Assert.isTrue(lockStockResult, "订单提交失败:锁定商品库存失败!");
// 4. 生成订单
boolean result = this.saveOrder(submitForm);
log.info("order ({}) create result:{}", orderToken, result);
return orderToken;
}
/**
* 创建订单
*
* @param submitForm 订单提交表单对象
* @return
*/
private boolean saveOrder(OrderSubmitForm submitForm) {
OmsOrder order = orderConverter.form2Entity(submitForm);
order.setStatus(OrderStatusEnum.UNPAID.getValue());
order.setMemberId(SecurityUtils.getMemberId());
order.setSource(submitForm.getOrderSource().getValue());
boolean result = this.save(order);
Long orderId = order.getId();
if (result) {
// 保存订单明细
List<OmsOrderItem> orderItemEntities = orderItemConverter.item2Entity(submitForm.getOrderItems());
orderItemEntities.forEach(item -> item.setOrderId(orderId));
orderItemService.saveBatch(orderItemEntities);
// 订单超时未支付取消
rabbitTemplate.convertAndSend("order.exchange", "order.close.delay", submitForm.getOrderToken());
}
return result;
}
/**
* 订单支付
* <p>
* 余额支付库存余额订单处理
* 微信支付生成微信支付调起参数订单库存余额处理在支付回调
*/
@Override
@GlobalTransactional
public boolean payOrder(Long orderId) {
OmsOrder order = this.getById(orderId);
public <T> T payOrder(OrderPaymentForm paymentForm) {
String orderSn = paymentForm.getOrderSn();
OmsOrder order = this.getOne(new LambdaQueryWrapper<OmsOrder>().eq(OmsOrder::getOrderSn, orderSn));
Assert.isTrue(order != null, "订单不存在");
Assert.isTrue(OrderStatusEnum.UNPAID.getValue().equals(order.getStatus()), "订单不可支付,请检查订单状态");
RLock lock = redissonClient.getLock(ORDER_LOCK_PREFIX + order.getOrderSn());
RLock lock = redissonClient.getLock(OrderConstants.ORDER_LOCK_PREFIX + order.getOrderSn());
try {
lock.lock();
// 扣减余额
memberFeignClient.deductBalance(SecurityUtils.getMemberId(), order.getPayAmount());
// 扣减库存
skuFeignClient.deductStock(order.getOrderSn());
// 修改订单状态 已支付
order.setStatus(OrderStatusEnum.PAID.getValue());
order.setPayType(PayTypeEnum.BALANCE.getValue());
order.setPayTime(new Date());
this.updateById(order);
// 支付成功删除购物车已勾选的商品
cartService.removeCheckedItem();
return true;
T result;
switch (paymentForm.getPaymentMethod()) {
case WX_JSAPI:
result = (T) wxJsapiPay(paymentForm.getAppId(), order.getOrderSn(), order.getPaymentAmount());
break;
default:
result = (T) balancePay(order);
break;
}
return result;
} finally {
//释放锁
if (lock.isLocked()) {
@ -254,40 +278,75 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
}
}
/**
* 微信支付
* 余额支付
*
* @param appId
* @param order
* @return
*/
private WxPayUnifiedOrderV3Result.JsapiResult wxJsapiPay(String appId, OmsOrder order) {
private Boolean balancePay(OmsOrder order) {
// 扣减余额
Long memberId = order.getMemberId();
Long payAmount = order.getPaymentAmount();
Result<?> deductBalanceResult = memberFeignClient.deductBalance(memberId, payAmount);
Assert.isTrue(Result.isSuccess(deductBalanceResult), "扣减账户余额失败");
// 扣减库存
skuFeignClient.deductStock(order.getOrderSn());
// 更新订单状态
order.setStatus(OrderStatusEnum.PAID.getValue());
order.setPaymentMethod(PaymentMethodEnum.BALANCE.getValue());
order.setPaymentTime(new Date());
this.updateById(order);
// 支付成功删除购物车已勾选的商品
cartService.removeCheckedItem();
return Boolean.TRUE;
}
/**
* 微信支付调起
*
* @param appId 微信小程序ID
* @param orderSn 订单编号
* @param paymentAmount 支付金额
* @return 微信支付调起参数
*/
private WxPayUnifiedOrderV3Result.JsapiResult wxJsapiPay(String appId, String orderSn, Long paymentAmount) {
Long memberId = SecurityUtils.getMemberId();
Long payAmount = order.getPayAmount();
// 如果已经有outTradeNo了就先进行关单
if (PayTypeEnum.WX_JSAPI.getValue().equals(order.getPayType()) && StrUtil.isNotBlank(order.getOutTradeNo())) {
if (StrUtil.isNotBlank(orderSn)) {
try {
wxPayService.closeOrderV3(order.getOutTradeNo());
wxPayService.closeOrderV3(orderSn);
} catch (WxPayException e) {
log.error(e.getMessage(), e);
throw new BizException("微信关单异常");
}
}
// 用户id前补零保证五位对超出五位的保留后五位
String userIdFilledZero = String.format("%05d", memberId);
String fiveDigitsUserId = userIdFilledZero.substring(userIdFilledZero.length() - 5);
// 在前面加上wxowx order等前缀是为了人工可以快速分辨订单号是下单还是退款来自哪家支付机构等
// 将时间戳+3位随机数+五位id组成商户订单号规则参考自<a href="https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html">大众点评</a>
String outTradeNo = "wxo_" + System.currentTimeMillis() + RandomUtil.randomNumbers(3) + fiveDigitsUserId;
log.info("商户订单号拼接完成:{}", outTradeNo);
// 更新订单状态
order.setPayType(PayTypeEnum.WX_JSAPI.getValue());
order.setOutTradeNo(outTradeNo);
this.updateById(order);
boolean result = this.update(new LambdaUpdateWrapper<OmsOrder>()
.set(OmsOrder::getPaymentMethod, PaymentMethodEnum.WX_JSAPI.getValue())
.eq(OmsOrder::getOrderSn, orderSn)
);
String memberOpenId = memberFeignClient.getMemberOpenId(memberId).getData();
WxPayUnifiedOrderV3Request wxRequest = new WxPayUnifiedOrderV3Request().setOutTradeNo(outTradeNo).setAppid(appId).setNotifyUrl(wxPayProperties.getPayNotifyUrl()).setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(Math.toIntExact(payAmount))).setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(memberOpenId)).setDescription("赅买-订单编号" + order.getOrderSn());
WxPayUnifiedOrderV3Request wxRequest = new WxPayUnifiedOrderV3Request()
.setAppid(appId)
.setOutTradeNo(orderSn)
.setAmount(new WxPayUnifiedOrderV3Request
.Amount()
.setTotal(Math.toIntExact(paymentAmount))
)
.setPayer(
new WxPayUnifiedOrderV3Request.Payer()
.setOpenid(memberOpenId)
)
.setDescription("赅买-订单编号:" + orderSn)
.setNotifyUrl(wxPayProperties.getPayNotifyUrl());
WxPayUnifiedOrderV3Result.JsapiResult jsapiResult;
try {
jsapiResult = wxPayService.createOrderV3(TradeTypeEnum.JSAPI, wxRequest);
@ -299,29 +358,19 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
}
/**
* 关闭订单(超时未支付)
* 关闭订单
*
* @param orderSn 订单编号
* @return
* @return 是否成功 true|false
*/
@Override
public boolean closeOrder(String orderSn) {
OmsOrder order = this.getOne(new LambdaQueryWrapper<OmsOrder>()
return this.update(new LambdaUpdateWrapper<OmsOrder>()
.eq(OmsOrder::getOrderSn, orderSn)
.select(OmsOrder::getId, OmsOrder::getStatus)
.eq(OmsOrder::getStatus, OrderStatusEnum.UNPAID.getValue())
.set(OmsOrder::getStatus, OrderStatusEnum.CANCELED.getValue())
);
Assert.isTrue(order != null, "订单不存在");
boolean result;
if (OrderStatusEnum.UNPAID.getValue().equals(order.getStatus())) {
result = this.update(new LambdaUpdateWrapper<OmsOrder>()
.eq(OmsOrder::getId, order.getId())
.set(OmsOrder::getStatus, OrderStatusEnum.CANCELED.getValue()));
// 关单成功释放锁定的商品库存
rabbitTemplate.convertAndSend("stock.exchange", "stock.release.routing.key", orderSn);
} else { // 订单非待付款状态无需关闭
result = true;
}
return result;
}
/**
@ -359,7 +408,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
if (WxPayConstants.WxpayTradeStatus.SUCCESS.equals(result.getTradeState())) {
orderDO.setStatus(OrderStatusEnum.PAID.getValue());
orderDO.setTransactionId(result.getTransactionId());
orderDO.setPayTime(new Date());
orderDO.setPaymentTime(new Date());
this.updateById(orderDO);
}
log.info("账单更新成功");
@ -399,21 +448,38 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
List<OrderItemDTO> orderItems;
if (skuId != null) { // 直接购买
orderItems = new ArrayList<>();
SkuDTO skuDTO = skuFeignClient.getSkuInfo(skuId).getData();
SkuInfoDTO skuInfoDTO = skuFeignClient.getSkuInfo(skuId);
OrderItemDTO orderItemDTO = new OrderItemDTO();
BeanUtil.copyProperties(skuDTO, orderItemDTO);
orderItemDTO.setCount(1); // 直接购买商品的数量为1
BeanUtil.copyProperties(skuInfoDTO, orderItemDTO);
orderItemDTO.setSkuId(skuInfoDTO.getId());
orderItemDTO.setQuantity(1); // 直接购买商品的数量为1
orderItems.add(orderItemDTO);
} else { // 购物车结算
List<CartItemDTO> cartItems = cartService.listCartItems(memberId);
orderItems = cartItems.stream().filter(CartItemDTO::getChecked).map(cartItem -> {
OrderItemDTO orderItemDTO = new OrderItemDTO();
BeanUtil.copyProperties(cartItem, orderItemDTO);
return orderItemDTO;
}).collect(Collectors.toList());
orderItems = cartItems.stream()
.filter(CartItemDTO::getChecked)
.map(cartItem -> {
OrderItemDTO orderItemDTO = new OrderItemDTO();
BeanUtil.copyProperties(cartItem, orderItemDTO);
return orderItemDTO;
}).collect(Collectors.toList());
}
return orderItems;
}
/**
* 生成商户订单号
*
* @param memberId 会员ID
* @return
*/
private String generateTradeNo(Long memberId) {
// 用户id前补零保证五位对超出五位的保留后五位
String userIdFilledZero = String.format("%05d", memberId);
String fiveDigitsUserId = userIdFilledZero.substring(userIdFilledZero.length() - 5);
// 在前面加上wxowx order等前缀是为了人工可以快速分辨订单号是下单还是退款来自哪家支付机构等
// 将时间戳+3位随机数+五位id组成商户订单号规则参考自<a href="https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html">大众点评</a>
return System.currentTimeMillis() + RandomUtil.randomNumbers(3) + fiveDigitsUserId;
}
}

View File

@ -1,9 +1,9 @@
package com.youlai.mall.oms.service.impl;
package com.youlai.mall.oms.service.app.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.oms.mapper.OrderSettingMapper;
import com.youlai.mall.oms.model.entity.OmsOrderSetting;
import com.youlai.mall.oms.service.OrderSettingService;
import com.youlai.mall.oms.service.app.OrderSettingService;
import org.springframework.stereotype.Service;

View File

@ -11,16 +11,11 @@ spring:
nacos:
# 注册中心
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
server-addr: http://192.168.179.21:8848
# 配置中心
config:
server-addr: http://localhost:8848
server-addr: http://192.168.179.21:8848
file-extension: yaml
# 公共配置
shared-configs[0]:
data-id: youlai-common.yaml
refresh: true
username: nacos
password: nacos
refresh: true

View File

@ -4,16 +4,17 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.mall.oms.mapper.OrderItemMapper">
<!-- 根据订单ID获取订单明细 -->
<select id="listOrderItemsByOrderId" resultType="com.youlai.mall.oms.model.vo.OrderPageVO$OrderItem">
<!-- 根据订单ID获取订单商品列表 -->
<select id="getOrderItemsByOrderId" resultType="com.youlai.mall.oms.model.bo.OrderBO$OrderItem">
SELECT
id,
order_id,
sku_id,
sku_sn,
sku_name,
pic_url,
price,
count,
quantity,
total_amount
FROM
oms_order_item

View File

@ -4,68 +4,50 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.mall.oms.mapper.OrderMapper">
<resultMap id="OrderPageMap" type="com.youlai.mall.oms.model.vo.OrderPageVO">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="orderSn" column="order_sn" jdbcType="VARCHAR"/>
<result property="totalAmount" column="total_amount" jdbcType="BIGINT"/>
<result property="payAmount" column="pay_amount" jdbcType="BIGINT"/>
<result property="payType" column="pay_type" jdbcType="TINYINT"/>
<result property="status" column="status" jdbcType="TINYINT"/>
<result property="totalQuantity" column="total_quantity" jdbcType="TINYINT"/>
<result property="createTime" column="create_time" jdbcType="VARCHAR"/>
<result property="memberId" column="member_id" jdbcType="BIGINT"/>
<result property="sourceType" column="source_type" jdbcType="TINYINT"/>
<resultMap id="OrderMap" type="com.youlai.mall.oms.model.bo.OrderBO">
<collection property="orderItems"
column="id" select="com.youlai.mall.oms.mapper.OrderItemMapper.listOrderItemsByOrderId">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="orderId" column="order_id" jdbcType="BIGINT"/>
<result property="skuId" column="sku_id" jdbcType="BIGINT"/>
<result property="skuName" column="sku_name" jdbcType="VARCHAR"/>
<result property="picUrl" column="sku_pic" jdbcType="VARCHAR"/>
<result property="price" column="sku_price" jdbcType="BIGINT"/>
<result property="count" column="sku_quantity" jdbcType="INTEGER"/>
<result property="totalAmount" column="sku_total_price" jdbcType="BIGINT"/>
<result property="spuName" column="spu_name" jdbcType="VARCHAR"/>
column="{orderId=id}"
select="com.youlai.mall.oms.mapper.OrderItemMapper.getOrderItemsByOrderId">
</collection>
</resultMap>
<!-- 订单分页列表 -->
<select id="listOrderPages" resultMap="OrderPageMap">
<select id="getOrderPage" resultMap="OrderMap">
SELECT
id,
order_sn,
total_amount,
pay_amount,
pay_type,
status,
total_amount,
total_quantity,
create_time,
member_id,
source_type
FROM oms_order
t1.id,
t1.order_sn,
t1.total_amount,
t1.payment_amount,
t1.payment_method,
t1.status,
t1.total_amount,
t1.total_quantity,
t1.create_time,
t1.member_id,
t1.source,
t1.remark
FROM
oms_order t1
<where>
<if test ='queryParams.keywords !=null and queryParams.keywords.trim() neq ""' >
AND
(
t1.order_sn like concat('%',#{queryParams.keywords},'%')
)
</if>
<if test ='queryParams.status !=null ' >
AND status= #{queryParams.status}
AND t1.status= #{queryParams.status}
</if>
<if test ='queryParams.memberId !=null ' >
AND member_id= #{queryParams.memberId}
</if>
<if test ='queryParams.orderSn !=null and queryParams.orderSn.trim() neq ""' >
AND order_sn like concat('%',#{queryParams.orderSn},'%')
<if test ='queryParams.beginDate !=null' >
AND t1.create_time &gt;= #{queryParams.beginDate}
</if>
<if test ='queryParams.beginDate !=null and queryParams.beginDate.trim() neq ""' >
AND date_format (create_time,'%Y-%m-%d') &gt;= date_format(#{queryParams.beginDate},'%Y-%m-%d')
</if>
<if test ='queryParams.endDate !=null and queryParams.endDate.trim() neq ""' >
AND date_format (create_time,'%Y-%m-%d') &lt;= date_format(#{queryParams.endDate},'%Y-%m-%d')
<if test ='queryParams.endDate !=null' >
AND t1.create_time &lt;= #{queryParams.endDate}
</if>
</where>
ORDER BY create_time DESC
ORDER BY
t1.create_time DESC
</select>
</mapper>

View File

@ -0,0 +1,242 @@
package com.youlai.mall.oms.controller;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.youlai.mall.oms.enums.OrderSourceEnum;
import com.youlai.mall.oms.enums.PaymentMethodEnum;
import com.youlai.mall.oms.model.form.OrderPaymentForm;
import com.youlai.mall.oms.model.form.OrderSubmitForm;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.Base64;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 订单单元测试
*
* @author haoxr
* @since 2.3.0
*/
@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
public class OrderControllerTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private RestTemplate restTemplate;
private final String mobile = "18866668888";// 商城会员手机号
private final String verifyCode = "666666";// 短信验证码666666是免校验验证码
private final Long skuId = 1L;// 购买商品ID
/**
* 购买商品-正常流程测试
*/
@Test
void testPurchaseFlow_Normal() throws Exception {
// 会员登录
String accessToken = acquireTokenByLogin(mobile, verifyCode); // 获取 accessToken填充请求头用于身份认证
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
// 添加购物车
this.addToCard(skuId, headers);
// 订单确认
String orderToken = this.confirmOrder(headers); // 返回订单提交令牌用于订单提交
// 订单提交
String orderSn = this.submitOrder(orderToken, headers); // 返回订单编号用于订单支付
// 订单支付
this.payOrder(orderSn, headers); // 支付成功商品库存扣减账户余额扣减订单状态改变(待支付 待发货)
}
/**
* 购买商品-超时未支付流程测试
*/
@Test
void testPurchaseFlow_PaymentTimeout() throws Exception {
// 会员登录
String accessToken = acquireTokenByLogin(mobile, verifyCode); // 获取 accessToken填充请求头用于身份认证
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
// 添加购物车
this.addToCard(skuId, headers);
// 订单确认
String orderToken = this.confirmOrder(headers); // 返回订单提交令牌用于订单提交
// 订单提交
String orderSn = this.submitOrder(orderToken, headers); // 返回订单编号用于订单支付
// 模拟等待超过支付超时时间
Thread.sleep(30 * 1000); // OrderRabbitConfig#orderDelayQueue#x-message-ttl 设置10s未支付取消
// 订单支付
this.payOrder(orderSn, headers); // 此处支付会异常因为超时未支付订单已被系统自动取消无法再进行支付
}
/**
* 添加商品至购物车
*/
private void addToCard(Long skuId, HttpHeaders headers) throws Exception {
mockMvc.perform(post("/app-api/v1/carts")
.param("skuId", String.valueOf(skuId))
.headers(headers))
.andExpect(status().isOk())
.andReturn();
}
/**
* 订单确认
*/
private String confirmOrder(HttpHeaders headers) throws Exception {
MvcResult confirmResult = mockMvc.perform(
post("/app-api/v1/orders/confirm")
.headers(headers)
).andExpect(status().isOk())
.andReturn();
String confirmJsonResponse = confirmResult.getResponse().getContentAsString();
log.info("订单确认响应:{}", confirmJsonResponse);
JsonNode confirmJsonNode = objectMapper.readTree(confirmJsonResponse);
return confirmJsonNode.path("data").path("orderToken").asText();
}
/**
* 订单提交
*/
private String submitOrder(String orderToken, HttpHeaders headers) throws Exception {
// 构造请求体
OrderSubmitForm submitForm = new OrderSubmitForm();
// submitForm - 商品列表
OrderSubmitForm.OrderItem orderItem = new OrderSubmitForm.OrderItem();
orderItem.setSkuId(skuId);
orderItem.setQuantity(1);
orderItem.setSkuName("REDMI K60 16G+1T");
orderItem.setSkuSn("sn001");
orderItem.setSpuName("REDMI K60");
orderItem.setPrice(399900L);
orderItem.setPicUrl("https://www.youlai.tech/files/default/c25b39470474494485633c49101a0f5d.png");
submitForm.setOrderItems(Arrays.asList(orderItem));
// submitForm - 收货地址
OrderSubmitForm.ShippingAddress shippingAddress = new OrderSubmitForm.ShippingAddress();
shippingAddress.setProvince("上海");
shippingAddress.setCity("上海市");
shippingAddress.setDistrict("浦东新区");
shippingAddress.setConsigneeName("法外张三");
shippingAddress.setConsigneeMobile("18866668888");
shippingAddress.setDetailAddress("世纪公园");
submitForm.setShippingAddress(shippingAddress);
// submitForm - 订单信息
submitForm.setOrderToken(orderToken);
submitForm.setPaymentAmount(orderItem.getPrice() * 1);
submitForm.setOrderSource(OrderSourceEnum.APP);
submitForm.setRemark("单元测试生成订单");
// 发起 POST 请求
MockHttpServletRequestBuilder requestBuilder = post("/app-api/v1/orders/submit")
.headers(headers)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(submitForm));
// 执行请求并断言结果
MvcResult submitResult = mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andReturn();
String confirmJsonResponse = submitResult.getResponse().getContentAsString();
JsonNode confirmJsonNode = objectMapper.readTree(confirmJsonResponse);
return confirmJsonNode.path("data").asText();
}
/**
* 订单支付
*/
private void payOrder(String orderSn, HttpHeaders headers) throws Exception {
OrderPaymentForm paymentForm = new OrderPaymentForm();
paymentForm.setOrderSn(orderSn);
paymentForm.setPaymentMethod(PaymentMethodEnum.BALANCE);
mockMvc.perform(post("/app-api/v1/orders/payment")
.headers(headers)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(paymentForm))
).andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
}
/**
* 登录获取访问令牌
*
* @param mobile 手机号
* @param verifyCode 短信验证码
* @return
*/
private String acquireTokenByLogin(String mobile, String verifyCode) {
String clientId = "mall-app";
String clientSecret = "123456";
String tokenUrl = "http://localhost:9000/oauth2/token";
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 构建请求体
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
requestBody.add("grant_type", "sms_code");
requestBody.add("client_id", clientId);
requestBody.add("client_secret", clientSecret);
requestBody.add("mobile", mobile);
requestBody.add("code", verifyCode);
// 创建 Basic Auth 头部
String authHeader = clientId + ":" + clientSecret;
String encodedAuthHeader = Base64.getEncoder().encodeToString(authHeader.getBytes());
headers.set("Authorization", "Basic " + encodedAuthHeader);
// 创建请求实体
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
// 发送请求
String jsonStr = restTemplate.postForEntity(tokenUrl, requestEntity, String.class).getBody();
return JSONUtil.parseObj(jsonStr).getJSONObject("data").getStr("access_token");
}
}

View File

@ -1,20 +0,0 @@
package com.youlai.mall.oms.controller;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void payOrderTest() {
rabbitTemplate.convertAndSend("order.exchange", "order.create.routing.key", "4acd475a-c6aa-4d9a-a3a5-40da7472cbee");
}
}

View File

@ -0,0 +1,36 @@
package com.youlai.mall.oms.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OmsOrderPageVO;
import com.youlai.mall.oms.service.admin.OmsOrderService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
class OmsOrderServiceImplTest {
@Autowired
private OmsOrderService omsOrderService;
@Test
void testGetOrderPage() {
OrderPageQuery queryParams = new OrderPageQuery();
queryParams.setPageNum(1);
queryParams.setPageSize(10);
queryParams.setBeginDate(DateUtil.parseDate("2022-01-01"));
queryParams.setEndDate(DateUtil.parseDate("2025-01-01"));
IPage<OmsOrderPageVO> orderPage = omsOrderService.getOrderPage(queryParams);
log.info(JSONUtil.toJsonStr(orderPage));
}
}

View File

@ -0,0 +1,35 @@
package com.youlai.mall.oms.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.mall.oms.model.query.OrderPageQuery;
import com.youlai.mall.oms.model.vo.OrderPageVO;
import com.youlai.mall.oms.service.app.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
class OrderServiceImplTest {
@Autowired
private OrderService orderService;
@Test
void testGetOrderPage() {
OrderPageQuery queryParams = new OrderPageQuery();
queryParams.setPageNum(1);
queryParams.setPageSize(10);
queryParams.setBeginDate(DateUtil.parseDate("2022-01-01"));
queryParams.setEndDate(DateUtil.parseDate("2025-01-01"));
IPage<OrderPageVO> orderPage = orderService.getOrderPage(queryParams);
log.info(JSONUtil.toJsonStr(orderPage));
}
}

View File

@ -18,6 +18,12 @@
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>

View File

@ -1,48 +1,53 @@
package com.youlai.mall.pms.api;
import com.youlai.common.result.Result;
import com.youlai.mall.pms.model.dto.CheckPriceDTO;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.LockStockDTO;
import com.youlai.common.web.config.FeignDecoderConfig;
import com.youlai.mall.pms.model.dto.LockedSkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(value = "mall-pms", contextId = "sku")
@FeignClient(value = "mall-pms", contextId = "sku", configuration = {FeignDecoderConfig.class})
public interface SkuFeignClient {
/**
* 获取商品库存单元信息
* 获取商品库存信息
*/
@GetMapping("/app-api/v1/sku/{skuId}/info")
Result<SkuDTO> getSkuInfo(@PathVariable Long skuId);
@GetMapping("/app-api/v1/skus/{skuId}")
SkuInfoDTO getSkuInfo(@PathVariable Long skuId);
/**
* 获取商品库存信息列表
*
* @param skuIds SKU ID 列表
* @return 商品库存信息列表
*/
@GetMapping("/app-api/v1/skus")
List<SkuInfoDTO> getSkuInfoList(@RequestParam List<Long> skuIds);
/**
* 锁定商品库存
*/
@PutMapping("/app-api/v1/sku/_lock")
Result lockStock(@RequestBody LockStockDTO lockStockDTO);
@PutMapping("/app-api/v1/skus/lock")
boolean lockStock(@RequestParam String orderToken, @RequestBody List<LockedSkuDTO> lockedSkuList);
/**
* 解锁商品库存
*/
@PutMapping("/app-api/v1/sku/_unlock")
Result unlockStock(@RequestParam String orderSn);
@PutMapping("/app-api/v1/skus/unlock")
boolean unlockStock(@RequestParam String orderSn);
/**
* 扣减订单商品库存
*/
@PutMapping("/app-api/v1/sku/_deduct")
Result deductStock(@RequestParam String orderSn);
/**
* 订单商品验价
* <p>
* 扣减指定订单商品的库存数量
*
* @param checkPriceDTO
* @param orderSn 订单编号
* @return 扣减库存结果
*/
@PostMapping("/app-api/v1/sku/price/_check")
Result<Boolean> checkPrice(@RequestBody CheckPriceDTO checkPriceDTO);
@PutMapping("/app-api/v1/skus/deduct")
boolean deductStock(@RequestParam String orderSn);
}

View File

@ -11,7 +11,7 @@ import java.util.List;
* 订单商品验价传输对象
*
* @author haoxr
* @since 2022/2/7
* @date 2022/2/7
*/
@Data
@NoArgsConstructor
@ -19,20 +19,15 @@ import java.util.List;
@ToString
public class CheckPriceDTO {
/**
* 订单编号
*/
private String orderSn;
/**
* 订单总金额
*/
private Long orderTotalAmount;
private Long totalAmount;
/**
* 订单商品明细
*/
private List<OrderSku> orderSkus;
private List<OrderSku> skus;
/**
* 订单商品对象

View File

@ -1,49 +0,0 @@
package com.youlai.mall.pms.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
/**
* 锁定库存传输对象
*
* @author haoxr
* @since 2022/12/20
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class LockStockDTO {
/**
* 订单编号
*/
private String orderSn;
/**
* 锁定商品集合
*/
private List<LockedSku> lockedSkus;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class LockedSku {
/**
* 锁定商品ID
*/
private Long skuId;
/**
* 商品数量
*/
private Integer count;
}
}

View File

@ -0,0 +1,37 @@
package com.youlai.mall.pms.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 锁定库存传输对象
*
* @author haoxr
* @since 2.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LockedSkuDTO {
/**
* 商品ID
*/
private Long skuId;
/**
* 商品数量
*/
private Integer quantity;
/**
* 商品编码
*/
private String skuSn;
}

View File

@ -3,18 +3,20 @@ package com.youlai.mall.pms.model.dto;
import lombok.Data;
/**
* SKU信息传输对象
* 商品库存信息DTO
* <p>
* 用于表示商品的库存信息
*
* @author haoxr
* @since 2022/2/5 23:09
* @since 2.0.0
*/
@Data
public class SkuDTO {
public class SkuInfoDTO {
/**
* skuId
* SKU的唯一标识符
*/
private Long skuId;
private Long id;
/**
* SKU 编号
*/
@ -34,9 +36,9 @@ public class SkuDTO {
/**
* SKU 库存数量
*/
private Integer stockNum;
private Integer stock;
/**
* SPU 名称
* 所属SPU的名称
*/
private String spuName;
}

View File

@ -0,0 +1,26 @@
package com.youlai.mall.pms.common.constant;
/**
* 商品常量
*
* @author haoxr
* @since 2.0.0
*/
public interface ProductConstants {
/**
* 锁定的商品列表缓存键前缀
*/
String LOCKED_SKUS_PREFIX = "product:locked_skus:";
/**
* 商品分布式锁缓存键前缀
*/
String SKU_LOCK_PREFIX = "product:sku_lock:";
/**
* 临时规格ID前缀
*/
String SPEC_TEMP_ID_PREFIX = "tid_";
}

View File

@ -1,4 +1,4 @@
package com.youlai.mall.pms.enums;
package com.youlai.mall.pms.common.enums;
import lombok.Getter;
@ -6,7 +6,7 @@ import lombok.Getter;
* 商品属性类型枚举
*
* @author haoxr
* @since 2022/12/20
* @date 2022/12/20
*/
public enum AttributeTypeEnum {

View File

@ -0,0 +1,69 @@
package com.youlai.mall.pms.common.util;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
import static com.google.common.base.Preconditions.checkArgument;
/**
* 布隆过滤器摘录自Google-guava包
*
* @author DaniR
* @date 2021/6/23 20:30
*/
public class BloomFilterUtils<T> {
private final int numHashFunctions;
private final int bitSize;
private final Funnel<T> funnel;
public BloomFilterUtils(Funnel<T> funnel, int expectedInsertions, double fpp) {
checkArgument(funnel != null, "funnel不能为空");
checkArgument(
expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
this.funnel = funnel;
// 计算bit数组长度
bitSize = optimalNumOfBits(expectedInsertions, fpp);
// 计算hash方法执行次数
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
public int[] murmurHash(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
offset[i - 1] = combinedHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
// 设定最小期望长度
p = Double.MIN_VALUE;
}
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash方法执行次数
*/
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}

View File

@ -15,7 +15,7 @@ import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 管理端商品控制层
* ADMIN-商品控制层
*
* @author haoxr
* @since 2021/1/4
@ -28,38 +28,38 @@ public class PmsSpuController {
private SpuService spuServiced;
@Operation(summary= "商品分页列表")
@Operation(summary = "商品分页列表")
@GetMapping("/pages")
public PageResult listPmsSpuPages(SpuPageQuery queryParams) {
IPage<PmsSpuPageVO> result = spuServiced.listPmsSpuPages(queryParams);
IPage<PmsSpuPageVO> result = spuServiced.getSpuPage(queryParams);
return PageResult.success(result);
}
@Operation(summary= "商品详情")
@Operation(summary = "商品详情")
@GetMapping("/{id}")
public Result detail( @Parameter(name = "商品ID") @PathVariable Long id) {
public Result detail(@Parameter(name = "商品ID") @PathVariable Long id) {
PmsSpuDetailVO pmsSpuDetailVO = spuServiced.getPmsSpuDetail(id);
return Result.success(pmsSpuDetailVO);
}
@Operation(summary= "新增商品")
@Operation(summary = "新增商品")
@PostMapping
public Result addSpu(@RequestBody PmsSpuForm formData) {
boolean result = spuServiced.addSpu(formData);
return Result.judge(result);
}
@Operation(summary= "修改商品")
@Operation(summary = "修改商品")
@PutMapping(value = "/{id}")
public Result updateSpuById(
@Parameter(name = "商品ID") @PathVariable Long id,
@RequestBody PmsSpuForm formData
) {
boolean result = spuServiced.updateSpuById(id,formData);
boolean result = spuServiced.updateSpuById(id, formData);
return Result.judge(result);
}
@Operation(summary= "删除商品")
@Operation(summary = "删除商品")
@DeleteMapping("/{ids}")
public Result delete(@Parameter(name = "商品ID,多个以英文逗号(,)分隔") @PathVariable String ids) {
boolean result = spuServiced.removeBySpuIds(ids);

View File

@ -3,9 +3,9 @@ package com.youlai.mall.pms.controller.app;
import com.youlai.common.result.Result;
import com.youlai.mall.pms.model.vo.CategoryVO;
import com.youlai.mall.pms.service.CategoryService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -27,10 +27,9 @@ public class CategoryController {
private final CategoryService categoryService;
@Operation(summary= "分类列表")
@Operation(summary = "分类列表")
@GetMapping
public Result list(
@Parameter(name = "上级分类ID") Long parentId) {
public Result list(@Parameter(name = "上级分类ID") Long parentId) {
List<CategoryVO> list = categoryService.listCategory(parentId);
return Result.success(list);
}

View File

@ -1,9 +1,8 @@
package com.youlai.mall.pms.controller.app;
import com.youlai.common.result.Result;
import com.youlai.mall.pms.model.dto.CheckPriceDTO;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.LockStockDTO;
import com.youlai.mall.pms.model.dto.LockedSkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.service.SkuService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@ -11,63 +10,61 @@ import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* SKU控制层
* 商品库存
*
* @author haoxr
* @since 2022/12/21
* @since 2.0.0
*/
@Tag(name = "「移动端」SKU接口")
@Tag(name = "「移动端」商品库存接口")
@RestController
@RequestMapping("/app-api/v1/sku")
@RequestMapping("/app-api/v1/skus")
@RequiredArgsConstructor
public class SkuController {
private final SkuService skuService;
@Operation(summary = "获取商品库存信息")
@GetMapping("/{skuId}/info")
public Result<SkuDTO> getSkuInfo(
@Parameter(name = "商品ID") @PathVariable Long skuId
@GetMapping("/{skuId}")
public Result<SkuInfoDTO> getSkuInfo(
@Parameter(name ="商品ID") @PathVariable Long skuId
) {
SkuDTO skuInfo = skuService.getSkuInfo(skuId);
SkuInfoDTO skuInfo = skuService.getSkuInfo(skuId);
return Result.success(skuInfo);
}
@Operation(summary ="获取商品库存数量")
@GetMapping("/{skuId}/stock_num")
public Result<Integer> getStockNum(
@Parameter(name = "商品ID") @PathVariable Long skuId
@Operation(summary = "获取商品库存列表")
@GetMapping
public Result<List<SkuInfoDTO>> getSkuInfoList(
@Parameter(name ="SKU ID 列表") @RequestParam List<Long> skuIds
) {
Integer stockNum = skuService.getStockNum(skuId);
return Result.success(stockNum);
List<SkuInfoDTO> skuInfos = skuService.listSkuInfos(skuIds);
return Result.success(skuInfos);
}
@Operation(summary = "锁定库存")
@PutMapping("/_lock")
public Result lockStock(@RequestBody LockStockDTO lockStockDTO) {
boolean lockResult = skuService.lockStock(lockStockDTO);
return Result.success(lockResult);
@Operation(summary = "校验并锁定库存")
@PutMapping("/lock")
public Result<?> lockStock(
@RequestParam String orderToken,
@RequestBody List<LockedSkuDTO> lockedSkuList
) {
boolean lockStockResult = skuService.lockStock(orderToken,lockedSkuList);
return Result.success(lockStockResult);
}
@Operation(summary = "解锁库存")
@PutMapping("/_unlock")
public Result<Boolean> unlockStock(String orderToken) {
@PutMapping("/unlock")
public Result<?> unlockStock(String orderToken) {
boolean result = skuService.unlockStock(orderToken);
return Result.judge(result);
}
@Operation(summary = "扣减库存")
@PutMapping("/_deduct")
public Result<Boolean> deductStock(String orderToken) {
boolean result = skuService.deductStock(orderToken);
@PutMapping("/deduct")
public Result<?> deductStock(String orderSn) {
boolean result = skuService.deductStock(orderSn);
return Result.judge(result);
}
@Operation(summary = "商品验价")
@PostMapping("/price/_check")
public Result<Boolean> checkPrice(@RequestBody CheckPriceDTO checkPriceDTO) {
boolean result = skuService.checkPrice(checkPriceDTO);
return Result.success(result);
}
}

View File

@ -5,8 +5,8 @@ import com.youlai.common.result.PageResult;
import com.youlai.common.result.Result;
import com.youlai.mall.pms.model.query.SpuPageQuery;
import com.youlai.mall.pms.model.vo.SeckillingSpuVO;
import com.youlai.mall.pms.model.vo.SpuPageVO;
import com.youlai.mall.pms.model.vo.SpuDetailVO;
import com.youlai.mall.pms.model.vo.SpuPageVO;
import com.youlai.mall.pms.service.SpuService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "「移动端」商品接口")
@Tag(name = "「移动端」商品接口")
@RestController
@RequestMapping("/app-api/v1/spu")
@RequiredArgsConstructor
@ -27,23 +27,23 @@ public class SpuController {
private final SpuService spuService;
@Operation(summary= "商品分页列表")
@Operation(summary = "商品分页列表")
@GetMapping("/pages")
public PageResult listSpuPages(SpuPageQuery queryParams) {
IPage<SpuPageVO> result = spuService.listSpuPages(queryParams);
public PageResult getSpuPageForApp(SpuPageQuery queryParams) {
IPage<SpuPageVO> result = spuService.getSpuPageForApp(queryParams);
return PageResult.success(result);
}
@Operation(summary= "获取商品详情")
@Operation(summary = "获取商品详情")
@GetMapping("/{spuId}")
public Result<SpuDetailVO> getSpuDetail(
@Parameter(name = "商品ID") @PathVariable Long spuId
@Parameter(name ="商品ID") @PathVariable Long spuId
) {
SpuDetailVO spuDetailVO = spuService.getSpuDetail(spuId);
return Result.success(spuDetailVO);
}
@Operation(summary= "获取秒杀商品列表")
@Operation(summary = "获取秒杀商品列表")
@GetMapping("/seckilling")
public Result<List<SeckillingSpuVO>> listSeckillingSpu() {
List<SeckillingSpuVO> list = spuService.listSeckillingSpu();

View File

@ -0,0 +1,21 @@
package com.youlai.mall.pms.converter;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku;
import org.mapstruct.Mapper;
import java.util.List;
/**
* 商品对象转换器
*
* @author haoxr
* @since 2022/6/11
*/
@Mapper(componentModel = "spring")
public interface SkuConverter {
SkuInfoDTO entity2SkuInfoDto(PmsSku entity);
List<SkuInfoDTO> entity2SkuInfoDto(List<PmsSku> list);
}

View File

@ -10,7 +10,7 @@ import org.mapstruct.Mappings;
* 商品属性对象转换器
*
* @author haoxr
* @since 2022/6/11
* @date 2022/6/11
*/
@Mapper(componentModel = "spring")
public interface SpuAttributeConverter {

View File

@ -14,7 +14,7 @@ import java.util.List;
* 商品对象转换器
*
* @author haoxr
* @since 2022/6/11
* @date 2022/6/11
*/
@Mapper(componentModel = "spring")
public interface SpuConverter {

View File

@ -1,7 +1,7 @@
package com.youlai.mall.pms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku;
import org.apache.ibatis.annotations.Mapper;
@ -14,5 +14,5 @@ public interface PmsSkuMapper extends BaseMapper<PmsSku> {
* @param skuId
* @return
*/
SkuDTO getSkuInfo(Long skuId);
SkuInfoDTO getSkuInfo(Long skuId);
}

View File

@ -20,7 +20,7 @@ public interface PmsSpuMapper extends BaseMapper<PmsSpu> {
* @param queryParams
* @return
*/
List<PmsSpuPageVO> listPmsSpuPages(Page<PmsSpuPageVO> page, SpuPageQuery queryParams);
List<PmsSpuPageVO> getSpuPage(Page<PmsSpuPageVO> page, SpuPageQuery queryParams);
/**
* 应用端商品分页列表
@ -29,7 +29,7 @@ public interface PmsSpuMapper extends BaseMapper<PmsSpu> {
* @param queryParams
* @return
*/
List<SpuPageVO> listSpuPages(Page<SpuPageVO> page, SpuPageQuery queryParams);
List<SpuPageVO> getSpuPageForApp(Page<SpuPageVO> page, SpuPageQuery queryParams);
}

View File

@ -1,8 +1,8 @@
package com.youlai.mall.pms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.pms.model.form.PmsCategoryAttributeForm;
import com.youlai.mall.pms.model.entity.PmsCategoryAttribute;
import com.youlai.mall.pms.model.form.PmsCategoryAttributeForm;
public interface AttributeService extends IService<PmsCategoryAttribute> {

View File

@ -1,41 +1,45 @@
package com.youlai.mall.pms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.pms.model.dto.CheckPriceDTO;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.LockedSkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku;
import com.youlai.mall.pms.model.dto.LockStockDTO;
import java.util.List;
/**
* 商品库存单元接口
* 商品库存接口
*
* @author haoxr
* @since 2022/2/5 17:11
* @since 2.0.0
*/
public interface SkuService extends IService<PmsSku> {
/**
* 获取商品的库存数量
*
* @param skuId
* @return
*/
Integer getStockNum(Long skuId);
/**
* 获取商品库存信息
*
* @param skuId
* @return
* @param skuId SKU ID
* @return 商品库存信息
*/
SkuDTO getSkuInfo(Long skuId);
SkuInfoDTO getSkuInfo(Long skuId);
/**
* 锁定库存
* 获取商品库存信息列表
*
* @param skuIds SKU ID 列表
* @return 商品库存信息列表
*/
boolean lockStock(LockStockDTO lockStockDTO);
List<SkuInfoDTO> listSkuInfos(List<Long> skuIds);
/**
* 校验并锁定库存
*
* @param orderToken 订单临时编号 (此时订单未创建)
* @param lockedSkuList 锁定商品库存信息列表
* @return true/false
*/
boolean lockStock(String orderToken,List<LockedSkuDTO> lockedSkuList);
/**
* 解锁库存
@ -48,12 +52,4 @@ public interface SkuService extends IService<PmsSku> {
boolean deductStock(String orderSn);
/**
* 商品验价
*
* @param checkPriceDTO
* @return
*/
boolean checkPrice(CheckPriceDTO checkPriceDTO);
}

View File

@ -2,8 +2,8 @@ package com.youlai.mall.pms.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.pms.model.form.PmsSpuForm;
import com.youlai.mall.pms.model.entity.PmsSpu;
import com.youlai.mall.pms.model.form.PmsSpuForm;
import com.youlai.mall.pms.model.query.SpuPageQuery;
import com.youlai.mall.pms.model.vo.*;
@ -13,7 +13,7 @@ import java.util.List;
* 商品业务接口
*
* @author haoxr
* @since 2022/2/5
* @date 2022/2/5
*/
public interface SpuService extends IService<PmsSpu> {
@ -24,7 +24,7 @@ public interface SpuService extends IService<PmsSpu> {
* @param queryParams
* @return
*/
IPage<PmsSpuPageVO> listPmsSpuPages(SpuPageQuery queryParams);
IPage<PmsSpuPageVO> getSpuPage(SpuPageQuery queryParams);
/**
* 应用端商品分页列表
@ -32,7 +32,7 @@ public interface SpuService extends IService<PmsSpu> {
* @param queryParams
* @return
*/
IPage<SpuPageVO> listSpuPages(SpuPageQuery queryParams);
IPage<SpuPageVO> getSpuPageForApp(SpuPageQuery queryParams);
/**

View File

@ -4,8 +4,8 @@ import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.pms.mapper.PmsCategoryAttributeMapper;
import com.youlai.mall.pms.model.form.PmsCategoryAttributeForm;
import com.youlai.mall.pms.model.entity.PmsCategoryAttribute;
import com.youlai.mall.pms.model.form.PmsCategoryAttributeForm;
import com.youlai.mall.pms.service.AttributeService;
import org.springframework.stereotype.Service;
@ -17,7 +17,7 @@ import java.util.stream.Collectors;
* 商品属性业务实现类
*
* @author haoxr
* @since 2022/7/2
* @date 2022/7/2
*/
@Service
public class AttributeServiceImpl extends ServiceImpl<PmsCategoryAttributeMapper, PmsCategoryAttribute> implements AttributeService {

View File

@ -1,8 +1,8 @@
package com.youlai.mall.pms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.pms.model.entity.PmsBrand;
import com.youlai.mall.pms.mapper.PmsBrandMapper;
import com.youlai.mall.pms.model.entity.PmsBrand;
import com.youlai.mall.pms.service.BrandService;
import org.springframework.stereotype.Service;

View File

@ -5,10 +5,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.common.web.model.Option;
import com.youlai.mall.pms.model.entity.PmsCategory;
import com.youlai.mall.pms.mapper.PmsCategoryMapper;
import com.youlai.mall.pms.service.CategoryService;
import com.youlai.mall.pms.model.entity.PmsCategory;
import com.youlai.mall.pms.model.vo.CategoryVO;
import com.youlai.mall.pms.service.CategoryService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

View File

@ -2,30 +2,29 @@ package com.youlai.mall.pms.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.ProductConstants;
import com.youlai.mall.pms.common.constant.ProductConstants;
import com.youlai.mall.pms.converter.SkuConverter;
import com.youlai.mall.pms.mapper.PmsSkuMapper;
import com.youlai.mall.pms.model.dto.CheckPriceDTO;
import com.youlai.mall.pms.model.dto.SkuDTO;
import com.youlai.mall.pms.model.dto.LockStockDTO;
import com.youlai.mall.pms.model.dto.LockedSkuDTO;
import com.youlai.mall.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku;
import com.youlai.mall.pms.service.SkuService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 商品SKU业务实现类
* 商品库存业务实现类
*
* @author haoxr
* @since 2022/12/21
@ -37,68 +36,68 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
private final RedisTemplate redisTemplate;
private final RedissonClient redissonClient;
private final SkuConverter skuConverter;
/**
* 获取商品库存数量
* 获取商品库存信息
*
* @param skuId
* @return
* @param skuId SKU ID
* @return 商品库存信息
*/
@Override
@Cacheable(cacheNames = "pms", key = "'stock_num:'+#skuId")
public Integer getStockNum(Long skuId) {
PmsSku pmsSku = this.getOne(new LambdaQueryWrapper<PmsSku>().eq(PmsSku::getId, skuId)
.select(PmsSku::getStockNum));
Integer stockNum = pmsSku != null ? pmsSku.getStockNum() : 0;
return stockNum;
public SkuInfoDTO getSkuInfo(Long skuId) {
return this.baseMapper.getSkuInfo(skuId);
}
/**
* 锁定库存
* 获取商品库存信息列表
*
* @param lockStock 订单编号 + 锁定商品集合
* @param skuIds SKU ID 列表
* @return 商品库存信息列表
*/
@Override
public List<SkuInfoDTO> listSkuInfos(List<Long> skuIds) {
List<PmsSku> list = this.list(new LambdaQueryWrapper<PmsSku>().in(PmsSku::getId, skuIds));
return skuConverter.entity2SkuInfoDto(list);
}
/**
* 校验并锁定库存
*
* @param orderToken 订单临时编号 (此时订单未创建)
* @param lockedSkuList 锁定商品库存信息列表
* @return true/false
*/
@Override
@Transactional
public boolean lockStock(LockStockDTO lockStock) {
String orderSn = lockStock.getOrderSn();
log.info("创建订单【{}】锁定商品库存:{}", orderSn, lockStock);
public boolean lockStock(String orderToken, List<LockedSkuDTO> lockedSkuList) {
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkuList), "订单({})未包含任何商品", orderToken);
List<LockStockDTO.LockedSku> lockedSkus = lockStock.getLockedSkus();
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkus), "锁定的商品为空");
// 循环遍历锁定商品
for (LockStockDTO.LockedSku lockedSku : lockedSkus) {
// 获取商品分布式锁
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId());
// 加锁
lock.lock();
// 校验库存数量是否足够以及锁定库存
for (LockedSkuDTO lockedSku : lockedSkuList) {
Long skuId = lockedSku.getSkuId();
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + skuId); // 构建商品锁对象
try {
lock.lock();
Integer quantity = lockedSku.getQuantity(); // 订单的商品数量
// 库存足够
boolean lockResult = this.update(new LambdaUpdateWrapper<PmsSku>()
.setSql("locked_stock_num = locked_stock_num + " + lockedSku.getCount())
.setSql("locked_stock = locked_stock + " + quantity) // 修改锁定商品数
.eq(PmsSku::getId, lockedSku.getSkuId())
.apply("stock_num - locked_stock_num >= {0}", lockedSku.getCount())
.apply("stock - locked_stock >= {0}", quantity) // 剩余商品数 订单商品数
);
Assert.isTrue(lockResult, "锁定商品 {} 失败", lockedSku.getSkuId());
Assert.isTrue(lockResult, "商品({})库存不足", lockedSku.getSkuSn());
} finally {
// 释放锁
if (lock.isLocked()) {
lock.unlock();
}
}
}
/**
* 将锁定的商品和数量缓存至Redis用于后续的场景:
*
* 1.订单取消解锁库存
* 2.订单支付扣减库存
*/
redisTemplate.opsForValue().set(ProductConstants.ORDER_LOCKED_SKUS_PREFIX + orderSn, lockedSkus);
// 锁定的商品缓存至 Redis (后续使用1.取消订单解锁库存2支付订单扣减库存)
redisTemplate.opsForValue().set(ProductConstants.LOCKED_SKUS_PREFIX + orderToken, lockedSkuList);
return true;
}
@ -107,137 +106,75 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
* <p>
* 订单超时未支付释放锁定的商品库存
*
* @param orderSn 订单号
* @return true/false
*/
@Override
public boolean unlockStock(String orderSn) {
log.info("订单取消:释放订单【{}】锁定的商品库存", orderSn);
List<LockStockDTO.LockedSku> lockedSkus = (List<LockStockDTO.LockedSku>) redisTemplate.opsForValue()
.get(ProductConstants.ORDER_LOCKED_SKUS_PREFIX + orderSn);
List<LockedSkuDTO> lockedSkus = (List<LockedSkuDTO>) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
log.info("释放订单({})锁定的商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus));
// 遍历解锁商品
if (CollectionUtil.isNotEmpty(lockedSkus)) {
for (LockStockDTO.LockedSku lockedSku : lockedSkus) {
// 获取商品分布式锁
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId());
// 加锁
// 库存已释放
if (CollectionUtil.isEmpty(lockedSkus)) {
return true;
}
// 遍历恢复锁定的商品库存
for (LockedSkuDTO lockedSku : lockedSkus) {
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁
try {
lock.lock();
try {
this.update(new LambdaUpdateWrapper<PmsSku>()
.eq(PmsSku::getId, lockedSku.getSkuId())
.setSql("locked_stock_num = locked_stock_num - " + lockedSku.getCount()));
} finally {
// 释放锁
if (lock.isLocked()) {
lock.unlock();
}
this.update(new LambdaUpdateWrapper<PmsSku>()
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
.eq(PmsSku::getId, lockedSku.getSkuId())
);
} finally {
if (lock.isLocked()) {
lock.unlock();
}
}
}
// 移除订单的商品信息缓存
redisTemplate.delete(ProductConstants.ORDER_LOCKED_SKUS_PREFIX + orderSn);
// 移除 redis 订单锁定的商品
redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
return true;
}
/**
* 扣减库存
* <p>
* 订单支付完成扣减订单商品库存和释放锁定库存
* 订单支付扣减商品库存和释放锁定库存
*
* @param orderSn 订单编号
* @return ture/false
*/
@Override
public boolean deductStock(String orderSn) {
log.info("订单【{}】支付成功:扣减订单商品库存", orderSn);
// 获取订单提交时锁定的商品
List<LockStockDTO.LockedSku> lockedSkus = (List<LockStockDTO.LockedSku>) redisTemplate.opsForValue()
.get(ProductConstants.ORDER_LOCKED_SKUS_PREFIX + orderSn);
List<LockedSkuDTO> lockedSkus = (List<LockedSkuDTO>) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
log.info("订单({})支付成功,扣减订单商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus));
if (CollectionUtil.isNotEmpty(lockedSkus)) {
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkus), "扣减商品库存失败:订单({})未包含商品");
for (LockStockDTO.LockedSku lockedSku : lockedSkus) {
// 获取商品分布式锁
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId());
// 加锁
for (LockedSkuDTO lockedSku : lockedSkus) {
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁
try {
lock.lock();
try {
this.update(new LambdaUpdateWrapper<PmsSku>()
.eq(PmsSku::getId, lockedSku.getSkuId())
.setSql("stock_num = stock_num - " + lockedSku.getCount())
.setSql("locked_stock_num = locked_stock_num - " + lockedSku.getCount())
);
} finally {
// 释放锁
if (lock.isLocked()) {
lock.unlock();
}
this.update(new LambdaUpdateWrapper<PmsSku>()
.setSql("stock = stock - " + lockedSku.getQuantity())
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
.eq(PmsSku::getId, lockedSku.getSkuId())
);
} finally {
if (lock.isLocked()) {
lock.unlock();
}
}
}
// 移除订单的商品缓存
redisTemplate.delete(ProductConstants.ORDER_LOCKED_SKUS_PREFIX + orderSn);
// 移除订单锁定的商品
redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
return true;
}
/**
* 订单商品验价
* <p>
* 判断订单总金额和订单商品实时价格之和是否相等
* 不相等商品实时价格和页面价格不等订单无效
*
* @param checkPrice 验价参数(订单总金额订单商品明细)
* @return true/false
*/
@Override
public boolean checkPrice(CheckPriceDTO checkPrice) {
log.info("订单【{}】商品验价:{}", checkPrice);
// 订单总金额
Long orderTotalAmount = checkPrice.getOrderTotalAmount();
// 计算商品总金额
List<CheckPriceDTO.OrderSku> orderSkus = checkPrice.getOrderSkus();
if (orderTotalAmount == null || CollectionUtil.isEmpty(orderSkus)) {
log.warn("订单【{}】验价失败:订单总金额或订单商品为空,无法进行验价!");
return false;
}
// 订单商品ID集合
List<Long> orderSkuIds = orderSkus.stream().map(orderItem -> orderItem.getSkuId())
.collect(Collectors.toList());
// 获取商品的实时价格
List<PmsSku> skuList = this.list(new LambdaQueryWrapper<PmsSku>().in(PmsSku::getId, orderSkuIds)
.select(PmsSku::getId, PmsSku::getPrice)
);
if (CollectionUtil.isEmpty(skuList)) {
log.warn("订单【{}】验价失败:订单商品库存不存在或已下架!");
return false;
}
// 计算商品实时总价
Long skuTotalAmount = skuList.stream().map(sku -> {
// 获取订单中该商品数量
CheckPriceDTO.OrderSku matchOrderSku = orderSkus.stream()
.filter(orderSku -> sku.getId().equals(orderSku.getSkuId()))
.findFirst().orElse(null);
// 单个商品实时总价 = 实时单价 * 订单该商品数量
return matchOrderSku != null ? sku.getPrice() * matchOrderSku.getCount() : 0L;
}).reduce(0L, Long::sum);
// 比较订单总价和商品实时总价
boolean checkPriceResult = orderTotalAmount.compareTo(skuTotalAmount) == 0;
return checkPriceResult;
}
/**
* 获取商品库存信息
*
* @param skuId
* @return
*/
@Override
public SkuDTO getSkuInfo(Long skuId) {
return this.baseMapper.getSkuInfo(skuId);
}
}

View File

@ -1,8 +1,8 @@
package com.youlai.mall.pms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.pms.model.entity.PmsSpuAttribute;
import com.youlai.mall.pms.mapper.PmsSpuAttributeMapper;
import com.youlai.mall.pms.model.entity.PmsSpuAttribute;
import com.youlai.mall.pms.service.SpuAttributeService;
import org.springframework.stereotype.Service;

View File

@ -10,16 +10,16 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.common.constant.ProductConstants;
import com.youlai.mall.pms.enums.AttributeTypeEnum;
import com.youlai.mall.pms.common.constant.ProductConstants;
import com.youlai.mall.pms.common.enums.AttributeTypeEnum;
import com.youlai.mall.pms.converter.SpuAttributeConverter;
import com.youlai.mall.pms.converter.SpuConverter;
import com.youlai.mall.pms.mapper.PmsSpuMapper;
import com.youlai.mall.pms.model.form.PmsSpuAttributeForm;
import com.youlai.mall.pms.model.form.PmsSpuForm;
import com.youlai.mall.pms.model.entity.PmsSku;
import com.youlai.mall.pms.model.entity.PmsSpu;
import com.youlai.mall.pms.model.entity.PmsSpuAttribute;
import com.youlai.mall.pms.model.form.PmsSpuAttributeForm;
import com.youlai.mall.pms.model.form.PmsSpuForm;
import com.youlai.mall.pms.model.query.SpuPageQuery;
import com.youlai.mall.pms.model.vo.*;
import com.youlai.mall.pms.service.SkuService;
@ -38,7 +38,7 @@ import java.util.stream.Collectors;
* 商品业务实现类
*
* @author <a href="mailto:xianrui0365@163.com">haoxr</a>
* @since 2021/8/8
* @date 2021/8/8
*/
@Service
@RequiredArgsConstructor
@ -59,9 +59,9 @@ public class SpuServiceImpl extends ServiceImpl<PmsSpuMapper, PmsSpu> implements
* @return
*/
@Override
public IPage<PmsSpuPageVO> listPmsSpuPages(SpuPageQuery queryParams) {
public IPage<PmsSpuPageVO> getSpuPage(SpuPageQuery queryParams) {
Page<PmsSpuPageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
List<PmsSpuPageVO> list = this.baseMapper.listPmsSpuPages(page, queryParams);
List<PmsSpuPageVO> list = this.baseMapper.getSpuPage(page, queryParams);
page.setRecords(list);
return page;
}
@ -73,9 +73,9 @@ public class SpuServiceImpl extends ServiceImpl<PmsSpuMapper, PmsSpu> implements
* @return
*/
@Override
public IPage<SpuPageVO> listSpuPages(SpuPageQuery queryParams) {
public IPage<SpuPageVO> getSpuPageForApp(SpuPageQuery queryParams) {
Page<SpuPageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
List<SpuPageVO> list = this.baseMapper.listSpuPages(page, queryParams);
List<SpuPageVO> list = this.baseMapper.getSpuPageForApp(page, queryParams);
page.setRecords(list);
return page;
}

View File

@ -11,22 +11,17 @@ spring:
nacos:
# 注册中心
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
server-addr: http://192.168.179.21:8848
# 配置中心
config:
# 本地启动
## server-addr: ${spring.cloud.nacos.discovery.server-addr}
# 极速启动
server-addr: http://localhost:8848
server-addr: http://192.168.179.21:8848
file-extension: yaml
shared-configs[0]:
data-id: youlai-common.yaml
refresh: true
username: nacos
password: nacos

View File

@ -18,7 +18,7 @@
</select>
<!-- 获取商品库存单元信息 -->
<select id="getSkuInfo" resultType="com.youlai.mall.pms.model.dto.SkuDTO">
<select id="getSkuInfo" resultType="com.youlai.mall.pms.model.dto.SkuInfoDTO">
select
t1.id skuId,
t1.sku_sn,

View File

@ -11,18 +11,14 @@ spring:
nacos:
# 注册中心
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
server-addr: http://192.168.179.21:8848
# 配置中心
config:
# 本地启动
## server-addr: ${spring.cloud.nacos.discovery.server-addr}
# 极速启动
server-addr: http://localhost:8848
server-addr: http://192.168.179.21:8848
file-extension: yaml
shared-configs[0]:
data-id: youlai-common.yaml
refresh: true
username: nacos
password: nacos
refresh: true

View File

@ -9,15 +9,11 @@ spring:
nacos:
# 注册中心
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
server-addr: http://192.168.179.21:8848
# 配置中心
config:
server-addr: http://localhost:8848
server-addr: http://192.168.179.21:8848
file-extension: yaml
shared-configs[0]:
data-id: youlai-common.yaml
refresh: true
username: nacos
password: nacos

15
pom.xml
View File

@ -49,7 +49,7 @@
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<!-- api doc -->
<knife4j.version>4.1.0</knife4j.version>
<knife4j.version>4.3.0</knife4j.version>
<swagger.version>2.1.0</swagger.version>
<!-- tools -->
@ -57,7 +57,6 @@
<mapstruct.version>1.5.5.Final</mapstruct.version>
<weixin-java.version>4.1.5.B</weixin-java.version>
<easyexcel.version>3.0.5</easyexcel.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<nimbus-jose-jwt.version>9.16.1</nimbus-jose-jwt.version>
<thumbnailator.version>0.4.19</thumbnailator.version>
@ -222,12 +221,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>${easy-captcha.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
@ -276,12 +269,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>

View File

@ -1,5 +1,6 @@
package com.youlai.auth.authentication.captcha;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
@ -81,13 +82,16 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 证码校验
// 证码校验
Map<String, Object> additionalParameters = captchaAuthenticationToken.getAdditionalParameters();
String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE);
String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE_KEY);
String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_KEY_PREFIX + verifyCodeKey);
if (!StrUtil.equals(verifyCode, cacheCode)) {
String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_KEY_PREFIX + verifyCodeKey);
// 验证码比对
MathGenerator mathGenerator = new MathGenerator();
if (!mathGenerator.verify(cacheCode, verifyCode)) {
throw new OAuth2AuthenticationException("验证码错误");
}

View File

@ -3,7 +3,7 @@ package com.youlai.auth.authentication.miniapp;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.lang.Assert;
import com.youlai.auth.userdetails.member.MemberDetailsService;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;

View File

@ -2,7 +2,7 @@ package com.youlai.auth.authentication.smscode;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.userdetails.member.MemberDetailsService;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import com.youlai.common.constant.SecurityConstants;
import lombok.extern.slf4j.Slf4j;

View File

@ -22,9 +22,9 @@ import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationProvider;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken;
import com.youlai.auth.handler.MyAuthenticationFailureHandler;
import com.youlai.auth.handler.MyAuthenticationSuccessHandler;
import com.youlai.auth.userdetails.member.MemberDetailsService;
import com.youlai.auth.userdetails.user.SysUserDetails;
import com.youlai.auth.userdetails.user.jackson.SysUserMixin;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.auth.model.SysUserDetails;
import com.youlai.auth.jackson.SysUserMixin;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
@ -245,8 +245,6 @@ public class AuthorizationServerConfig {
/**
* 初始化创建商城管理客户端
*
* @param registeredClientRepository
*/
private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) {
@ -254,7 +252,6 @@ public class AuthorizationServerConfig {
String clientSecret = "123456";
String clientName = "商城管理客户端";
/*
如果使用明文客户端认证时会自动升级加密方式换句话说直接修改客户端密码所以直接使用 bcrypt 加密避免不必要的麻烦
官方ISSUE https://github.com/spring-projects/spring-authorization-server/issues/1099
@ -286,8 +283,6 @@ public class AuthorizationServerConfig {
/**
* 初始化创建商城APP客户端
*
* @param registeredClientRepository
*/
private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) {

View File

@ -1,13 +1,14 @@
package com.youlai.auth.config;
import com.youlai.auth.userdetails.member.MemberDetails;
import com.youlai.auth.userdetails.user.SysUserDetails;
import com.youlai.auth.model.MemberDetails;
import com.youlai.auth.model.SysUserDetails;
import com.youlai.common.constant.SecurityConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
@ -20,7 +21,7 @@ import java.util.Set;
import java.util.stream.Collectors;
/**
* JWT 自定义字段
* JWT 自定义字段配置
*
* @author haoxr
* @see <a href="https://github.com/spring-projects/spring-authorization-server/pull/1264">How-to: Authorize an access token containing custom authorities</a>
@ -32,6 +33,9 @@ public class JwtTokenClaimsConfig {
private final RedisTemplate redisTemplate;
/**
* JWT 自定义字段
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return context -> {
@ -42,7 +46,14 @@ public class JwtTokenClaimsConfig {
if (principal instanceof SysUserDetails userDetails) { // 系统用户添加自定义字段
Long userId = userDetails.getUserId();
claims.claim("user_id", userId);
claims.claim("userId", userDetails.getUserId());
claims.claim("username", userDetails.getUsername());
claims.claim("deptId", userDetails.getDeptId());
claims.claim("dataScope", userDetails.getDataScope());
Set<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
claims.claim("authorities", roles);
// 这里存入角色至JWT解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter
var authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
@ -52,7 +63,7 @@ public class JwtTokenClaimsConfig {
// 权限数据比较多缓存至redis
Set<String> perms = userDetails.getPerms();
redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userId, perms);
redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_KEY_PREFIX + userId, perms);
} else if (principal instanceof MemberDetails userDetails) { // 商城会员添加自定义字段
claims.claim("member_id", String.valueOf(userDetails.getId()));

View File

@ -1,15 +0,0 @@
package com.youlai.auth.controller;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证控制器
*
* @author haoxr
* @since 2023/6/29
*/
@RestController
public class AuthController {
}

View File

@ -1,5 +1,6 @@
package com.youlai.auth.handler;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -33,24 +34,23 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand
* MappingJackson2HttpMessageConverter Spring 框架提供的一个 HTTP 消息转换器用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
*/
private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
private final Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
/**
* 自定义认证成功响应数据结构
*
* @param request the request which caused the successful authentication
* @param response the response
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
* @throws IOException
* @throws ServletException
* the authentication process.
* @throws IOException if an I/O related error occurs during the response
* @throws ServletException if a servlet-related error occurs during the response
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
@ -69,11 +69,17 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter
.convert(accessTokenResponse);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);
String clientId = accessTokenAuthentication.getRegisteredClient().getClientId();
if (SecurityConstants.KNIFE4J_TEST_CLIENT_ID.equals(clientId)) {
// Knife4j测试客户端IDKnife4j自动填充的 access_token 须原生返回不能被包装成业务码数据格式
this.accessTokenHttpResponseConverter.write(tokenResponseParameters, null, httpResponse);
} else {
this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);
}
}
}

View File

@ -1,14 +1,13 @@
package com.youlai.auth.userdetails.user.jackson;
package com.youlai.auth.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.youlai.auth.userdetails.user.SysUserDetails;
import com.youlai.auth.model.SysUserDetails;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

View File

@ -1,4 +1,4 @@
package com.youlai.auth.userdetails.user.jackson;
package com.youlai.auth.jackson;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

View File

@ -1,4 +1,4 @@
package com.youlai.auth.userdetails.member;
package com.youlai.auth.model;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.mall.ums.dto.MemberAuthDTO;

Some files were not shown because too many files have changed in this diff Show More