Commit cd858559 by Lizh

优化pageList查询接口

parent 315f36e6
......@@ -3,9 +3,7 @@ package com.jomalls.custom.app.dto;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.jomalls.custom.page.PageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
......@@ -140,6 +138,8 @@ public class CustomProductInfoSnakeDTO extends PageRequest {
* <p>
* 兼容前端发送 Boolean(true→1, false→0)和 Number(0/1/2)。
*/
@Min(value = 0, message = "processing 不能小于0")
@Max(value = 2, message = "processing 不能大于2")
@Schema(description = "是否九猫处理(0=否 1=是 2=未设置,也支持 true/false)")
private Integer processing;
......
package com.jomalls.custom.app.enums;
import lombok.Getter;
/**
* @Author: Lizh
* @Date: 2026/6/11 16:47
* @Description:
* @Version: 1.0
*/
@Getter
public enum ProcessingStatus {
/** 是否九猫处理(0=否 1=是 2=未设置,也支持 true/false) */
NO(0),
YES(1),
NOT_SET(2); // 2表示IS NULL
private final int code;
ProcessingStatus(int code) {
this.code = code;
}
}
......@@ -103,8 +103,6 @@ public interface CustomProductInfoService {
*/
List<CraftCenterVO> getCraftById(Integer id);
// ==================== ERP 专用接口(对齐 TS) ====================
/**
* ERP 分页查询(对齐 TS erpPage)
* <p>
......
package com.jomalls.custom.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.jomalls.custom.app.dto.*;
import com.jomalls.custom.app.enums.ProcessingStatus;
import com.jomalls.custom.app.enums.SkuGenerateEnums;
import com.jomalls.custom.app.enums.TemplateStatus;
import com.jomalls.custom.app.exception.ServiceException;
......@@ -88,19 +88,21 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
@Override
public IPage<CustomProductInfoSnakeVO> pageList(CustomProductInfoSnakeDTO param) {
CustomAsserts.nonNull(param, "分页查询参数不能为空");
QueryWrapper<CustomProductInfoEntity> queryWrapper = new QueryWrapper<>();
LambdaQueryWrapper<CustomProductInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
// 构造查询条件
toQueryWrapper(param, queryWrapper);
IPage<CustomProductInfoEntity> page = customProductInfoDomainService.selectPage(queryWrapper, param);
return page.convert(e -> {
CustomProductInfoSnakeVO snakeVO = BeanMapper.snakeCase().convert(e, CustomProductInfoSnakeVO.class);
snakeVO.setColorImageList(Arrays.asList(e.getColorImages().split(",")));
if (StringUtils.isNotBlank(e.getColorImages())) {
snakeVO.setColorImageList(Arrays.asList(e.getColorImages().split(",")));
}
return snakeVO;
});
}
/** 标准分页查询条件构建(非 ERP) */
private void toQueryWrapper(CustomProductInfoSnakeDTO param, QueryWrapper<CustomProductInfoEntity> queryWrapper) {
private void toQueryWrapper(CustomProductInfoSnakeDTO param, LambdaQueryWrapper<CustomProductInfoEntity> queryWrapper) {
toQueryWrapper(param, queryWrapper, false);
}
......@@ -109,7 +111,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
*
* @param isErp true=ERP 模式(title 双字段 OR 搜索,processing 支持 2=IS NULL,跳过 DIY/黑名单过滤)
*/
private void toQueryWrapper(CustomProductInfoSnakeDTO param, QueryWrapper<CustomProductInfoEntity> queryWrapper, boolean isErp) {
private void toQueryWrapper(CustomProductInfoSnakeDTO param, LambdaQueryWrapper<CustomProductInfoEntity> queryWrapper, boolean isErp) {
// 分类层级过滤
if (param.getCategory_id() != null) {
List<CategoryInfoModel> cateList = saasAdminService.getAllList();
......@@ -120,7 +122,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
CategoryInfoModel cate = cateList.stream().filter(c -> c.getId().equals(param.getCategory_id()))
.findFirst().orElseThrow(() -> new ServiceException("不存在该类别, category_id=" + param.getCategory_id()));
String pids = String.valueOf(cate.getId());
if (cate.getPids() != null && !cate.getPids().isEmpty()) {
if (StringUtils.isNotBlank(cate.getPids())) {
pids = cate.getPids() + "," + pids;
}
String finalPids = pids;
......@@ -132,10 +134,10 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
})
.map(CategoryInfoModel::getId).collect(Collectors.toList());
cateIds.add(cate.getId());
queryWrapper.in("category_id", cateIds);
queryWrapper.in(CustomProductInfoEntity::getCategoryId, cateIds);
}
// 工厂 ID 过滤(通过 product_factory_rel M2M 表,对齐 TS:271-283
// 工厂 ID 过滤(通过 product_factory_rel M2M 表)
if (param.getFactory_id() != null) {
List<Integer> factoryProductIds = productFactoryRelDomainService.list(
new LambdaQueryWrapper<ProductFactoryRelEntity>()
......@@ -143,122 +145,117 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.stream().map(ProductFactoryRelEntity::getProductId)
.distinct().collect(Collectors.toList());
if (factoryProductIds.isEmpty()) {
// 没有关联商品时返回空结果
queryWrapper.eq("id", -1);
queryWrapper.eq(CustomProductInfoEntity::getId, -1);
} else {
queryWrapper.in("id", factoryProductIds);
queryWrapper.in(CustomProductInfoEntity::getId, factoryProductIds);
}
}
// DIY 用户过滤与黑名单过滤(仅标准分页使用,ERP权限过滤)
if (!isErp) {
applyDiyAndBlacklistFilter(param, queryWrapper);
// 排序:按 id 降序(对齐 TS:337)
queryWrapper.orderByDesc("id");
queryWrapper.orderByDesc(CustomProductInfoEntity::getId);
}
// 是否九猫处理过滤
// 是否九猫处理过滤(0=否 1=是 2=未设置,也支持 true/false)
Integer processing = param.getProcessing();
if (processing != null) {
if (processing == 2) {
// ERP: processing=2 → IS NULL(对齐 TS:626)
queryWrapper.isNull("processing");
if (processing != null ) {
if (processing == ProcessingStatus.NOT_SET.getCode()) {
queryWrapper.isNull(CustomProductInfoEntity::getProcessing);
} else {
queryWrapper.eq("processing", processing);
queryWrapper.eq(CustomProductInfoEntity::getProcessing, processing);
}
}
// 直接列等值过滤
if (param.getId() != null) {
queryWrapper.eq("id", param.getId());
queryWrapper.eq(CustomProductInfoEntity::getId, param.getId());
}
// sku 支持逗号分隔多值 IN 查询(ERP查询逻辑)
// sku 支持逗号分隔多值 IN 查询
if (StringUtils.isNotBlank(param.getSku())) {
String[] skuArr = param.getSku().split(",");
if (skuArr.length > 1) {
queryWrapper.in("sku", (Object[]) skuArr);
queryWrapper.in(CustomProductInfoEntity::getSku, (Object[]) skuArr);
} else {
queryWrapper.eq("sku", param.getSku());
queryWrapper.eq(CustomProductInfoEntity::getSku, param.getSku());
}
}
if (param.getStatus() != null) {
queryWrapper.eq("status", param.getStatus());
queryWrapper.eq(CustomProductInfoEntity::getStatus, param.getStatus());
}
if (StringUtils.isNotBlank(param.getProduct_type())) {
queryWrapper.eq("product_type", param.getProduct_type());
queryWrapper.eq(CustomProductInfoEntity::getProductType, param.getProduct_type());
}
// 货号:逗号分隔多值 IN 查询,单个值模糊 LIKE 查询(对齐 TS:774-779)
// 货号:逗号分隔多值 IN 查询,单个值模糊 LIKE 查询
if (StringUtils.isNotBlank(param.getProduct_no())) {
String[] productNoArr = param.getProduct_no().split(",");
if (productNoArr.length > 1) {
queryWrapper.in("product_no", (Object[]) productNoArr);
queryWrapper.in(CustomProductInfoEntity::getProductNo, (Object[]) productNoArr);
} else {
queryWrapper.like("product_no", param.getProduct_no());
queryWrapper.like(CustomProductInfoEntity::getProductNo, param.getProduct_no());
}
}
if (param.getPrint_type() != null) {
queryWrapper.eq("print_type", param.getPrint_type());
queryWrapper.eq(CustomProductInfoEntity::getPrintType, param.getPrint_type());
}
if (StringUtils.isNotBlank(param.getName())) {
queryWrapper.like("name", param.getName());
queryWrapper.like(CustomProductInfoEntity::getName, param.getName());
}
// ERP: title 关键词同时搜索 name 和 title 两列(对齐 TS:610-611)
// ERP: title 关键词同时搜索 name 和 title 两列
if (StringUtils.isNotBlank(param.getTitle())) {
if (isErp) {
queryWrapper.and(w -> w.like("name", param.getTitle()).or().like("title", param.getTitle()));
queryWrapper.and(w -> w.like(CustomProductInfoEntity::getName, param.getTitle())
.or().like(CustomProductInfoEntity::getTitle, param.getTitle()));
} else {
queryWrapper.like("title", param.getTitle());
queryWrapper.like(CustomProductInfoEntity::getTitle, param.getTitle());
}
}
// 工厂过滤(factoryIds 直接列 — 用于创建/更新场景,保留兼容)
if (param.getFactoryIds() != null) {
queryWrapper.in("factory_id", param.getFactoryIds());
queryWrapper.in(CustomProductInfoEntity::getFactoryId, param.getFactoryIds());
}
}
/**
* DIY 用户与黑名单过滤(标准分页专用)
* <p>
* 对齐 TS page:284-322 — diyUserId 与 blackUserId 互斥(else if),
* diyUserId 与 blackUserId 互斥(else if),
* diyUserId=-1 使用 LEFT JOIN 语义查找无绑定的商品。
*/
private void applyDiyAndBlacklistFilter(CustomProductInfoSnakeDTO param, QueryWrapper<CustomProductInfoEntity> queryWrapper) {
private void applyDiyAndBlacklistFilter(CustomProductInfoSnakeDTO param, LambdaQueryWrapper<CustomProductInfoEntity> queryWrapper) {
if (param.getDiyUserId() != null) {
// diyUserId 过滤
List<Integer> productIds;
if (param.getDiyUserId() == -1) {
// 查找无任何 diy_user 绑定的商品(对齐 TS:287-295 LEFT JOIN WHERE IS NULL)
// 通过 Domain 层 Mapper XML 的 selectIdsWithoutDiyUserBind 执行 LEFT JOIN 查询
// 查找无任何 diy_user 绑定的商品(通过 Domain 层 LEFT JOIN 查询)
List<Integer> unboundIds = customProductInfoDomainService.selectIdsWithoutDiyUserBind();
if (CollectionUtils.isEmpty(unboundIds)) {
queryWrapper.eq("id", -1); // 无匹配结果
queryWrapper.eq(CustomProductInfoEntity::getId, -1);
} else {
queryWrapper.in("id", unboundIds);
queryWrapper.in(CustomProductInfoEntity::getId, unboundIds);
}
} else {
productIds = customProductDiyUserRelDomainService.list(
List<Integer> productIds = customProductDiyUserRelDomainService.list(
new LambdaQueryWrapper<CustomProductDiyUserRelEntity>()
.eq(CustomProductDiyUserRelEntity::getDiyUserId, param.getDiyUserId()))
.stream().map(CustomProductDiyUserRelEntity::getProductId)
.distinct().collect(Collectors.toList());
if (CollectionUtils.isEmpty(productIds)) {
queryWrapper.eq("id", -1); // 无匹配结果
queryWrapper.eq(CustomProductInfoEntity::getId, -1);
} else {
queryWrapper.in("id", productIds);
queryWrapper.in(CustomProductInfoEntity::getId, productIds);
}
}
} else if (param.getBlackUserId() != null) {
// blackUserId 过滤(仅在 diyUserId 未设置时生效,对齐 TS else if:311-322
// blackUserId 过滤(仅在 diyUserId 未设置时生效)
List<Integer> productIds = customProductBlacklistDomainService.list(
new LambdaQueryWrapper<CustomProductBlacklistEntity>()
.eq(CustomProductBlacklistEntity::getDiyUserId, param.getBlackUserId()))
.stream().map(CustomProductBlacklistEntity::getProductId)
.distinct().collect(Collectors.toList());
if (productIds.isEmpty()) {
queryWrapper.eq("id", -1); // 无匹配结果
queryWrapper.eq(CustomProductInfoEntity::getId, -1);
} else {
queryWrapper.in("id", productIds);
queryWrapper.in(CustomProductInfoEntity::getId, productIds);
}
}
}
......@@ -396,7 +393,29 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
@Override
public CustomProductInfoSnakeVO getByIdOrSku(Integer id, String sku, String namespace) {
// 1. 查询主表
// 1. 校验并查询主表
CustomProductInfoEntity entity = validateAndQueryEntity(id, sku);
final Integer productId = entity.getId();
// 2. 查询用户(如果指定了 namespace)
DbDiyUserEntity user = queryUserByNamespace(namespace);
// 3. 并行查询所有子表数据
ProductRelatedData relatedData = queryRelatedDataInParallel(productId);
// 4. 组合完整 VO
CustomProductInfoSnakeVO fullVO = buildProductFullVO(entity,
relatedData.items, relatedData.images, relatedData.factoryPrices,
relatedData.diyUserIds, relatedData.craftIds, relatedData.warehouseIds,
relatedData.remark, relatedData.cnRemark, relatedData.factoryIds);
// 5. 解析属性名称
resolvePropertyNames(relatedData.properties, fullVO);
// 6. 应用用户折扣
if (user != null) {
diyUserService.setProductExternalPrice(user, fullVO);
}
return fullVO;
}
/** 校验并查询商品主表实体 */
private CustomProductInfoEntity validateAndQueryEntity(Integer id, String sku) {
CustomProductInfoEntity entity;
if (id != null) {
entity = customProductInfoDomainService.getById(id);
......@@ -408,18 +427,27 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
if (entity == null) {
throw new ServiceException("商品信息不存在, id=" + id + ", sku=" + sku);
}
final Integer productId = entity.getId();
return entity;
}
// 1a. 根据 namespace 查询 DIY 用户(对齐 TS:197-203)
DbDiyUserEntity user = null;
if (StringUtils.isNotBlank(namespace)) {
user = diyUserService.getByNamespace(namespace);
if (user == null) {
throw new ServiceException("用户不存在, namespace=" + namespace);
}
/**
* 根据 namespace 查询用户
*/
private DbDiyUserEntity queryUserByNamespace(String namespace) {
if (StringUtils.isBlank(namespace)) {
return null;
}
DbDiyUserEntity user = diyUserService.getByNamespace(namespace);
if (user == null) {
throw new ServiceException("用户不存在, namespace=" + namespace);
}
return user;
}
// 2. 并行查询所有子表(单表查询,Java 层组合,使用自定义线程池 + 超时保护)
/**
* 并行查询所有子表数据,带超时保护
*/
private ProductRelatedData queryRelatedDataInParallel(Integer productId) {
CompletableFuture<List<CustomProductItemEntity>> itemsFuture = CompletableFuture.supplyAsync(() ->
customProductItemDomainService.list(new LambdaQueryWrapper<CustomProductItemEntity>()
.eq(CustomProductItemEntity::getProductId, productId)
......@@ -458,15 +486,14 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.stream().map(r -> r.getWarehouseId().intValue())
.collect(Collectors.toList()), threadPoolExecutor);
CompletableFuture<CustomProductRemarkEntity> remarkFuture = CompletableFuture.supplyAsync(() -> customProductRemarkDomainService.getOne(
new LambdaQueryWrapper<CustomProductRemarkEntity>()
CompletableFuture<CustomProductRemarkEntity> remarkFuture = CompletableFuture.supplyAsync(() ->
customProductRemarkDomainService.getOne(new LambdaQueryWrapper<CustomProductRemarkEntity>()
.eq(CustomProductRemarkEntity::getProductId, productId)), threadPoolExecutor);
CompletableFuture<CustomProductCnRemarkEntity> cnRemarkFuture = CompletableFuture.supplyAsync(() -> customProductCnRemarkDomainService.getOne(
new LambdaQueryWrapper<CustomProductCnRemarkEntity>()
CompletableFuture<CustomProductCnRemarkEntity> cnRemarkFuture = CompletableFuture.supplyAsync(() ->
customProductCnRemarkDomainService.getOne(new LambdaQueryWrapper<CustomProductCnRemarkEntity>()
.eq(CustomProductCnRemarkEntity::getProductId, productId)), threadPoolExecutor);
// 工厂关联 ID 列表(product_factory_rel 表)
CompletableFuture<List<Integer>> factoryIdsFuture = CompletableFuture.supplyAsync(() ->
productFactoryRelDomainService.list(
new LambdaQueryWrapper<ProductFactoryRelEntity>()
......@@ -474,7 +501,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.stream().map(ProductFactoryRelEntity::getFactoryId)
.collect(Collectors.toList()), threadPoolExecutor);
// 3. 等待所有查询完成(带超时保护,防止某个查询永久阻塞
// 等待所有查询完成(带超时保护
try {
CompletableFuture.allOf(itemsFuture, imagesFuture, propertiesFuture,
factoryPriceFuture, diyUserIdsFuture, craftIdsFuture,
......@@ -483,7 +510,6 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.orTimeout(QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.join();
} catch (CompletionException e) {
// 超时或异常时取消所有未完成的任务
cancelAll(itemsFuture, imagesFuture, propertiesFuture,
factoryPriceFuture, diyUserIdsFuture, craftIdsFuture,
warehouseIdsFuture, remarkFuture, cnRemarkFuture,
......@@ -492,38 +518,35 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.setDetailMessage(e.toString());
}
// 4. Java 层组合结果
CustomProductInfoSnakeVO fullVO;
List<CustomProductInfoPropertyEntity> properties;
// 收集结果
try {
properties = propertiesFuture.get();
fullVO = buildProductFullVO(entity,
itemsFuture.get(),
imagesFuture.get(),
factoryPriceFuture.get(),
diyUserIdsFuture.get(),
craftIdsFuture.get(),
warehouseIdsFuture.get(),
remarkFuture.get(),
cnRemarkFuture.get(),
return new ProductRelatedData(
itemsFuture.get(), imagesFuture.get(), propertiesFuture.get(),
factoryPriceFuture.get(), diyUserIdsFuture.get(), craftIdsFuture.get(),
warehouseIdsFuture.get(), remarkFuture.get(), cnRemarkFuture.get(),
factoryIdsFuture.get());
} catch (Exception e) {
throw new ServiceException("组装商品详情失败, productId=" + productId + ": " + e.getMessage())
.setDetailMessage(e.toString());
}
// 5. 通过 AdminPropertyService 解析属性名称(对齐 TS:217-236)
resolvePropertyNames(properties, fullVO);
// 6. 如果查询了用户,应用外部定价折扣(对齐 TS:241-243)
if (user != null) {
diyUserService.setProductExternalPrice(user, fullVO);
}
return fullVO;
}
/**
* 并行查询结果聚合
*/
private record ProductRelatedData(
List<CustomProductItemEntity> items,
List<CustomProductImageEntity> images,
List<CustomProductInfoPropertyEntity> properties,
List<CustomProductFactoryPriceRelEntity> factoryPrices,
List<Integer> diyUserIds,
List<Long> craftIds,
List<Integer> warehouseIds,
CustomProductRemarkEntity remark,
CustomProductCnRemarkEntity cnRemark,
List<Integer> factoryIds) {}
/**
* 通过 AdminPropertyService 解析属性名称,填充到 FullVO 的 skuProperties / normalProperties 中
* <p>
* 对齐ts代码,将数据库中存储的 property_id/value_id 转换为带名称的属性列表。
......@@ -625,7 +648,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
CustomAsserts.nonNull(dto, "黑名单参数不能为空");
CustomAsserts.nonNull(dto.getProductIds(), "商品 ID 列表不能为空");
// 1. 校验客户是否存在(对齐 TS:563-568)
// 1. 校验客户是否存在
String logStr;
if (dto.getDiyUserIds() != null && !dto.getDiyUserIds().isEmpty()) {
List<DbDiyUserEntity> diyUsers = dbDiyUserDomainService.listByIds(dto.getDiyUserIds());
......@@ -677,7 +700,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
/**
* 获取商品绑定的 DIY 模板列表(对齐 TS:513-526)
* 获取商品绑定的 DIY 模板列表
* <p>
* 通过 product_template_info 表查询商品关联的所有 diy_id,
* 再批量查询 db_diy 表返回完整模板信息。
......@@ -726,7 +749,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
/**
* 获取商品绑定的工艺列表(对齐 TS:786-788 → getCraftByProductId)
* 获取商品绑定的工艺列表
* <p>
* 通过 custom_product_craft_rel 查 craft_id 列表,
* 再批量查询 craft_center 表返回完整工艺实体。
......@@ -753,8 +776,6 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
.collect(Collectors.toList());
}
// ==================== ERP 专用接口 ====================
@Override
public List<DbDiySnakeVO> getBindsDiyByIdAndUserMark(Integer id, String userMark, String namespace) {
CustomAsserts.nonNull(id, "商品 ID 不能为空");
......@@ -778,7 +799,8 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
List<Integer> diyIds = templates.stream().map(ProductTemplateInfoEntity::getDiyId)
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
// 3. 查询 db_diy,根据用户类型过滤状态
// 3. 查询 db_diy,SQL 层完成状态过滤 + FIND_IN_SET 权限过滤(对齐 TS:766-783)
final Integer userId = user.getId();
List<Integer> statusList;
String userName = user.getName();
if ("demo".equals(userName)) {
......@@ -787,23 +809,23 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
statusList = Collections.singletonList(TemplateStatus.SHELF_CODE);
}
List<DbDiyEntity> diys = dbDiyDomainService.list(new LambdaQueryWrapper<DbDiyEntity>()
.in(DbDiyEntity::getId, diyIds).in(DbDiyEntity::getStatus, statusList));
if (CollectionUtils.isEmpty(diys)) {
return Collections.emptyList();
}
// 4. 权限过滤:user_ids / ban_user_ids
final Integer userId = user.getId();
diys = diys.stream().filter(d -> isUserAuthorized(d.getUserIds(), userId))
.filter(d -> !isUserBanned(d.getBanUserIds(), userId)).collect(Collectors.toList());
.in(DbDiyEntity::getId, diyIds)
.in(DbDiyEntity::getStatus, statusList)
// user_ids 权限:NULL(对所有人开放)OR FIND_IN_SET(userId, user_ids) > 0
.and(w -> w.isNull(DbDiyEntity::getUserIds)
.or().apply("FIND_IN_SET({0}, user_ids) > 0", userId))
// ban_user_ids 排除:NULL(无人被禁止)OR FIND_IN_SET(userId, ban_user_ids) = 0
.and(w -> w.isNull(DbDiyEntity::getBanUserIds)
.or().apply("FIND_IN_SET({0}, ban_user_ids) = 0", userId)));
if (CollectionUtils.isEmpty(diys)) {
return Collections.emptyList();
}
// 5. 批量查询所有效果图,按 diyId 分组(1 次 IN 查询替代 N 次逐条查询)
// 4. 批量查询所有效果图,按 diyId 分组(1 次 IN 查询替代 N 次逐条查询)
List<Integer> ids = diys.stream().map(DbDiyEntity::getId).collect(Collectors.toList());
Map<Integer, List<DbDiyXiaoguotuEntity>> xiaoguotuMap = dbDiyXiaoguotuDomainService.selectByDiyIds(ids)
.stream().collect(Collectors.groupingBy(DbDiyXiaoguotuEntity::getDiyId));
// 6. 转换为 VO 并附带效果图
// 5. 转换为 VO 并附带效果图
return diys.stream().map(diy -> {
DbDiySnakeVO vo = BeanMapper.snakeCase().convert(diy, DbDiySnakeVO.class);
List<DbDiyXiaoguotuEntity> xiaoguotus = xiaoguotuMap.getOrDefault(diy.getId(), Collections.emptyList());
......@@ -815,38 +837,6 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}).collect(Collectors.toList());
}
/**
* 检查用户是否在授权名单中(对齐 TS:774-777 FIND_IN_SET)
* <p>
* user_ids 为 null 或空字符串 = 对所有人开放。
* 使用 FIND_IN_SET 语义:逗号分隔值中精确匹配用户 ID。
*/
private boolean isUserAuthorized(String userIds, Integer userId) {
if (StringUtils.isBlank(userIds)) {
return true;
}
String targetId = String.valueOf(userId);
return Arrays.stream(userIds.split(","))
.map(String::strip)
.anyMatch(s -> s.equals(targetId));
}
/**
* 检查用户是否在黑名单中(对齐 TS:778-781 FIND_IN_SET)
* <p>
* ban_user_ids 为 null 或空字符串 = 无人被禁止。
* 使用 FIND_IN_SET 语义:逗号分隔值中精确匹配用户 ID。
*/
private boolean isUserBanned(String banUserIds, Integer userId) {
if (StringUtils.isBlank(banUserIds)) {
return false;
}
String targetId = String.valueOf(userId);
return Arrays.stream(banUserIds.split(","))
.map(String::strip)
.anyMatch(s -> s.equals(targetId));
}
@Override
public IPage<CustomProductInfoVO> erpPage(CustomProductInfoSnakeDTO param) {
CustomAsserts.nonNull(param, "分页查询参数不能为空");
......@@ -860,8 +850,8 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
}
// 2. 构建查询条件(ERP 模式:title OR 搜索,processing 支持 2=IS NULL,跳过 DIY/黑名单)
QueryWrapper<CustomProductInfoEntity> queryWrapper = new QueryWrapper<>();
// 2. 构建查询条件
LambdaQueryWrapper<CustomProductInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
toQueryWrapper(param, queryWrapper, true);
// 3. DIY 模板过滤(对齐 TS:630-657)
......@@ -894,7 +884,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
if (productIds.isEmpty()) {
return emptyPage(param);
}
queryWrapper.in("id", productIds);
queryWrapper.in(CustomProductInfoEntity::getId, productIds);
}
// 4. 仓库国家过滤(对齐 TS:673-681)
......@@ -914,23 +904,20 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
List<Integer> productIds = rels.stream()
.map(CustomProductWarehouseRelEntity::getProductId).distinct().collect(Collectors.toList());
queryWrapper.in("id", productIds);
queryWrapper.in(CustomProductInfoEntity::getId, productIds);
}
// 5. ERP 权限过滤(对齐 TS:686-706 — native SQL 黑名单排除 + 用户绑定过滤)
// 使用单次 JOIN 查询,避免 Java 层多次查询+set 操作,保证性能
// 5. ERP 权限过滤
if (user != null) {
List<Integer> allowedIds = customProductInfoDomainService.selectIdsByErpPermission(user.getId());
if (allowedIds.isEmpty()) {
return emptyPage(param);
}
// MyBatis-Plus 多个 in("id", ...) 叠加 = SQL 层 AND 交集
queryWrapper.in("id", allowedIds);
queryWrapper.in(CustomProductInfoEntity::getId, allowedIds);
}
// 6. 排序(对齐 TS:716-721:sort IS NULL ASC, sort ASC, id DESC)
// MySQL 默认 ASC 时空值排最前,等价于 sort IS NULL ASC
queryWrapper.orderByAsc("sort").orderByDesc("id");
queryWrapper.orderByAsc(CustomProductInfoEntity::getSort).orderByDesc(CustomProductInfoEntity::getId);
// 7. 执行分页查询
IPage<CustomProductInfoEntity> page = customProductInfoDomainService.selectPage(queryWrapper, param);
......@@ -977,7 +964,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
/**
* 批量查询 DIY 模板上架状态,返回 diyId → isShelf 映射
* <p>
* 对齐 TS:724-731,使用单次 IN 查询代替 N+1 逐条查询。
* 使用单次 IN 查询代替 N+1 逐条查询。
*/
private Map<Integer, Boolean> batchQueryDiyShelfStatus(List<CustomProductInfoEntity> rows) {
List<Integer> diyIds = rows.stream()
......@@ -1060,7 +1047,6 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
* 逐条保存子项并返回原始 SKU → 已保存实体的映射
* <p>
* 逐条保存是为了获取每条记录的自增 ID,供后续工厂价格关联的 item_id 填充使用。
* 对齐 TS {@code save} 方法 100-117 行。
*
* @return Map<原始DTO中的SKU, 已保存的实体(含自增ID和替换后的SKU)>
*/
......@@ -1094,7 +1080,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
/**
* 保存工厂价格关联(利用 itemMap 替换 item_sku 为生成的 SKU 并填充 item_id)
* <p>
* 对齐 TS {@code save} 方法 132-139 行。
* 对齐 TS {@code save}
*/
private void saveFactoryPriceRels(List<FactoryPriceRelSnakeDTO> factoryPriceList,
Integer productId,
......@@ -1120,7 +1106,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
/**
* 保存产品-工厂关联(product_factory_rel 表)
* <p>
* 对齐 TS {@code save} 方法 162-163 行。
* 对齐 TS {@code save} 方法
*/
private void saveFactoryRels(List<Integer> factoryIds, Integer productId) {
if (factoryIds == null || factoryIds.isEmpty()) {
......@@ -1171,7 +1157,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
* 保存价格区间关联
*/
private void saveFactoryPriceIntervalRels(List<FactoryPriceIntervalRelSnakeDTO> intervals, Integer productId) {
if (intervals == null || intervals.isEmpty()) {
if (CollectionUtils.isEmpty(intervals)) {
return;
}
List<CustomProductFactoryPriceIntervalRelEntity> rels = intervals.stream().map(dto -> {
......@@ -1302,12 +1288,10 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
customProductWarehouseRelDomainService.saveBatch(rels);
}
// -------- updateFull 辅助方法 --------
/**
* 处理子项变更(增/删/改)
* <p>
* 对齐 TS update:407-420 — 新增子项逐条保存以获取自增 ID,
* 新增子项逐条保存以获取自增 ID,
* 返回 SKU→Entity 映射供后续工厂价格 item_id 回填。
*
* @return 新增子项的原始 SKU → 已保存实体映射(含自增 ID)
......@@ -1363,7 +1347,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
/**
* 将 sizeChange 合并到 imageChange(对齐 TS:369-379)
* 将 sizeChange 合并到 imageChange
* <p>
* sizeChange 的 addList/updateList 设置 type=1 后合并到 imageChange,
* removeList 也合并,最终统一处理。
......@@ -1420,8 +1404,8 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
/**
* 处理工厂价格变更(增/删/改)
* <p>
* 对齐 TS update:422-430 — 顺序:新增 → 修改 → 删除。
* 新增时从 newItemMap 回填 item_id(对齐 TS:413-416)
* 顺序:新增 → 修改 → 删除。
* 新增时从 newItemMap 回填 item_id。
*/
private void handleFactoryPriceChanges(
CustomProductInfoUpdateSnakeDTO.ProductFactoryPriceChangeDTO change, Integer productId,
......@@ -1431,7 +1415,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
for (FactoryPriceRelSnakeDTO dto : change.getAddList()) {
CustomProductFactoryPriceRelEntity rel = BeanMapper.snakeCase().convert(dto, CustomProductFactoryPriceRelEntity.class);
rel.setProductId(productId);
// 从新增子项映射中回填 item_id 和 item_sku(对齐 TS:413-416)
// 从新增子项映射中回填 item_id 和 item_sku
if (newItemMap != null && StringUtils.isNotBlank(dto.getItem_sku())) {
CustomProductItemEntity newItem = newItemMap.get(dto.getItem_sku());
if (newItem != null) {
......@@ -1590,6 +1574,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
logEntry.setDescription(description);
logEntry.setEmployeeId(loginUser.getUserId());
logEntry.setEmployeeAccount(loginUser.getUsername());
logEntry.setCreateTime(new Date());
logCustomProductDomainService.save(logEntry);
}
......@@ -1598,6 +1583,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
*/
private void saveLogBatch(String action, List<Integer> productIds) {
LoginUser loginUser = SecurityUtils.getLoginUser();
Date now = new Date();
List<LogCustomProductEntity> logs = new ArrayList<>();
for (Integer productId : productIds) {
LogCustomProductEntity logEntry = new LogCustomProductEntity();
......@@ -1605,6 +1591,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
logEntry.setDescription(action);
logEntry.setEmployeeId(loginUser.getUserId());
logEntry.setEmployeeAccount(loginUser.getUsername());
logEntry.setCreateTime(now);
logs.add(logEntry);
}
logCustomProductDomainService.saveBatch(logs);
......@@ -1621,8 +1608,6 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
}
// -------- getByIdOrSku 辅助方法 --------
/**
* 将并行查询结果组装为 FullVO
*/
......@@ -1638,18 +1623,20 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
List<Integer> factoryIds) {
CustomProductInfoSnakeVO fullVO = BeanMapper.snakeCase().convert(entity, CustomProductInfoSnakeVO.class);
// 子项(对齐 TS:209-214 — 同步主表的 print_type 到每个子项)
// 子项(同步主表的 print_type 到每个子项)
fullVO.setProductList(items.stream()
.map(e -> {
CustomProductItemSnakeVO vo = BeanMapper.snakeCase().convert(e, CustomProductItemSnakeVO.class);
// 同步主表 printType 到子项(对齐 TS:212)
// 同步主表 printType 到子项
vo.setPrint_type(entity.getPrintType());
return vo;
})
.collect(Collectors.toList()));
// 图片拆分:按类型分为普通图和尺码图(对齐 TS:205-206)
fullVO.setColorImageList(Arrays.asList(entity.getColorImages().split(",")));
// 图片拆分:按类型分为普通图和尺码图
if (entity.getColorImages() != null) {
fullVO.setColorImageList(Arrays.asList(entity.getColorImages().split(",")));
}
fullVO.setImageList(images.stream()
.filter(img -> img.getType() == null || img.getType() == IMAGE_TYPE_NORMAL)
.map(img -> BeanMapper.snakeCase().convert(img, CustomProductImageSnakeVO.class))
......@@ -1667,7 +1654,7 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
fullVO.setDiyUserIds(diyUserIds);
fullVO.setCraftIds(craftIds.stream().map(String::valueOf).collect(Collectors.toList()));
fullVO.setWarehouseIds(warehouseIds);
// 工厂关联 ID 列表(对齐 TS:238)
// 工厂关联 ID 列表
fullVO.setFactoryIds(factoryIds);
// 备注
......
package com.jomalls.custom.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jomalls.custom.app.vo.LogCustomProductSnakeVO;
import com.jomalls.custom.app.service.LogCustomProductService;
import com.jomalls.custom.app.utils.BeanMapper;
......@@ -32,11 +32,12 @@ public class LogCustomProductServiceImpl implements LogCustomProductService {
@Override
public List<LogCustomProductSnakeVO> getListByProductId(Integer productId) {
CustomAsserts.nonNull(productId, "商品 ID 不能为空");
// 按 product_id 查询所有日志,按 id 降序排列
// 按 product_id 查询日志,按 id 降序,限制 100 条防止数据膨胀
List<LogCustomProductEntity> logs = logCustomProductDomainService.list(
new QueryWrapper<LogCustomProductEntity>()
.eq("product_id", productId)
.orderByDesc("id"));
new LambdaQueryWrapper<LogCustomProductEntity>()
.eq(LogCustomProductEntity::getProductId, productId)
.orderByDesc(LogCustomProductEntity::getId)
.last("LIMIT 100"));
return logs.stream()
.map(e -> BeanMapper.snakeCase().convert(e, LogCustomProductSnakeVO.class))
.collect(Collectors.toList());
......
......@@ -19,6 +19,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* 商品分类服务
......@@ -43,8 +44,14 @@ public class SaasAdminService {
private static final int DEFAULT_MAP_SIZE = 2;
/** 分类列表缓存 TTL(毫秒):5 分钟 */
private static final long CACHE_TTL_MS = 5 * 60 * 1000;
private final RemoteApiClient remoteApiClient;
/** 分类列表本地缓存 */
private final AtomicReference<CacheEntry<List<CategoryInfoModel>>> categoryCache = new AtomicReference<>();
@Value("${server.admin.base-url:https://admin.jomalls.com}")
private String adminBaseUrl;
......@@ -178,26 +185,54 @@ public class SaasAdminService {
}
/**
* 查询所有分类
* 查询所有分类(带本地缓存,TTL 5 分钟)
* <p>
* 对齐 TS {@code allList()}
* 对齐 TS {@code allList()}。分类数据不频繁变动,
* 使用本地缓存避免每次分页查询都发起远程 HTTP 调用。
*/
public List<CategoryInfoModel> getAllList() {
// 命中缓存且未过期 → 直接返回
CacheEntry<List<CategoryInfoModel>> entry = categoryCache.get();
if (entry != null && !entry.isExpired()) {
return entry.data;
}
// 未命中或已过期 → 远程调用刷新
try {
ResponseEntity<SaasAdminApiResponseModel<List<CategoryInfoModel>>> response = remoteApiClient.get(
adminBaseUrl + GET_ALL_LIST_URL,
new ParameterizedTypeReference<>() {},
getHeader());
if (response != null && response.getBody() != null) {
log.debug("[ SaasAdminService ] getAllList 成功, 返回: {}", response.toString());
SaasAdminApiResponseModel<List<CategoryInfoModel>> responseBody = response.getBody();
if (responseBody.getCode() == CodeEnum.SUCCESS.getCode()) {
return responseBody.getData();
List<CategoryInfoModel> data = responseBody.getData();
categoryCache.set(new CacheEntry<>(data));
return data;
}
}
} catch (Exception e) {
log.error("[ SaasAdminService ] getAllList 调用失败", e);
}
// 远程调用失败但有旧缓存 → 降级返回旧缓存
if (entry != null) {
log.warn("[ SaasAdminService ] getAllList 远程失败,降级使用过期缓存");
return entry.data;
}
return Collections.emptyList();
}
/** 简单 TTL 缓存条目 */
private static class CacheEntry<T> {
final T data;
final long expireAt;
CacheEntry(T data) {
this.data = data;
this.expireAt = System.currentTimeMillis() + CACHE_TTL_MS;
}
boolean isExpired() {
return System.currentTimeMillis() > expireAt;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment