diff --git a/src/main/java/com/aida/seller/common/annotation/InternalOnly.java b/src/main/java/com/aida/seller/common/annotation/InternalOnly.java new file mode 100644 index 0000000..d4e7a37 --- /dev/null +++ b/src/main/java/com/aida/seller/common/annotation/InternalOnly.java @@ -0,0 +1,17 @@ +package com.aida.seller.common.annotation; + +import java.lang.annotation.*; + +/** + * 标记接口仅允许内部服务调用(Feign 远程调用)。 + *

+ * 被此注解标记的 Controller 方法会通过 AOP 拦截, + * 仅放行携带了正确内部调用 Header 的请求,外部 HTTP 请求将被拒绝。 + * + * @see com.aida.seller.common.aop.InternalOnlyAspect + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InternalOnly { +} diff --git a/src/main/java/com/aida/seller/common/aop/InternalOnlyAspect.java b/src/main/java/com/aida/seller/common/aop/InternalOnlyAspect.java new file mode 100644 index 0000000..01e8054 --- /dev/null +++ b/src/main/java/com/aida/seller/common/aop/InternalOnlyAspect.java @@ -0,0 +1,46 @@ +package com.aida.seller.common.aop; + +import com.aida.seller.common.annotation.InternalOnly; +import com.aida.seller.common.constants.CommonConstants; +import com.aida.seller.common.exception.BusinessException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * AOP 切面:校验 {@link InternalOnly} 标记的方法是否来自内部服务调用。 + *

+ * 内部调用(Feign)会携带 {@link CommonConstants#INTERNAL_CALL_HEADER} Header, + * 外部直接 HTTP 请求则不携带,视为非法访问。 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class InternalOnlyAspect { + + @Around("@annotation(com.aida.seller.common.annotation.InternalOnly)") + public Object validateInternalCall(ProceedingJoinPoint joinPoint) throws Throwable { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new BusinessException("禁止外部直接访问此接口"); + } + + HttpServletRequest request = attributes.getRequest(); + String internalCall = request.getHeader(CommonConstants.INTERNAL_CALL_HEADER); + if (!CommonConstants.INTERNAL_CALL_VALUE.equals(internalCall)) { + log.warn("Unauthorized external access attempt to internal-only endpoint: {}", + ((MethodSignature) joinPoint.getSignature()).getMethod().getName()); + throw new BusinessException("禁止外部直接访问此接口"); + } + + return joinPoint.proceed(); + } +} diff --git a/src/main/java/com/aida/seller/common/constants/CommonConstants.java b/src/main/java/com/aida/seller/common/constants/CommonConstants.java index d35c591..2334a42 100644 --- a/src/main/java/com/aida/seller/common/constants/CommonConstants.java +++ b/src/main/java/com/aida/seller/common/constants/CommonConstants.java @@ -6,9 +6,9 @@ public class CommonConstants { public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期(Hour) - - - - - + /** + * 内部服务间调用的签名 Header,Feign 远程调用时携带,用于标识为内部可信调用 + */ + public static final String INTERNAL_CALL_HEADER = "X-Internal-Call"; + public static final String INTERNAL_CALL_VALUE = "true"; } \ No newline at end of file diff --git a/src/main/java/com/aida/seller/module/listing/controller/ListingController.java b/src/main/java/com/aida/seller/module/listing/controller/ListingController.java index a39d461..a5d4df9 100644 --- a/src/main/java/com/aida/seller/module/listing/controller/ListingController.java +++ b/src/main/java/com/aida/seller/module/listing/controller/ListingController.java @@ -84,7 +84,7 @@ public class ListingController { return Response.success(needPopup ? 1 : 0); } - @Operation(summary = "获取店铺商品列表", description = "按 status=1、deleted=0、designFor 筛选,返回店铺已发布商品分页列表") + @Operation(summary = "获取店铺商品列表", description = "按返回店铺已发布商品分页列表") @GetMapping("/shop/seller") public Response> getShopListings( @Parameter(description = "设计师用户ID") @RequestParam Long sellerId, diff --git a/src/main/java/com/aida/seller/module/listing/controller/ListingMallController.java b/src/main/java/com/aida/seller/module/listing/controller/ListingMallController.java index d372ca8..d2ea091 100644 --- a/src/main/java/com/aida/seller/module/listing/controller/ListingMallController.java +++ b/src/main/java/com/aida/seller/module/listing/controller/ListingMallController.java @@ -24,7 +24,7 @@ public class ListingMallController { private final ListingMallService listingMallService; - @Operation(summary = "商城首页商品分页列表", description = "面向所有卖家,支持分类筛选、多字段排序、分页") + @Operation(summary = "商城首页商品分页列表", description = "") @PostMapping("/mall") public PageResponse getMallListings( @RequestBody ListingMallQueryDTO dto) { diff --git a/src/main/java/com/aida/seller/module/order/controller/OrderController.java b/src/main/java/com/aida/seller/module/order/controller/OrderController.java index 124e461..7497312 100644 --- a/src/main/java/com/aida/seller/module/order/controller/OrderController.java +++ b/src/main/java/com/aida/seller/module/order/controller/OrderController.java @@ -1,25 +1,30 @@ package com.aida.seller.module.order.controller; +import com.aida.seller.common.annotation.InternalOnly; import com.aida.seller.common.context.UserContext; import com.aida.seller.common.result.PageResponse; import com.aida.seller.common.result.Response; +import com.aida.seller.module.order.dto.CreateOrderDTO; import com.aida.seller.module.order.dto.OrderListDTO; import com.aida.seller.module.order.service.OrderService; import com.aida.seller.module.order.vo.BuyerOrderVO; +import com.aida.seller.module.order.vo.CreateOrderResultVO; import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderVO; import com.baomidou.mybatisplus.core.metadata.IPage; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; + +import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - /** * My Orders - 订单管理控制器 */ diff --git a/src/main/java/com/aida/seller/module/order/dto/CreateOrderDTO.java b/src/main/java/com/aida/seller/module/order/dto/CreateOrderDTO.java new file mode 100644 index 0000000..6d20143 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/dto/CreateOrderDTO.java @@ -0,0 +1,26 @@ +package com.aida.seller.module.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 创建订单请求参数 + */ +@Data +@Schema(description = "创建订单请求参数") +public class CreateOrderDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "商品ID列表") + private List listingIds; + + @Schema(description = "买家ID") + private Long buyerId; + + @Schema(description = "买家账号") + private String buyerUsername; +} diff --git a/src/main/java/com/aida/seller/module/order/dto/UpdateOrderStatusDTO.java b/src/main/java/com/aida/seller/module/order/dto/UpdateOrderStatusDTO.java new file mode 100644 index 0000000..dc2522a --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/dto/UpdateOrderStatusDTO.java @@ -0,0 +1,23 @@ +package com.aida.seller.module.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 批量修改订单状态请求参数 + */ +@Data +@Schema(description = "批量修改订单状态请求参数") +public class UpdateOrderStatusDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "订单ID列表") + private List orderIds; + + @Schema(description = "目标状态:0-未支付,1-已支付,2-已取消") + private Integer status; +} diff --git a/src/main/java/com/aida/seller/module/order/service/OrderService.java b/src/main/java/com/aida/seller/module/order/service/OrderService.java index 666a1b0..1ddbfa8 100644 --- a/src/main/java/com/aida/seller/module/order/service/OrderService.java +++ b/src/main/java/com/aida/seller/module/order/service/OrderService.java @@ -1,7 +1,10 @@ package com.aida.seller.module.order.service; +import com.aida.seller.module.order.dto.CreateOrderDTO; import com.aida.seller.module.order.dto.OrderListDTO; +import com.aida.seller.module.order.dto.UpdateOrderStatusDTO; import com.aida.seller.module.order.vo.BuyerOrderVO; +import com.aida.seller.module.order.vo.CreateOrderResultVO; import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderVO; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -13,6 +16,21 @@ import java.util.List; */ public interface OrderService { + /** + * 创建订单(按卖家分组合并) + * + * @param dto 创建订单参数(包含商品ID列表、买家ID、买家账号) + * @return 包含订单ID列表和总金额的结果对象 + */ + CreateOrderResultVO createOrder(CreateOrderDTO dto); + + /** + * 批量修改订单状态 + * + * @param dto 包含订单ID列表和目标状态 + */ + void updateOrderStatus(UpdateOrderStatusDTO dto); + /** * 获取卖家订单数据总览 * @@ -31,10 +49,13 @@ public interface OrderService { IPage getOrderPage(OrderListDTO dto, Long sellerId); /** - * 根据买家ID查询订单列表(供远程调用) + * 根据买家ID分页查询订单列表(供远程调用) * * @param buyerId 买家ID - * @return 买家订单列表 + * @param page 页码 + * @param size 每页数量 + * @param status 订单状态筛选(可选) + * @return 买家订单分页列表,按更新时间降序排列 */ - List getOrdersByBuyerId(Long buyerId); + IPage getOrdersByBuyerId(Long buyerId, long page, long size, Integer status); } diff --git a/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java b/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java index fe093bf..21fb3e5 100644 --- a/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java +++ b/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java @@ -1,22 +1,33 @@ package com.aida.seller.module.order.service; import com.aida.seller.common.constants.CommonConstants; +import com.aida.seller.common.exception.BusinessException; +import com.aida.seller.module.designer.entity.DesignerEntity; +import com.aida.seller.module.designer.mapper.DesignerMapper; +import com.aida.seller.module.listing.entity.ListingEntity; +import com.aida.seller.module.listing.mapper.ListingMapper; +import com.aida.seller.module.order.dto.CreateOrderDTO; import com.aida.seller.module.order.dto.OrderListDTO; +import com.aida.seller.module.order.dto.UpdateOrderStatusDTO; import com.aida.seller.module.order.entity.OrderInfoEntity; import com.aida.seller.module.order.entity.OrderItemEntity; import com.aida.seller.module.order.mapper.OrderInfoMapper; import com.aida.seller.module.order.mapper.OrderItemMapper; import com.aida.seller.module.order.vo.BuyerOrderItemVO; import com.aida.seller.module.order.vo.BuyerOrderVO; +import com.aida.seller.module.order.vo.CreateOrderResultVO; import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderVO; import com.aida.seller.util.MinioUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.math.BigDecimal; @@ -35,6 +46,8 @@ public class OrderServiceImpl extends ServiceImpl getOrdersByBuyerId(Long buyerId) { + public IPage getOrdersByBuyerId(Long buyerId, long page, long size, Integer status) { + Page pageParam = new Page<>(page, size); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(OrderInfoEntity::getBuyerId, buyerId); + if (status != null) { + wrapper.eq(OrderInfoEntity::getStatus, status); + } wrapper.orderByDesc(OrderInfoEntity::getUpdateTime); - List orders = this.list(wrapper); - if (orders.isEmpty()) { - return Collections.emptyList(); + Page orderPage = this.page(pageParam, wrapper); + if (orderPage.getRecords().isEmpty()) { + return new Page<>(page, size, 0); } - List orderIds = orders.stream() + List orderIds = orderPage.getRecords().stream() .map(OrderInfoEntity::getId) .collect(Collectors.toList()); @@ -147,7 +165,7 @@ public class OrderServiceImpl extends ServiceImpl { + List voList = orderPage.getRecords().stream().map(order -> { BuyerOrderVO vo = new BuyerOrderVO(); vo.setOrderId(order.getId()); vo.setUpdateTime(order.getUpdateTime()); @@ -169,5 +187,114 @@ public class OrderServiceImpl extends ServiceImpl resultPage = new Page<>(orderPage.getCurrent(), orderPage.getSize(), orderPage.getTotal()); + resultPage.setRecords(voList); + return resultPage; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public CreateOrderResultVO createOrder(CreateOrderDTO dto) { + if (CollectionUtils.isEmpty(dto.getListingIds())) { + throw new BusinessException("商品ID列表不能为空"); + } + if (dto.getBuyerId() == null) { + throw new BusinessException("买家ID不能为空"); + } + if (!StringUtils.hasText(dto.getBuyerUsername())) { + throw new BusinessException("买家账号不能为空"); + } + + List listings = listingMapper.selectBatchIds(dto.getListingIds()); + if (CollectionUtils.isEmpty(listings)) { + throw new BusinessException("未找到对应的商品"); + } + + Map> listingsBySeller = listings.stream() + .collect(Collectors.groupingBy(ListingEntity::getSellerId)); + + List orderIds = new ArrayList<>(); + List totalPrices = new ArrayList<>(); + + for (Map.Entry> entry : listingsBySeller.entrySet()) { + Long sellerId = entry.getKey(); + List sellerListings = entry.getValue(); + + DesignerEntity designer = designerMapper.selectOne( + new LambdaQueryWrapper() + .eq(DesignerEntity::getUserId, sellerId) + .eq(DesignerEntity::getDeleted, 0)); + String shopName = (designer != null) ? designer.getShopName() : null; + + BigDecimal totalPrice = sellerListings.stream() + .map(ListingEntity::getPrice) + .filter(p -> p != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + totalPrices.add(totalPrice); + + Long totalViews = sellerListings.stream() + .map(ListingEntity::getViewCount) + .filter(v -> v != null) + .reduce(0, Integer::sum).longValue(); + + for (ListingEntity listing : sellerListings) { + listing.setSalesVolume(listing.getSalesVolume() == null ? 1 : listing.getSalesVolume() + 1); + listingMapper.updateById(listing); + } + + OrderInfoEntity order = new OrderInfoEntity(); + order.setSellerId(sellerId); + order.setBuyerId(dto.getBuyerId()); + order.setBuyerUsername(dto.getBuyerUsername()); + order.setStatus(0); + order.setShopName(shopName); + order.setTotalPrice(totalPrice); + order.setTotalItems(sellerListings.size()); + order.setTotalViews(totalViews); + this.save(order); + + for (ListingEntity listing : sellerListings) { + OrderItemEntity item = new OrderItemEntity(); + item.setOrderId(order.getId()); + item.setSellerId(sellerId); + item.setBuyerId(dto.getBuyerId()); + item.setListingId(listing.getId()); + item.setListingName(listing.getTitle()); + item.setThumbnailUrl(listing.getCover()); + item.setPrice(listing.getPrice()); + item.setProductCategory(listing.getProductCategory()); + orderItemMapper.insert(item); + } + + orderIds.add(order.getId()); + } + + CreateOrderResultVO result = new CreateOrderResultVO(); + result.setOrderIds(orderIds); + BigDecimal grandTotal = totalPrices.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + result.setTotalAmount(grandTotal); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateOrderStatus(UpdateOrderStatusDTO dto) { + if (dto == null || dto.getOrderIds() == null || dto.getOrderIds().isEmpty()) { + throw new BusinessException("订单ID列表不能为空"); + } + if (dto.getStatus() == null) { + throw new BusinessException("订单状态不能为空"); + } + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.in(OrderInfoEntity::getId, dto.getOrderIds()) + .set(OrderInfoEntity::getStatus, dto.getStatus()); + boolean updated = this.update(updateWrapper); + if (!updated) { + throw new BusinessException("订单不存在或无权修改"); + } } } diff --git a/src/main/java/com/aida/seller/module/order/vo/CreateOrderResultVO.java b/src/main/java/com/aida/seller/module/order/vo/CreateOrderResultVO.java new file mode 100644 index 0000000..980b4df --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/vo/CreateOrderResultVO.java @@ -0,0 +1,21 @@ +package com.aida.seller.module.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Data +@Schema(description = "创建订单结果") +public class CreateOrderResultVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "订单ID列表(按卖家分组)") + private List orderIds; + + @Schema(description = "所有订单总金额(HK$)") + private BigDecimal totalAmount; +}