feat(pdd): 发品价格按建议零售价折算(七五折+单买加分),market_price 与满折配置
Made-with: Cursor
This commit is contained in:
parent
e5061125a2
commit
89c239149c
|
|
@ -43,6 +43,12 @@ external:
|
||||||
auto-fetch-cat-rule: false
|
auto-fetch-cat-rule: false
|
||||||
# sku-overrides 为空时按类目规则 + spec.id 自动拼规格(与 SKU 条数一致时才继续调 goods.add)
|
# sku-overrides 为空时按类目规则 + spec.id 自动拼规格(与 SKU 条数一致时才继续调 goods.add)
|
||||||
auto-resolve-spec-ids-when-sku-overrides-empty: true
|
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 对齐)
|
# categoryCode(请求体)-> 拼多多叶子类目 cat_id(与 maindata default-category-code=DEFAULT 对齐)
|
||||||
category-map:
|
category-map:
|
||||||
DEFAULT: "15693"
|
DEFAULT: "15693"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ external:
|
||||||
gateway-url: https://gw-api.pinduoduo.com/api/router
|
gateway-url: https://gw-api.pinduoduo.com/api/router
|
||||||
auto-fetch-cat-rule: false
|
auto-fetch-cat-rule: false
|
||||||
auto-resolve-spec-ids-when-sku-overrides-empty: true
|
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:
|
category-map:
|
||||||
DEFAULT: "15693"
|
DEFAULT: "15693"
|
||||||
cost-template-map:
|
cost-template-map:
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,21 @@ public class ExternalPddProperties {
|
||||||
private boolean defaultIsRefundable = true;
|
private boolean defaultIsRefundable = true;
|
||||||
private boolean defaultSecondHand = false;
|
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 后的顺序),
|
* 与单次请求 {@code skus} 列表顺序一一对应(过滤掉 null 后的顺序),
|
||||||
* 每条对应 PDD sku_list 一项的 spec_id_list 与 sku_properties。
|
* 每条对应 PDD sku_list 一项的 spec_id_list 与 sku_properties。
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import java.util.Map;
|
||||||
/**
|
/**
|
||||||
* 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的<strong>单根业务 JSON</strong>(字段名为 POP 下划线形式);
|
* 将 Canonical 落库结果组装为 {@code pdd.goods.add} 的<strong>单根业务 JSON</strong>(字段名为 POP 下划线形式);
|
||||||
* {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的<strong>表单顶层</strong>字段(与 POP curl / SDK 一致,不使用 {@code param_json})。
|
* {@link cn.qihangerp.service.external.pdd.PddPopClient#invokeGoodsAdd} 会将其展开为官方网关要求的<strong>表单顶层</strong>字段(与 POP curl / SDK 一致,不使用 {@code param_json})。
|
||||||
|
* <p>拼多多售价:以 SKU {@code retailPrice}(档案建议零售价)为基准,拼单价=建议零售价×{@code external.pdd.group-buy-discount-percent}%,
|
||||||
|
* 单买价=拼单价+{@code external.pdd.single-buy-add-fen}(分);{@code market_price} 取各 SKU 建议零售价(分)之最大(必要时高于单买价以满足校验)。</p>
|
||||||
*
|
*
|
||||||
* @author guochengyu
|
* @author guochengyu
|
||||||
*/
|
*/
|
||||||
|
|
@ -68,6 +70,7 @@ public class PddGoodsAddParamBuilder {
|
||||||
root.put("is_refundable", props.isDefaultIsRefundable());
|
root.put("is_refundable", props.isDefaultIsRefundable());
|
||||||
root.put("second_hand", props.isDefaultSecondHand());
|
root.put("second_hand", props.isDefaultSecondHand());
|
||||||
root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
|
root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
|
||||||
|
root.put("two_pieces_discount", props.getTwoPiecesDiscount());
|
||||||
|
|
||||||
List<String> carousel = resolveCarousel(goods, req);
|
List<String> carousel = resolveCarousel(goods, req);
|
||||||
if (carousel.isEmpty()) {
|
if (carousel.isEmpty()) {
|
||||||
|
|
@ -82,7 +85,7 @@ public class PddGoodsAddParamBuilder {
|
||||||
}
|
}
|
||||||
root.put("detail_gallery", detail);
|
root.put("detail_gallery", detail);
|
||||||
|
|
||||||
long marketFen = resolveMarketPriceFen(req, skuRows);
|
long maxSuggestedFen = maxSuggestedRetailFen(skuRows);
|
||||||
|
|
||||||
if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
|
if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
|
||||||
root.put("out_goods_id", goods.getOuterErpGoodsId());
|
root.put("out_goods_id", goods.getOuterErpGoodsId());
|
||||||
|
|
@ -97,14 +100,9 @@ public class PddGoodsAddParamBuilder {
|
||||||
if (!StringUtils.hasText(ov.getSpecIdList())) {
|
if (!StringUtils.hasText(ov.getSpecIdList())) {
|
||||||
throw new IllegalStateException("external.pdd.sku-overrides[" + i + "].spec-id-list 不能为空");
|
throw new IllegalStateException("external.pdd.sku-overrides[" + i + "].spec-id-list 不能为空");
|
||||||
}
|
}
|
||||||
long groupFen = yuanToFen(firstNonNull(row.getGroupPrice(), row.getRetailPrice()));
|
long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId());
|
||||||
long singleFen = yuanToFen(firstNonNull(row.getRetailPrice(), row.getGroupPrice()));
|
long groupFen = gs[0];
|
||||||
if (groupFen <= 0) {
|
long singleFen = gs[1];
|
||||||
throw new IllegalStateException("SKU 团购价/拼团价无效(groupPrice/salePrice 换算为 0 分),outSkuId=" + row.getOuterErpSkuId());
|
|
||||||
}
|
|
||||||
if (singleFen <= groupFen) {
|
|
||||||
singleFen = groupFen + 100;
|
|
||||||
}
|
|
||||||
if (singleFen > maxSingleFen) {
|
if (singleFen > maxSingleFen) {
|
||||||
maxSingleFen = singleFen;
|
maxSingleFen = singleFen;
|
||||||
}
|
}
|
||||||
|
|
@ -141,9 +139,7 @@ public class PddGoodsAddParamBuilder {
|
||||||
sku.put("sku_properties", skuProps);
|
sku.put("sku_properties", skuProps);
|
||||||
skuList.add(sku);
|
skuList.add(sku);
|
||||||
}
|
}
|
||||||
if (marketFen <= maxSingleFen) {
|
long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, maxSingleFen);
|
||||||
marketFen = maxSingleFen + 100;
|
|
||||||
}
|
|
||||||
root.put("market_price", marketFen);
|
root.put("market_price", marketFen);
|
||||||
root.put("sku_list", skuList);
|
root.put("sku_list", skuList);
|
||||||
root.put("goods_properties", new JSONArray());
|
root.put("goods_properties", new JSONArray());
|
||||||
|
|
@ -188,6 +184,7 @@ public class PddGoodsAddParamBuilder {
|
||||||
root.put("is_refundable", props.isDefaultIsRefundable());
|
root.put("is_refundable", props.isDefaultIsRefundable());
|
||||||
root.put("second_hand", props.isDefaultSecondHand());
|
root.put("second_hand", props.isDefaultSecondHand());
|
||||||
root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
|
root.put("shipment_limit_second", resolveShipmentSeconds(req, props));
|
||||||
|
root.put("two_pieces_discount", props.getTwoPiecesDiscount());
|
||||||
root.put("is_sku", false);
|
root.put("is_sku", false);
|
||||||
|
|
||||||
List<String> carousel = resolveCarousel(goods, req);
|
List<String> carousel = resolveCarousel(goods, req);
|
||||||
|
|
@ -202,18 +199,11 @@ public class PddGoodsAddParamBuilder {
|
||||||
}
|
}
|
||||||
root.put("detail_gallery", detail);
|
root.put("detail_gallery", detail);
|
||||||
|
|
||||||
long groupFen = yuanToFen(firstNonNull(row.getGroupPrice(), row.getRetailPrice()));
|
long[] gs = groupAndSingleFenFromSuggestedRetail(row.getRetailPrice(), props, row.getOuterErpSkuId());
|
||||||
long singleFen = yuanToFen(firstNonNull(row.getRetailPrice(), row.getGroupPrice()));
|
long groupFen = gs[0];
|
||||||
if (groupFen <= 0) {
|
long singleFen = gs[1];
|
||||||
throw new IllegalStateException("SKU 团购价/拼团价无效(换算为 0 分),outSkuId=" + row.getOuterErpSkuId());
|
long maxSuggestedFen = maxSuggestedRetailFen(skuRows);
|
||||||
}
|
long marketFen = resolvePddMarketPriceFen(maxSuggestedFen, singleFen);
|
||||||
if (singleFen <= groupFen) {
|
|
||||||
singleFen = groupFen + 100;
|
|
||||||
}
|
|
||||||
long marketFen = resolveMarketPriceFen(req, skuRows);
|
|
||||||
if (marketFen <= singleFen) {
|
|
||||||
marketFen = singleFen + 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
|
if (StringUtils.hasText(goods.getOuterErpGoodsId())) {
|
||||||
root.put("out_goods_id", goods.getOuterErpGoodsId());
|
root.put("out_goods_id", goods.getOuterErpGoodsId());
|
||||||
|
|
@ -354,22 +344,59 @@ public class PddGoodsAddParamBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long resolveMarketPriceFen(ExternalGoodsUpsertRequest req, List<OGoodsSku> skuRows) {
|
/**
|
||||||
if (req.getMarketPrice() != null) {
|
* 定价(market_price):默认取各 SKU 建议零售价(分)之最大;若低于单买价则抬到单买价+1 分以满足常见校验。
|
||||||
return yuanToFen(req.getMarketPrice());
|
*/
|
||||||
|
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<OGoodsSku> skuRows) {
|
||||||
|
long max = 0L;
|
||||||
for (OGoodsSku s : skuRows) {
|
for (OGoodsSku s : skuRows) {
|
||||||
BigDecimal p = firstNonNull(s.getRetailPrice(), s.getGroupPrice());
|
if (s == null) {
|
||||||
if (p != null && p.compareTo(maxSingle) > 0) {
|
continue;
|
||||||
maxSingle = p;
|
}
|
||||||
|
long fen = yuanToFen(s.getRetailPrice());
|
||||||
|
if (fen > max) {
|
||||||
|
max = fen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (maxSingle.compareTo(BigDecimal.ZERO) <= 0) {
|
return max;
|
||||||
throw new IllegalStateException("marketPrice 与各 SKU 价格均为空,无法生成参考价");
|
|
||||||
}
|
}
|
||||||
// 参考价略高于单买价(分)
|
|
||||||
return yuanToFen(maxSingle) + 100;
|
/**
|
||||||
|
* 拼单价(分)= 建议零售价 ×(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);
|
||||||
|
}
|
||||||
|
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) {
|
private static long yuanToFen(BigDecimal yuan) {
|
||||||
|
|
@ -383,8 +410,4 @@ public class PddGoodsAddParamBuilder {
|
||||||
private static String fenAsSkuPriceString(long fen) {
|
private static String fenAsSkuPriceString(long fen) {
|
||||||
return Long.toString(fen);
|
return Long.toString(fen);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigDecimal firstNonNull(BigDecimal a, BigDecimal b) {
|
|
||||||
return a != null ? a : b;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue