diff --git a/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java b/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java index e8a3ff7..4a9dea3 100644 --- a/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java +++ b/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java @@ -108,7 +108,7 @@ public class DesignerController { return Response.success(result); } - @Operation(summary = "获取设计师店铺详情", description = "根据 sellerId(即 卖家userId)获取店铺公开信息,供买家端店铺主页调用") + @Operation(summary = "获取商城店铺详情", description = "根据 sellerId(即 卖家userId)获取店铺公开信息,供买家端店铺主页调用") @GetMapping("/shop/{sellerId}") public Response getShopDetail( @Parameter(description = "设计师用户ID") @PathVariable Long sellerId) { diff --git a/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java b/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java index 5e097b1..a32f8c6 100644 --- a/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java +++ b/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java @@ -353,14 +353,14 @@ public class DesignerServiceImpl extends ServiceImpl { List covers = listingsByDesigner - .getOrDefault(d.getId(), List.of()) + .getOrDefault(d.getUserId(), List.of()) .stream() .filter(l -> l.getCover() != null && !l.getCover().isBlank()) .limit(5) .map(l -> minioUtil.processMinioResource(l.getCover(), CommonConstants.MINIO_PATH_TIMEOUT)) .collect(Collectors.toList()); - long listingTotal = listingCountMap.getOrDefault(d.getId(), 0L); + long listingTotal = listingCountMap.getOrDefault(d.getUserId(), 0L); return buildSearchVO(d, covers, listingTotal); }).collect(Collectors.toList()); } 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 f23c2c7..a39d461 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 @@ -40,7 +40,7 @@ public class ListingController { public Response getById( @Parameter(description = "商品ID") @RequestParam Long id) { Long sellerId = UserContext.getUserId(); - ListingSaveDTO result = listingService.getById(id, sellerId); + ListingSaveDTO result = listingService.getById(id); return Response.success(result); } @@ -85,7 +85,7 @@ public class ListingController { } @Operation(summary = "获取店铺商品列表", description = "按 status=1、deleted=0、designFor 筛选,返回店铺已发布商品分页列表") - @GetMapping("/shop") + @GetMapping("/shop/seller") public Response> getShopListings( @Parameter(description = "设计师用户ID") @RequestParam Long sellerId, @Parameter(description = "适用性别 female/male/all") @RequestParam String designFor, 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 new file mode 100644 index 0000000..d372ca8 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/controller/ListingMallController.java @@ -0,0 +1,42 @@ +package com.aida.seller.module.listing.controller; + +import com.aida.seller.common.result.PageResponse; +import com.aida.seller.common.result.Response; +import com.aida.seller.module.listing.dto.ListingMallQueryDTO; +import com.aida.seller.module.listing.service.ListingMallService; +import com.aida.seller.module.listing.vo.ListingDetailVO; +import com.aida.seller.module.listing.vo.ListingMallVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +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.*; + +/** + * 商城首页商品 Controller(Feign 端) + */ +@Tag(name = "ListingMall - 商城首页商品") +@RestController +@RequestMapping("/listing") +@RequiredArgsConstructor +public class ListingMallController { + + private final ListingMallService listingMallService; + + @Operation(summary = "商城首页商品分页列表", description = "面向所有卖家,支持分类筛选、多字段排序、分页") + @PostMapping("/mall") + public PageResponse getMallListings( + @RequestBody ListingMallQueryDTO dto) { + IPage page = listingMallService.getMallListings(dto); + return PageResponse.success(page); + } + + @Operation(summary = "商品详情(商城详情页)", description = "关联图片与店铺信息") + @GetMapping("/mall/detail") + public Response getListingDetail( + @Parameter(description = "商品ID") @RequestParam Long id) { + ListingDetailVO detail = listingMallService.getListingDetail(id); + return Response.success(detail); + } +} diff --git a/src/main/java/com/aida/seller/module/listing/dto/ListingMallQueryDTO.java b/src/main/java/com/aida/seller/module/listing/dto/ListingMallQueryDTO.java new file mode 100644 index 0000000..8075372 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/dto/ListingMallQueryDTO.java @@ -0,0 +1,34 @@ +package com.aida.seller.module.listing.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 商城首页商品查询 DTO + */ +@Data +public class ListingMallQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "适用性别 female/male/all,不传表示全部") + private String designFor; + + @Schema(description = "商品分类列表,支持多选,如 outwear&categories=dress") + private List categories; + + @Schema(description = "排序字段:price/salesVolume/updateTime/viewCount/createTime,默认 updateTime") + private String sortField = "updateTime"; + + @Schema(description = "排序方向:asc/desc,默认 desc") + private String sortOrder = "desc"; + + @Schema(description = "页码,默认1") + private Integer pageNum = 1; + + @Schema(description = "每页数量,默认10") + private Integer pageSize = 10; +} diff --git a/src/main/java/com/aida/seller/module/listing/mapper/ListingMallMapper.java b/src/main/java/com/aida/seller/module/listing/mapper/ListingMallMapper.java new file mode 100644 index 0000000..bc89ea9 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/mapper/ListingMallMapper.java @@ -0,0 +1,12 @@ +package com.aida.seller.module.listing.mapper; + +import com.aida.seller.module.listing.entity.ListingEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 商城首页商品 Mapper + */ +@Mapper +public interface ListingMallMapper extends BaseMapper { +} diff --git a/src/main/java/com/aida/seller/module/listing/service/ListingMallService.java b/src/main/java/com/aida/seller/module/listing/service/ListingMallService.java new file mode 100644 index 0000000..104bff0 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/service/ListingMallService.java @@ -0,0 +1,28 @@ +package com.aida.seller.module.listing.service; + +import com.aida.seller.module.listing.dto.ListingMallQueryDTO; +import com.aida.seller.module.listing.vo.ListingDetailVO; +import com.aida.seller.module.listing.vo.ListingMallVO; +import com.baomidou.mybatisplus.core.metadata.IPage; + +/** + * 商城首页商品 Service 接口 + */ +public interface ListingMallService { + + /** + * 商城首页商品分页列表 + * + * @param dto 查询条件 + * @return 分页结果 + */ + IPage getMallListings(ListingMallQueryDTO dto); + + /** + * 获取商品详情(商城详情页) + * + * @param id 商品ID + * @return 商品详情 + */ + ListingDetailVO getListingDetail(Long id); +} diff --git a/src/main/java/com/aida/seller/module/listing/service/ListingService.java b/src/main/java/com/aida/seller/module/listing/service/ListingService.java index a0dc10a..b8903c9 100644 --- a/src/main/java/com/aida/seller/module/listing/service/ListingService.java +++ b/src/main/java/com/aida/seller/module/listing/service/ListingService.java @@ -34,10 +34,9 @@ public interface ListingService extends IService { * 获取商品详情(含所有图片) * * @param id 商品ID - * @param sellerId 卖家ID * @return 商品详情 */ - ListingSaveDTO getById(Long id, Long sellerId); + ListingSaveDTO getById(Long id); /** * 分页查询商品列表 diff --git a/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java b/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java index 672bcec..c812c4d 100644 --- a/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java +++ b/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java @@ -110,11 +110,10 @@ public class ListingServiceImpl extends ServiceImpl() .eq(ListingEntity::getId, id) - .eq(ListingEntity::getSellerId, sellerId) .eq(ListingEntity::getDeleted, 0)); if (entity == null) { throw new BusinessException("商品不存在"); @@ -318,14 +317,10 @@ public class ListingServiceImpl extends ServiceImpl page = this.page(pageParam, queryWrapper); Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); - boolean loggedIn = UserContext.getBuyerId() != null; result.setRecords(page.getRecords().stream().map(entity -> { ListingPageVO vo = new ListingPageVO(); BeanUtils.copyProperties(entity, vo); vo.setCover(minioUtil.processMinioResource(vo.getCover(), CommonConstants.MINIO_PATH_TIMEOUT)); - if (!loggedIn) { - vo.setPrice(null); - } return vo; }).collect(Collectors.toList())); return result; diff --git a/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java b/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java new file mode 100644 index 0000000..4ecc902 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java @@ -0,0 +1,167 @@ +package com.aida.seller.module.listing.service.impl; + +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.dto.ListingMallQueryDTO; +import com.aida.seller.module.listing.entity.ListingEntity; +import com.aida.seller.module.listing.entity.ListingImageEntity; +import com.aida.seller.module.listing.enums.DesignForEnum; +import com.aida.seller.module.listing.mapper.ListingImageMapper; +import com.aida.seller.module.listing.mapper.ListingMallMapper; +import com.aida.seller.module.listing.vo.ListingDetailVO; +import com.aida.seller.module.listing.vo.ListingMallVO; +import com.aida.seller.util.MinioUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 商城首页商品 Service 实现 + */ +@Service +public class ListingMallServiceImpl extends ServiceImpl implements com.aida.seller.module.listing.service.ListingMallService { + + private final MinioUtil minioUtil; + private final ListingImageMapper listingImageMapper; + private final DesignerMapper designerMapper; + + public ListingMallServiceImpl(MinioUtil minioUtil, ListingImageMapper listingImageMapper, DesignerMapper designerMapper) { + this.minioUtil = minioUtil; + this.listingImageMapper = listingImageMapper; + this.designerMapper = designerMapper; + } + + @Override + public IPage getMallListings(ListingMallQueryDTO dto) { + Page pageParam = new Page<>(dto.getPageNum(), dto.getPageSize()); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ListingEntity::getStatus, 1); + queryWrapper.eq(ListingEntity::getDeleted, 0); + + if (dto.getDesignFor() != null && !dto.getDesignFor().isEmpty()) { + DesignForEnum designForEnum = DesignForEnum.of(dto.getDesignFor()); + if (designForEnum != null && designForEnum != DesignForEnum.ALL) { + queryWrapper.eq(ListingEntity::getDesignFor, designForEnum.getCode()); + } + } + + if (!CollectionUtils.isEmpty(dto.getCategories())) { + for (String cat : dto.getCategories()) { + queryWrapper.apply( + "JSON_CONTAINS(product_category, {0}, '$')", + "\"" + cat + "\"" + ); + } + } + + applySorting(queryWrapper, dto.getSortField(), dto.getSortOrder()); + + IPage page = this.page(pageParam, queryWrapper); + + Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + result.setRecords(page.getRecords().stream().map(entity -> { + ListingMallVO vo = new ListingMallVO(); + vo.setId(entity.getId()); + vo.setCover(minioUtil.processMinioResource(entity.getCover(), CommonConstants.MINIO_PATH_TIMEOUT)); + vo.setTitle(entity.getTitle()); + vo.setPrice(entity.getPrice()); + return vo; + }).toList()); + return result; + } + + private void applySorting(LambdaQueryWrapper queryWrapper, String sortField, String sortOrder) { + if (!StringUtils.hasText(sortField)) { + queryWrapper.orderByDesc(ListingEntity::getUpdateTime); + return; + } + boolean isAsc = "asc".equalsIgnoreCase(sortOrder); + + switch (sortField.toLowerCase()) { + case "price" -> { + if (isAsc) queryWrapper.orderByAsc(ListingEntity::getPrice); + else queryWrapper.orderByDesc(ListingEntity::getPrice); + } + case "salesvolume" -> { + if (isAsc) queryWrapper.orderByAsc(ListingEntity::getSalesVolume); + else queryWrapper.orderByDesc(ListingEntity::getSalesVolume); + } + case "viewcount" -> { + if (isAsc) queryWrapper.orderByAsc(ListingEntity::getViewCount); + else queryWrapper.orderByDesc(ListingEntity::getViewCount); + } + case "createtime" -> { + if (isAsc) queryWrapper.orderByAsc(ListingEntity::getCreateTime); + else queryWrapper.orderByDesc(ListingEntity::getCreateTime); + } + default -> queryWrapper.orderByDesc(ListingEntity::getUpdateTime); + } + } + + @Override + public ListingDetailVO getListingDetail(Long id) { + ListingEntity entity = this.getOne( + new LambdaQueryWrapper() + .eq(ListingEntity::getId, id) + .eq(ListingEntity::getStatus, 1) + .eq(ListingEntity::getDeleted, 0)); + if (entity == null) { + throw new BusinessException("商品不存在"); + } + + List images = listingImageMapper.selectList( + new LambdaQueryWrapper() + .eq(ListingImageEntity::getListingId, id) + .orderByAsc(ListingImageEntity::getSortOrder)); + + Map> imageMap = images.stream() + .filter(img -> StringUtils.hasText(img.getCategory())) + // 先按 category 分组,再按 sortOrder 组内排序,确保同组图片按 sortOrder 升序排列 + .sorted(Comparator.comparing(ListingImageEntity::getCategory) + .thenComparing(ListingImageEntity::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder()))) + .collect(Collectors.groupingBy( + ListingImageEntity::getCategory, + Collectors.mapping( + img -> minioUtil.processMinioResource(img.getImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT), + Collectors.toList() + ) + )); + + List mainProductUrls = imageMap.get("main_product"); + if (mainProductUrls != null && !mainProductUrls.isEmpty()) { + List productUrls = imageMap.get("product"); + if (productUrls != null) { + imageMap.put("product", productUrls.stream() + .filter(url -> !mainProductUrls.contains(url)) + .toList()); + } + } + + DesignerEntity designer = designerMapper.selectOne( + new LambdaQueryWrapper() + .eq(DesignerEntity::getUserId, entity.getSellerId()) + .eq(DesignerEntity::getDeleted, 0)); + + ListingDetailVO vo = new ListingDetailVO(); + vo.setId(entity.getId()); + vo.setTitle(entity.getTitle()); + vo.setDescription(entity.getDescription()); + vo.setPrice(entity.getPrice()); + vo.setUpdateTime(entity.getUpdateTime()); + vo.setShopName(designer != null ? designer.getShopName() : null); + vo.setImages(imageMap); + return vo; + } +} diff --git a/src/main/java/com/aida/seller/module/listing/vo/ListingDetailVO.java b/src/main/java/com/aida/seller/module/listing/vo/ListingDetailVO.java new file mode 100644 index 0000000..d72cfa0 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/vo/ListingDetailVO.java @@ -0,0 +1,44 @@ +package com.aida.seller.module.listing.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 商品详情 VO(商城详情页) + */ +@Data +@Schema(description = "商品详情") +public class ListingDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "商品ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + @Schema(description = "商品标题") + private String title; + + @Schema(description = "商品描述") + private String description; + + @Schema(description = "价格") + private BigDecimal price; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "店铺名称") + private String shopName; + + @Schema(description = "图片列表,key 为 category,value 为该分类下所有图片 URL") + private Map> images; +} diff --git a/src/main/java/com/aida/seller/module/listing/vo/ListingMallVO.java b/src/main/java/com/aida/seller/module/listing/vo/ListingMallVO.java new file mode 100644 index 0000000..8e55153 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/vo/ListingMallVO.java @@ -0,0 +1,25 @@ +package com.aida.seller.module.listing.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 商城首页商品 VO + */ +@Data +public class ListingMallVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String cover; + + private String title; + + private BigDecimal price; +} diff --git a/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java b/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java index 88e7116..e6ea3ab 100644 --- a/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java +++ b/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java @@ -25,6 +25,12 @@ public class OrderInfoEntity implements Serializable { /** 卖家ID */ private Long sellerId; + /** 买家ID */ + private Long buyerId; + + /** 订单状态:0-未支付,1-已支付,2-已取消 */ + private Integer status; + /** 订单总金额(HK$) */ private BigDecimal totalPrice; diff --git a/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java b/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java index 7982ca4..b6ebdb2 100644 --- a/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java +++ b/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java @@ -27,12 +27,18 @@ public class OrderItemEntity implements Serializable { @JsonSerialize(using = ToStringSerializer.class) private Long orderId; + /** 卖家ID */ + private Long sellerId; + + /** 买家ID */ + private Long buyerId; + /** 商品ID */ @JsonSerialize(using = ToStringSerializer.class) private Long listingId; /** 商品名称 */ - private String productName; + private String listingName; /** 商品缩略图URL */ private String thumbnailUrl; 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 b2b7266..1f8c2ae 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 @@ -82,6 +82,7 @@ public class OrderServiceImpl extends ServiceImpl page = this.page(pageParam, queryWrapper); @@ -109,7 +110,7 @@ public class OrderServiceImpl extends ServiceImpl itemVOs = items.stream().map(item -> { OrderVO.ItemVO itemVO = new OrderVO.ItemVO(); itemVO.setProductId(item.getListingId()); - itemVO.setProductName(item.getProductName()); + itemVO.setProductName(item.getListingName()); itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT)); return itemVO; }).collect(Collectors.toList()); diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index b4f6beb..f0d0c2d 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -62,6 +62,8 @@ CREATE TABLE seller_designer ( CREATE TABLE seller_orders ( id BIGINT PRIMARY KEY COMMENT '主键ID', seller_id BIGINT NOT NULL COMMENT '卖家ID', + buyer_id BIGINT NOT NULL COMMENT '买家ID', + status INT DEFAULT 0 COMMENT '订单状态: 0-未支付, 1-已支付, 2-已取消', total_price DECIMAL(10,2) COMMENT '订单总金额(HK$)', buyer_username VARCHAR(100) COMMENT '买家账号', total_items INT COMMENT '商品总数量', @@ -77,9 +79,11 @@ CREATE TABLE seller_orders ( -- 订单商品明细表 CREATE TABLE seller_order_item ( id BIGINT PRIMARY KEY COMMENT '主键ID', - order_id VARCHAR(50) NOT NULL COMMENT '订单ID', + order_id BIGINT NOT NULL COMMENT '订单ID', + seller_id BIGINT NOT NULL COMMENT '卖家ID', + buyer_id BIGINT NOT NULL COMMENT '买家ID', listing_id BIGINT NOT NULL COMMENT '商品ID', - product_name VARCHAR(255) COMMENT '商品名称', + listing_name VARCHAR(255) COMMENT '商品名称', thumbnail_url VARCHAR(200) COMMENT '商品缩略图URL', price DECIMAL(10,2) COMMENT '成交单价(HK$)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',