diff --git a/api/erp-api/src/main/resources/application.yml b/api/erp-api/src/main/resources/application.yml index 81f598be..0df70592 100644 --- a/api/erp-api/src/main/resources/application.yml +++ b/api/erp-api/src/main/resources/application.yml @@ -43,6 +43,12 @@ external: auto-fetch-cat-rule: false # sku-overrides 为空时按类目规则 + spec.id 自动拼规格(与 SKU 条数一致时才继续调 goods.add) auto-resolve-spec-ids-when-sku-overrides-empty: true + # 满件折 two_pieces_discount,默认 99(99 折) + two-pieces-discount: 99 + # 拼单价 = 建议零售价 × (本值/100),默认 75=七五折 + group-buy-discount-percent: 75 + # 单买价(分)= 拼单价(分)+ 本值,默认 200=2 元 + single-buy-add-fen: 200 # categoryCode(请求体)-> 拼多多叶子类目 cat_id(与 maindata default-category-code=DEFAULT 对齐) category-map: DEFAULT: "15693" diff --git a/api/erp-api/src/main/resources/nacos/erp-api.yaml b/api/erp-api/src/main/resources/nacos/erp-api.yaml index 8eaf5b8e..9f760f67 100644 --- a/api/erp-api/src/main/resources/nacos/erp-api.yaml +++ b/api/erp-api/src/main/resources/nacos/erp-api.yaml @@ -46,6 +46,9 @@ external: gateway-url: https://gw-api.pinduoduo.com/api/router auto-fetch-cat-rule: false auto-resolve-spec-ids-when-sku-overrides-empty: true + two-pieces-discount: 99 + group-buy-discount-percent: 75 + single-buy-add-fen: 200 category-map: DEFAULT: "15693" cost-template-map: diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java index 45ec5e65..37377f09 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/ExternalPddProperties.java @@ -46,6 +46,21 @@ public class ExternalPddProperties { private boolean defaultIsRefundable = true; private boolean defaultSecondHand = false; + /** + * {@code pdd.goods.add} 根级 {@code two_pieces_discount}(满件折,默认 99 表示 99 折) + */ + private int twoPiecesDiscount = 99; + + /** + * 拼单价 = 建议零售价 ×(本值/100),默认 75 即七五折;为百分比整数,非分。 + */ + private int groupBuyDiscountPercent = 75; + + /** + * 单买价(分)= 拼单价(分)+ 本值;须为正(分),默认 200=2 元。 + */ + private long singleBuyAddFen = 200L; + /** * 与单次请求 {@code skus} 列表顺序一一对应(过滤掉 null 后的顺序), * 每条对应 PDD sku_list 一项的 spec_id_list 与 sku_properties。 diff --git a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java index e09e0813..09f41b35 100644 --- a/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java +++ b/service/src/main/java/cn/qihangerp/service/external/pdd/PddGoodsAddParamBuilder.java @@ -19,6 +19,8 @@ import java.util.Map; /** * 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的单根业务 JSON(字段名为 POP 下划线形式); * {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的表单顶层字段(与 POP curl / SDK 一致,不使用 {@code param_json})。 + *

拼多多售价:以 SKU {@code retailPrice}(档案建议零售价)为基准,拼单价=建议零售价×{@code external.pdd.group-buy-discount-percent}%, + * 单买价=拼单价+{@code external.pdd.single-buy-add-fen}(分);{@code market_price} 取各 SKU 建议零售价(分)之最大(必要时高于单买价以满足校验)。

* * @author guochengyu */ @@ -68,6 +70,7 @@ public class PddGoodsAddParamBuilder { root.put("is_refundable", props.isDefaultIsRefundable()); root.put("second_hand", props.isDefaultSecondHand()); root.put("shipment_limit_second", resolveShipmentSeconds(req, props)); + root.put("two_pieces_discount", props.getTwoPiecesDiscount()); List carousel = resolveCarousel(goods, req); if (carousel.isEmpty()) { @@ -82,7 +85,7 @@ public class PddGoodsAddParamBuilder { } root.put("detail_gallery", detail); - long marketFen = resolveMarketPriceFen(req, skuRows); + long maxSuggestedFen = maxSuggestedRetailFen(skuRows); if (StringUtils.hasText(goods.getOuterErpGoodsId())) { root.put("out_goods_id", goods.getOuterErpGoodsId()); @@ -97,14 +100,9 @@ public class PddGoodsAddParamBuilder { if (!StringUtils.hasText(ov.getSpecIdList())) { throw new IllegalStateException("external.pdd.sku-overrides[" + i + "].spec-id-list 不能为空"); } - long groupFen = yuanToFen(firstNonNull(row.getGroupPrice(), row.getRetailPrice())); - long singleFen = yuanToFen(firstNonNull(row.getRetailPrice(), row.getGroupPrice())); - if (groupFen <= 0) { - throw new IllegalStateException("SKU 团购价/拼团价无效(groupPrice/salePrice 换算为 0 分),outSkuId=" + row.getOuterErpSkuId()); - } - if (singleFen <= groupFen) { - singleFen = groupFen + 100; - } + long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId()); + long groupFen = gs[0]; + long singleFen = gs[1]; if (singleFen > maxSingleFen) { maxSingleFen = singleFen; } @@ -141,9 +139,7 @@ public class PddGoodsAddParamBuilder { sku.put("sku_properties", skuProps); skuList.add(sku); } - if (marketFen <= maxSingleFen) { - marketFen = maxSingleFen + 100; - } + long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen); root.put("market_price", marketFen); root.put("sku_list", skuList); root.put("goods_properties", new JSONArray()); @@ -188,6 +184,7 @@ public class PddGoodsAddParamBuilder { root.put("is_refundable", props.isDefaultIsRefundable()); root.put("second_hand", props.isDefaultSecondHand()); root.put("shipment_limit_second", resolveShipmentSeconds(req, props)); + root.put("two_pieces_discount", props.getTwoPiecesDiscount()); root.put("is_sku", false); List carousel = resolveCarousel(goods, req); @@ -202,18 +199,11 @@ public class PddGoodsAddParamBuilder { } root.put("detail_gallery", detail); - long groupFen = yuanToFen(firstNonNull(row.getGroupPrice(), row.getRetailPrice())); - long singleFen = yuanToFen(firstNonNull(row.getRetailPrice(), row.getGroupPrice())); - if (groupFen <= 0) { - throw new IllegalStateException("SKU 团购价/拼团价无效(换算为 0 分),outSkuId=" + row.getOuterErpSkuId()); - } - if (singleFen <= groupFen) { - singleFen = groupFen + 100; - } - long marketFen = resolveMarketPriceFen(req, skuRows); - if (marketFen <= singleFen) { - marketFen = singleFen + 100; - } + long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId()); + long groupFen = gs[0]; + long singleFen = gs[1]; + long maxSuggestedFen = maxSuggestedRetailFen(skuRows); + long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, singleFen); if (StringUtils.hasText(goods.getOuterErpGoodsId())) { root.put("out_goods_id", goods.getOuterErpGoodsId()); @@ -354,22 +344,59 @@ public class PddGoodsAddParamBuilder { } } - private static long resolveMarketPriceFen(ExternalGoodsUpsertRequest req, List skuRows) { - if (req.getMarketPrice() != null) { - return yuanToFen(req.getMarketPrice()); + /** + * 定价(market_price):默认取各 SKU 建议零售价(分)之最大;若低于单买价则抬到单买价+1 分以满足常见校验。 + */ + private static long resolvePddMarketPriceFen(long maxSuggestedRetailFen, long maxSingleBuyFen) { + if (maxSuggestedRetailFen <= 0) { + throw new IllegalStateException("无法生成 market_price:各 SKU 建议零售价(retailPrice)无效"); } - BigDecimal maxSingle = BigDecimal.ZERO; + long m = maxSuggestedRetailFen; + if (m <= maxSingleBuyFen) { + return maxSingleBuyFen + 1; + } + return m; + } + + /** 各 SKU 建议零售价(元)换算为分后的最大值 */ + private static long maxSuggestedRetailFen(List skuRows) { + long max = 0L; for (OGoodsSku s : skuRows) { - BigDecimal p = firstNonNull(s.getRetailPrice(), s.getGroupPrice()); - if (p != null && p.compareTo(maxSingle) > 0) { - maxSingle = p; + if (s == null) { + continue; + } + long fen = yuanToFen(s.getRetailPrice()); + if (fen > max) { + max = fen; } } - if (maxSingle.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalStateException("marketPrice 与各 SKU 价格均为空,无法生成参考价"); + return max; + } + + /** + * 拼单价(分)= 建议零售价 ×(groupBuyDiscountPercent/100),单买价(分)= 拼单价 + singleBuyAddFen;元→分 HALF_UP。 + */ + private static long[] groupAndSingleFenFromSuggestedRetail(BigDecimal suggestedRetailYuan, ExternalPddProperties props, + String outSkuIdForError) { + if (suggestedRetailYuan == null || suggestedRetailYuan.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalStateException("SKU 建议零售价(retailPrice)无效,无法按配置折算拼多多价格,outSkuId=" + outSkuIdForError); } - // 参考价略高于单买价(分) - return yuanToFen(maxSingle) + 100; + int pct = props.getGroupBuyDiscountPercent(); + if (pct <= 0 || pct > 100) { + throw new IllegalStateException("external.pdd.group-buy-discount-percent 须为 1~100,当前=" + pct); + } + BigDecimal groupYuan = suggestedRetailYuan.multiply(BigDecimal.valueOf(pct)) + .divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); + long groupFen = groupYuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue(); + if (groupFen <= 0) { + throw new IllegalStateException("折算后拼单价为 0 分,请检查建议零售价与折扣,outSkuId=" + outSkuIdForError); + } + long add = props.getSingleBuyAddFen(); + if (add <= 0) { + throw new IllegalStateException("external.pdd.single-buy-add-fen 须为正整数(分),当前=" + add); + } + long singleFen = groupFen + add; + return new long[]{groupFen, singleFen}; } private static long yuanToFen(BigDecimal yuan) { @@ -383,8 +410,4 @@ public class PddGoodsAddParamBuilder { private static String fenAsSkuPriceString(long fen) { return Long.toString(fen); } - - private static BigDecimal firstNonNull(BigDecimal a, BigDecimal b) { - return a != null ? a : b; - } }