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;
+}