|
@@ -0,0 +1,557 @@
|
|
|
|
|
+package cn.yoshop.api.service;
|
|
|
|
|
+
|
|
|
|
|
+import cn.yoshop.api.support.ApiContext;
|
|
|
|
|
+import cn.yoshop.api.support.ApiView;
|
|
|
|
|
+import cn.yoshop.common.entity.Goods;
|
|
|
|
|
+import cn.yoshop.common.entity.GoodsImage;
|
|
|
|
|
+import cn.yoshop.common.entity.GoodsSku;
|
|
|
|
|
+import cn.yoshop.common.entity.GoodsSpecRel;
|
|
|
|
|
+import cn.yoshop.common.entity.Spec;
|
|
|
|
|
+import cn.yoshop.common.entity.SpecValue;
|
|
|
|
|
+import cn.yoshop.common.entity.UploadFile;
|
|
|
|
|
+import cn.yoshop.common.entity.User;
|
|
|
|
|
+import cn.yoshop.common.entity.UserGrade;
|
|
|
|
|
+import cn.yoshop.common.exception.BizException;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsCategoryRelMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsImageMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsServiceMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsServiceRelMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsSkuMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.GoodsSpecRelMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.SpecMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.SpecValueMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.UploadFileMapper;
|
|
|
|
|
+import cn.yoshop.common.mapper.UserGradeMapper;
|
|
|
|
|
+import cn.yoshop.common.page.PageVO;
|
|
|
|
|
+import cn.yoshop.common.util.PhpStr;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+import java.util.function.Function;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 客户端商品服务,对齐 PHP:
|
|
|
|
|
+ * app/api/model/Goods(getList / getDetails / getBasic / getSpecData)、
|
|
|
|
|
+ * app/api/service/Goods::getSkuInfo、app/api/model/goods/Service::getListByGoods。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>字段口径:PHP 各模型用 $hidden 裁剪,这里在序列化后用 {@link ApiView#hide} 移除同名键。
|
|
|
|
|
+ * 商品基础隐藏集:images / delivery / deductStockType / salesInitial / salesActual /
|
|
|
|
|
+ * sort / isDelete / storeId / createTime / updateTime。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>会员折扣:登录用户有有效等级且商品参与折扣时,goodsPriceMin/Max 与 sku.goodsPrice
|
|
|
|
|
+ * 按等级折扣率重算,并置 isUserGrade=true(对齐 PHP setGoodsGradeMoney)。
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class ApiGoodsService {
|
|
|
|
|
+
|
|
|
|
|
+ /** 商品基础隐藏字段(对齐 PHP api\model\Goods::$hidden) */
|
|
|
|
|
+ private static final String[] HIDDEN_BASE = {
|
|
|
|
|
+ "images", "delivery", "deductStockType", "salesInitial", "salesActual",
|
|
|
|
|
+ "sort", "isDelete", "storeId", "createTime", "updateTime"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private final GoodsMapper goodsMapper;
|
|
|
|
|
+ private final GoodsImageMapper goodsImageMapper;
|
|
|
|
|
+ private final UploadFileMapper uploadFileMapper;
|
|
|
|
|
+ private final GoodsSkuMapper goodsSkuMapper;
|
|
|
|
|
+ private final GoodsSpecRelMapper goodsSpecRelMapper;
|
|
|
|
|
+ private final SpecMapper specMapper;
|
|
|
|
|
+ private final SpecValueMapper specValueMapper;
|
|
|
|
|
+ private final GoodsCategoryRelMapper goodsCategoryRelMapper;
|
|
|
|
|
+ private final GoodsServiceMapper goodsServiceMapper;
|
|
|
|
|
+ private final GoodsServiceRelMapper goodsServiceRelMapper;
|
|
|
|
|
+ private final UserGradeMapper userGradeMapper;
|
|
|
|
|
+ private final ApiView view;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${yoshop.base-url}")
|
|
|
|
|
+ private String baseUrl;
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ 列表
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 商品列表,对齐 PHP api\model\Goods::getList:
|
|
|
|
|
+ * 固定 status=10(上架),支持 listType / categoryId / goodsName / goodsNo / sortType / sortPrice,
|
|
|
|
|
+ * 分页 15,列表隐藏 content / goodsImages / images。
|
|
|
|
|
+ */
|
|
|
|
|
+ public PageVO<ObjectNode> getList(Map<String, Object> params, User user) {
|
|
|
|
|
+ int page = intVal(params.get("page"), 1);
|
|
|
|
|
+ int listRows = 15;
|
|
|
|
|
+ QueryWrapper<Goods> qw = buildQuery(params);
|
|
|
|
|
+
|
|
|
|
|
+ Page<Goods> result = goodsMapper.selectPage(new Page<>(page, listRows), qw);
|
|
|
|
|
+ List<Goods> records = result.getRecords();
|
|
|
|
|
+ assemble(records);
|
|
|
|
|
+ PageVO<ObjectNode> vo = new PageVO<>();
|
|
|
|
|
+ vo.setTotal(result.getTotal());
|
|
|
|
|
+ vo.setPerPage(result.getSize());
|
|
|
|
|
+ vo.setCurrentPage(result.getCurrent() > result.getPages() && result.getPages() > 0
|
|
|
|
|
+ ? result.getPages() : result.getCurrent());
|
|
|
|
|
+ vo.setLastPage(result.getPages());
|
|
|
|
|
+ vo.setData(toApiNodes(records, user, true));
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** DIY 商品组件(自动来源):按 showNum 取前 N 条,不分页 */
|
|
|
|
|
+ public List<ObjectNode> getListForComponent(Map<String, Object> params, User user, int limit) {
|
|
|
|
|
+ QueryWrapper<Goods> qw = buildQuery(params);
|
|
|
|
|
+ qw.last("limit " + Math.max(limit, 1));
|
|
|
|
|
+ List<Goods> records = goodsMapper.selectList(qw);
|
|
|
|
|
+ assemble(records);
|
|
|
|
|
+ return toApiNodes(records, user, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 列表检索条件(列表与 DIY 组件共用) */
|
|
|
|
|
+ private QueryWrapper<Goods> buildQuery(Map<String, Object> params) {
|
|
|
|
|
+ String listType = strVal(params.get("listType"), "all");
|
|
|
|
|
+ Integer categoryId = intOrNull(params.get("categoryId"));
|
|
|
|
|
+ QueryWrapper<Goods> qw = new QueryWrapper<>();
|
|
|
|
|
+ qw.select(Goods.class, info -> !"content".equals(info.getColumn()));
|
|
|
|
|
+ qw.eq("is_delete", 0);
|
|
|
|
|
+ qw.eq("store_id", ApiContext.STORE_ID);
|
|
|
|
|
+ switch (listType) {
|
|
|
|
|
+ case "off_sale" -> qw.eq("status", 20);
|
|
|
|
|
+ case "sold_out" -> qw.eq("stock_total", 0);
|
|
|
|
|
+ default -> {
|
|
|
|
|
+ // on_sale / all 均落到下面的固定上架条件
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // PHP: array_merge($param, ['status' => ON_SALE]) —— 固定只查上架商品
|
|
|
|
|
+ qw.eq("status", 10);
|
|
|
|
|
+ if (categoryId != null && categoryId > 0) {
|
|
|
|
|
+ List<Integer> goodsIds = goodsCategoryRelMapper.selectGoodsIds(ApiContext.STORE_ID, categoryId);
|
|
|
|
|
+ qw.in("goods_id", goodsIds.isEmpty() ? List.of(-1) : goodsIds);
|
|
|
|
|
+ }
|
|
|
|
|
+ String goodsName = strOrNull(params.get("goodsName"));
|
|
|
|
|
+ if (StringUtils.hasText(goodsName)) {
|
|
|
|
|
+ qw.like("goods_name", goodsName);
|
|
|
|
|
+ }
|
|
|
|
|
+ String goodsNo = strOrNull(params.get("goodsNo"));
|
|
|
|
|
+ if (StringUtils.hasText(goodsNo)) {
|
|
|
|
|
+ qw.like("goods_no", goodsNo);
|
|
|
|
|
+ }
|
|
|
|
|
+ applySort(qw, strVal(params.get("sortType"), "all"), boolVal(params.get("sortPrice")));
|
|
|
|
|
+ return qw;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 按 ID 集取上架商品(DIY 页面商品组件用),保持传入顺序 */
|
|
|
|
|
+ public List<ObjectNode> getListByIds(List<Integer> goodsIds, User user) {
|
|
|
|
|
+ if (goodsIds == null || goodsIds.isEmpty()) {
|
|
|
|
|
+ return List.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ QueryWrapper<Goods> qw = new QueryWrapper<>();
|
|
|
|
|
+ qw.select(Goods.class, info -> !"content".equals(info.getColumn()));
|
|
|
|
|
+ qw.in("goods_id", goodsIds);
|
|
|
|
|
+ qw.eq("status", 10);
|
|
|
|
|
+ qw.eq("is_delete", 0);
|
|
|
|
|
+ List<Goods> records = goodsMapper.selectList(qw);
|
|
|
|
|
+ Map<Integer, Integer> order = new HashMap<>();
|
|
|
|
|
+ for (int i = 0; i < goodsIds.size(); i++) {
|
|
|
|
|
+ order.putIfAbsent(goodsIds.get(i), i);
|
|
|
|
|
+ }
|
|
|
|
|
+ records.sort(Comparator.comparingInt(g -> order.getOrDefault(g.getGoodsId(), Integer.MAX_VALUE)));
|
|
|
|
|
+ assemble(records);
|
|
|
|
|
+ return toApiNodes(records, user, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ 详情
|
|
|
|
|
+
|
|
|
|
|
+ /** 商品详情(含 SKU / 视频 / 规格),对齐 PHP api\model\Goods::getDetails */
|
|
|
|
|
+ public ObjectNode getDetail(int goodsId, boolean verifyStatus, User user) {
|
|
|
|
|
+ Goods goods = loadGoods(goodsId, verifyStatus);
|
|
|
|
|
+ assemble(List.of(goods));
|
|
|
|
|
+ goods.setSkuList(skuList(goodsId, true));
|
|
|
|
|
+ goods.setVideo(findFile(goods.getVideoId()));
|
|
|
|
|
+ goods.setVideoCover(findFile(goods.getVideoCoverId()));
|
|
|
|
|
+ goods.setSpecList(new ArrayList<>(getSpecList(goodsId)));
|
|
|
|
|
+ ObjectNode node = view.of(goods);
|
|
|
|
|
+ // PHP getDetails 额外隐藏 images(保留 goodsImages / goodsImage)
|
|
|
|
|
+ view.hide(node, HIDDEN_BASE);
|
|
|
|
|
+ applyGrade(node, user);
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 商品基础信息,对齐 PHP api\model\Goods::getBasic(仅主信息 + 图片) */
|
|
|
|
|
+ public ObjectNode getBasic(int goodsId, boolean verifyStatus, User user) {
|
|
|
|
|
+ Goods goods = loadGoods(goodsId, verifyStatus);
|
|
|
|
|
+ assemble(List.of(goods));
|
|
|
|
|
+ ObjectNode node = view.of(goods);
|
|
|
|
|
+ // 基础信息不含 skuList / video / specList / videoCover(未查询,实体侧为 null,直接移除)
|
|
|
|
|
+ view.hide(node, HIDDEN_BASE);
|
|
|
|
|
+ node.remove(List.of("skuList", "specList", "video", "videoCover",
|
|
|
|
|
+ "categoryIds", "serviceIds", "isSpecLocked"));
|
|
|
|
|
+ applyGrade(node, user);
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 规格数据:{skuList, specList},对齐 PHP api\model\Goods::getSpecData */
|
|
|
|
|
+ public Map<String, Object> getSpecData(int goodsId) {
|
|
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
|
|
+ ArrayNode skuNodes = view.mapper().createArrayNode();
|
|
|
|
|
+ for (GoodsSku sku : skuList(goodsId, false)) {
|
|
|
|
|
+ ObjectNode node = view.of(sku);
|
|
|
|
|
+ view.hide(node, "image", "storeId", "createTime", "updateTime");
|
|
|
|
|
+ node.put("imageUrl", sku.getImage() == null ? null : sku.getImage().getPreviewUrl());
|
|
|
|
|
+ skuNodes.add(node);
|
|
|
|
|
+ }
|
|
|
|
|
+ data.put("skuList", skuNodes);
|
|
|
|
|
+ data.put("specList", view.mapper().valueToTree(getSpecList(goodsId)));
|
|
|
|
|
+ return data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 指定 SKU 信息(含 goodsImage),对齐 PHP api\service\Goods::getSkuInfo */
|
|
|
|
|
+ public ObjectNode getSkuInfo(int goodsId, String goodsSkuId, User user) {
|
|
|
|
|
+ GoodsSku sku = goodsSkuMapper.selectOne(Wrappers.<GoodsSku>lambdaQuery()
|
|
|
|
|
+ .eq(GoodsSku::getGoodsId, goodsId)
|
|
|
|
|
+ .eq(GoodsSku::getGoodsSkuId, goodsSkuId)
|
|
|
|
|
+ .last("limit 1"));
|
|
|
|
|
+ if (sku == null) {
|
|
|
|
|
+ throw new BizException("很抱歉,商品规格信息不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ UploadFile image = findFile(sku.getImageId());
|
|
|
|
|
+ sku.setImage(image);
|
|
|
|
|
+ ObjectNode node = view.of(sku);
|
|
|
|
|
+ view.hide(node, "image", "storeId", "createTime", "updateTime");
|
|
|
|
|
+ node.put("goodsImage", image == null ? null : image.getPreviewUrl());
|
|
|
|
|
+ node.put("imageUrl", image == null ? null : image.getPreviewUrl());
|
|
|
|
|
+ // SKU 出参的会员价(PHP Goods::getSkuInfo 会对 skuInfo 应用折扣)
|
|
|
|
|
+ applySkuGrade(node, user);
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ 规格
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 商品服务与承诺,对齐 PHP api\model\goods\Service::getListByGoods:
|
|
|
|
|
+ * 按商品已绑定的 service_id 取启用记录,排序 sort asc + id asc。
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<ObjectNode> getServiceList(int goodsId) {
|
|
|
|
|
+ List<Integer> serviceIds = goodsServiceRelMapper.selectServiceIds(goodsId);
|
|
|
|
|
+ if (serviceIds.isEmpty()) {
|
|
|
|
|
+ return List.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<cn.yoshop.common.entity.GoodsService> list = goodsServiceMapper.selectList(
|
|
|
|
|
+ Wrappers.<cn.yoshop.common.entity.GoodsService>lambdaQuery()
|
|
|
|
|
+ .in(cn.yoshop.common.entity.GoodsService::getServiceId, serviceIds)
|
|
|
|
|
+ .eq(cn.yoshop.common.entity.GoodsService::getStatus, 1)
|
|
|
|
|
+ .orderByAsc(cn.yoshop.common.entity.GoodsService::getSort)
|
|
|
|
|
+ .orderByAsc(cn.yoshop.common.entity.GoodsService::getServiceId));
|
|
|
|
|
+ List<ObjectNode> nodes = new ArrayList<>();
|
|
|
|
|
+ for (cn.yoshop.common.entity.GoodsService service : list) {
|
|
|
|
|
+ ObjectNode node = view.of(service);
|
|
|
|
|
+ view.hide(node, "isDefault", "status", "sort", "isDelete", "storeId", "createTime", "updateTime");
|
|
|
|
|
+ nodes.add(node);
|
|
|
|
|
+ }
|
|
|
|
|
+ return nodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 规格属性列表,对齐 PHP GoodsSpecRel::getSpecList(结构与 store 端一致) */
|
|
|
|
|
+ public List<Map<String, Object>> getSpecList(int goodsId) {
|
|
|
|
|
+ List<GoodsSpecRel> rels = goodsSpecRelMapper.selectList(
|
|
|
|
|
+ Wrappers.<GoodsSpecRel>lambdaQuery()
|
|
|
|
|
+ .eq(GoodsSpecRel::getGoodsId, goodsId)
|
|
|
|
|
+ .orderByAsc(GoodsSpecRel::getId));
|
|
|
|
|
+ if (rels.isEmpty()) {
|
|
|
|
|
+ return List.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Integer> specIds = rels.stream().map(GoodsSpecRel::getSpecId).distinct().toList();
|
|
|
|
|
+ Map<Integer, Spec> specMap = specMapper.selectBatchIds(specIds).stream()
|
|
|
|
|
+ .collect(Collectors.toMap(Spec::getSpecId, Function.identity()));
|
|
|
|
|
+ List<Integer> valueIds = rels.stream().map(GoodsSpecRel::getSpecValueId).distinct().toList();
|
|
|
|
|
+ Map<Integer, SpecValue> valueMap = specValueMapper.selectBatchIds(valueIds).stream()
|
|
|
|
|
+ .collect(Collectors.toMap(SpecValue::getSpecValueId, Function.identity()));
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> specList = new ArrayList<>();
|
|
|
|
|
+ Map<Integer, Map<String, Object>> groupIndex = new LinkedHashMap<>();
|
|
|
|
|
+ for (GoodsSpecRel rel : rels) {
|
|
|
|
|
+ if (groupIndex.containsKey(rel.getSpecId())) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Spec spec = specMap.get(rel.getSpecId());
|
|
|
|
|
+ Map<String, Object> group = new LinkedHashMap<>();
|
|
|
|
|
+ group.put("specId", spec == null ? rel.getSpecId() : spec.getSpecId());
|
|
|
|
|
+ group.put("specName", spec == null ? "" : spec.getSpecName());
|
|
|
|
|
+ group.put("key", groupIndex.size());
|
|
|
|
|
+ group.put("valueList", new ArrayList<Map<String, Object>>());
|
|
|
|
|
+ groupIndex.put(rel.getSpecId(), group);
|
|
|
|
|
+ specList.add(group);
|
|
|
|
|
+ }
|
|
|
|
|
+ for (GoodsSpecRel rel : rels) {
|
|
|
|
|
+ Map<String, Object> group = groupIndex.get(rel.getSpecId());
|
|
|
|
|
+ if (group == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ List<Map<String, Object>> valueList = (List<Map<String, Object>>) group.get("valueList");
|
|
|
|
|
+ SpecValue value = valueMap.get(rel.getSpecValueId());
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("key", valueList.size());
|
|
|
|
|
+ item.put("groupKey", group.get("key"));
|
|
|
|
|
+ item.put("specValueId", value == null ? rel.getSpecValueId() : value.getSpecValueId());
|
|
|
|
|
+ item.put("specValue", value == null ? "" : value.getSpecValue());
|
|
|
|
|
+ valueList.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ return specList;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ 内部
|
|
|
|
|
+
|
|
|
|
|
+ private Goods loadGoods(int goodsId, boolean verifyStatus) {
|
|
|
|
|
+ Goods goods = goodsMapper.selectById(goodsId);
|
|
|
|
|
+ if (goods == null || Integer.valueOf(1).equals(goods.getIsDelete())) {
|
|
|
|
|
+ throw new BizException("很抱歉,商品信息不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (verifyStatus && Integer.valueOf(20).equals(goods.getStatus())) {
|
|
|
|
|
+ throw new BizException("很抱歉,当前商品已下架");
|
|
|
|
|
+ }
|
|
|
|
|
+ // content 在实体上 select=false(列表不查长文本),详情单独查一次并做 HTML 反转义
|
|
|
|
|
+ Goods contentHolder = goodsMapper.selectOne(
|
|
|
|
|
+ Wrappers.<Goods>query().select("goods_id", "content").eq("goods_id", goodsId));
|
|
|
|
|
+ if (contentHolder != null && contentHolder.getContent() != null) {
|
|
|
|
|
+ goods.setContent(PhpStr.htmlspecialcharsDecode(contentHolder.getContent()));
|
|
|
|
|
+ }
|
|
|
|
|
+ return goods;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<GoodsSku> skuList(int goodsId, boolean withImage) {
|
|
|
|
|
+ List<GoodsSku> skus = goodsSkuMapper.selectList(Wrappers.<GoodsSku>lambdaQuery()
|
|
|
|
|
+ .eq(GoodsSku::getGoodsId, goodsId)
|
|
|
|
|
+ .orderByAsc(GoodsSku::getId));
|
|
|
|
|
+ skus.forEach(sku -> {
|
|
|
|
|
+ sku.setGoodsProps(blankToNull(sku.getGoodsProps()));
|
|
|
|
|
+ sku.setSpecValueIds(blankToNull(sku.getSpecValueIds()));
|
|
|
|
|
+ });
|
|
|
|
|
+ if (withImage) {
|
|
|
|
|
+ fillSkuImage(skus);
|
|
|
|
|
+ }
|
|
|
|
|
+ return skus;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void fillSkuImage(List<GoodsSku> skuList) {
|
|
|
|
|
+ List<Integer> imageIds = skuList.stream().map(GoodsSku::getImageId)
|
|
|
|
|
+ .filter(id -> id != null && id > 0).distinct().toList();
|
|
|
|
|
+ if (imageIds.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<Integer, UploadFile> files = uploadFileMapper.selectBatchIds(imageIds).stream()
|
|
|
|
|
+ .peek(f -> f.setBaseUrl(baseUrl))
|
|
|
|
|
+ .collect(Collectors.toMap(UploadFile::getFileId, Function.identity()));
|
|
|
|
|
+ skuList.forEach(sku -> sku.setImage(files.get(sku.getImageId())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private UploadFile findFile(Integer fileId) {
|
|
|
|
|
+ if (fileId == null || fileId == 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ UploadFile file = uploadFileMapper.selectById(fileId);
|
|
|
|
|
+ if (file != null) {
|
|
|
|
|
+ file.setBaseUrl(baseUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+ return file;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 组装图片(goods_image / goods_images),对齐 PHP setGoodsData */
|
|
|
|
|
+ private void assemble(List<Goods> records) {
|
|
|
|
|
+ if (records.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Integer> goodsIds = records.stream().map(Goods::getGoodsId).toList();
|
|
|
|
|
+ List<GoodsImage> rels = goodsImageMapper.selectList(Wrappers.<GoodsImage>lambdaQuery()
|
|
|
|
|
+ .in(GoodsImage::getGoodsId, goodsIds)
|
|
|
|
|
+ .orderByAsc(GoodsImage::getId));
|
|
|
|
|
+ List<Integer> fileIds = rels.stream().map(GoodsImage::getImageId)
|
|
|
|
|
+ .filter(Objects::nonNull).distinct().toList();
|
|
|
|
|
+ Map<Integer, UploadFile> fileMap = fileIds.isEmpty() ? Collections.emptyMap()
|
|
|
|
|
+ : uploadFileMapper.selectBatchIds(fileIds).stream()
|
|
|
|
|
+ .peek(f -> f.setBaseUrl(baseUrl))
|
|
|
|
|
+ .collect(Collectors.toMap(UploadFile::getFileId, Function.identity()));
|
|
|
|
|
+ Map<Integer, List<GoodsImage>> byGoods = rels.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(GoodsImage::getGoodsId, LinkedHashMap::new, Collectors.toList()));
|
|
|
|
|
+ for (Goods goods : records) {
|
|
|
|
|
+ List<GoodsImage> images = byGoods.getOrDefault(goods.getGoodsId(), List.of());
|
|
|
|
|
+ images.forEach(img -> img.setFile(fileMap.get(img.getImageId())));
|
|
|
|
|
+ goods.setImages(images);
|
|
|
|
|
+ List<UploadFile> files = images.stream().map(GoodsImage::getFile)
|
|
|
|
|
+ .filter(Objects::nonNull).toList();
|
|
|
|
|
+ goods.setGoodsImages(files);
|
|
|
|
|
+ goods.setGoodsImage(files.isEmpty() ? null : files.get(0).getPreviewUrl());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 列表/详情出参:裁剪字段 + 会员价 */
|
|
|
|
|
+ private List<ObjectNode> toApiNodes(List<Goods> records, User user, boolean hideImages) {
|
|
|
|
|
+ List<ObjectNode> nodes = new ArrayList<>();
|
|
|
|
|
+ for (Goods goods : records) {
|
|
|
|
|
+ ObjectNode node = view.of(goods);
|
|
|
|
|
+ view.hide(node, HIDDEN_BASE);
|
|
|
|
|
+ if (hideImages) {
|
|
|
|
|
+ node.remove(List.of("images", "goodsImages"));
|
|
|
|
|
+ }
|
|
|
|
|
+ applyGrade(node, user);
|
|
|
|
|
+ nodes.add(node);
|
|
|
|
|
+ }
|
|
|
|
|
+ return nodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------ 会员折扣
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 商品会员价,对齐 PHP api\model\Goods::setGoodsGradeMoney:
|
|
|
|
|
+ * 未登录 / 无等级 / 商品不参与折扣时 isUserGrade=false,价格不变。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void applyGrade(ObjectNode node, User user) {
|
|
|
|
|
+ node.put("isUserGrade", false);
|
|
|
|
|
+ UserGrade grade = currentGrade(user);
|
|
|
|
|
+ if (grade == null || !Integer.valueOf(1).equals(node.path("isEnableGrade").asInt(0))) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal ratio = discountRatio(grade, node.get("isAloneGrade"), node.get("aloneGradeEquity"), grade.getGradeId());
|
|
|
|
|
+ if (ratio == null || ratio.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ node.put("isUserGrade", true);
|
|
|
|
|
+ node.put("goodsPriceMin", discountPrice(node.path("goodsPriceMin").asText(null), ratio));
|
|
|
|
|
+ node.put("goodsPriceMax", discountPrice(node.path("goodsPriceMax").asText(null), ratio));
|
|
|
|
|
+ JsonNode skuList = node.get("skuList");
|
|
|
|
|
+ if (skuList instanceof ArrayNode array) {
|
|
|
|
|
+ for (JsonNode sku : array) {
|
|
|
|
|
+ if (sku instanceof ObjectNode skuNode) {
|
|
|
|
|
+ skuNode.put("goodsPrice", discountPrice(skuNode.path("goodsPrice").asText(null), ratio));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applySkuGrade(ObjectNode skuNode, User user) {
|
|
|
|
|
+ UserGrade grade = currentGrade(user);
|
|
|
|
|
+ if (grade == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // SKU 单独查询时没有商品折扣配置,按等级默认折扣处理(PHP 传 goodsInfo 时同样读取商品配置)
|
|
|
|
|
+ BigDecimal ratio = discountRatio(grade, null, null, grade.getGradeId());
|
|
|
|
|
+ if (ratio == null || ratio.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ skuNode.put("goodsPrice", discountPrice(skuNode.path("goodsPrice").asText(null), ratio));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private UserGrade currentGrade(User user) {
|
|
|
|
|
+ if (user == null || user.getGradeId() == null || user.getGradeId() == 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ UserGrade grade = userGradeMapper.selectById(user.getGradeId());
|
|
|
|
|
+ if (grade == null || Integer.valueOf(1).equals(grade.getIsDelete())
|
|
|
|
|
+ || !Integer.valueOf(1).equals(grade.getStatus())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return grade;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 折扣比例:优先商品单独配置(按等级 ID),否则取等级权益 discount */
|
|
|
|
|
+ private BigDecimal discountRatio(UserGrade grade, JsonNode isAloneGrade, JsonNode aloneEquity, int gradeId) {
|
|
|
|
|
+ Object equityJson = PhpStr.jsonDecodeArray(grade.getEquity());
|
|
|
|
|
+ BigDecimal ratio = equityJson instanceof Map<?, ?> equity ? toDecimal(equity.get("discount")) : null;
|
|
|
|
|
+ boolean alone = isAloneGrade != null && isAloneGrade.asInt(0) == 1;
|
|
|
|
|
+ if (alone && aloneEquity instanceof ObjectNode equityNode && equityNode.has(String.valueOf(gradeId))) {
|
|
|
|
|
+ BigDecimal aloneRatio = toDecimal(equityNode.get(String.valueOf(gradeId)).asText(null));
|
|
|
|
|
+ if (aloneRatio != null) {
|
|
|
|
|
+ ratio = aloneRatio;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return ratio;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 对齐 PHP getDiscountPrice:round(price * discount / 10, 2),出参两位小数字符串 */
|
|
|
|
|
+ private static String discountPrice(String price, BigDecimal ratio) {
|
|
|
|
|
+ if (price == null || price.isBlank()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ BigDecimal value = new BigDecimal(price)
|
|
|
|
|
+ .multiply(ratio.divide(BigDecimal.TEN, 6, RoundingMode.HALF_UP))
|
|
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ return value.toPlainString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static BigDecimal toDecimal(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(String.valueOf(value));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void applySort(QueryWrapper<Goods> qw, String sortType, boolean sortPrice) {
|
|
|
|
|
+ switch (sortType) {
|
|
|
|
|
+ case "sales" -> qw.orderByDesc("(sales_initial + sales_actual)");
|
|
|
|
|
+ case "price" -> {
|
|
|
|
|
+ if (sortPrice) {
|
|
|
|
|
+ qw.orderByDesc("goods_price_max");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ qw.orderByAsc("goods_price_min");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ default -> qw.orderByAsc("sort");
|
|
|
|
|
+ }
|
|
|
|
|
+ qw.orderByDesc("goods_id");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String blankToNull(String value) {
|
|
|
|
|
+ return value == null || value.isBlank() ? null : value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String strVal(Object value, String defaultValue) {
|
|
|
|
|
+ return value == null || String.valueOf(value).isBlank() ? defaultValue : String.valueOf(value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String strOrNull(Object value) {
|
|
|
|
|
+ return value == null || String.valueOf(value).isBlank() ? null : String.valueOf(value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean boolVal(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ String s = String.valueOf(value);
|
|
|
|
|
+ return "true".equalsIgnoreCase(s) || "1".equals(s) || "on".equalsIgnoreCase(s);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Integer intOrNull(Object value) {
|
|
|
|
|
+ if (value == null || String.valueOf(value).isBlank()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return (int) Double.parseDouble(String.valueOf(value));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int intVal(Object value, int defaultValue) {
|
|
|
|
|
+ Integer v = intOrNull(value);
|
|
|
|
|
+ return v == null || v <= 0 ? defaultValue : v;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|