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