feat(weixin): default to product classify for category selection
Use channels.ec.product.category.classify by default before add/update to reduce class mismatch failures, and remove the category-classify-enabled toggle from config. Made-with: Cursor
This commit is contained in:
parent
2b576a0979
commit
864fd47907
|
|
@ -81,6 +81,7 @@ external:
|
|||
sph:
|
||||
publish-enabled: true
|
||||
image-upload-url: https://api.weixin.qq.com/shop/ec/basics/img/upload
|
||||
product-classify-url: https://api.weixin.qq.com/channels/ec/product/category/classify
|
||||
product-add-url: https://api.weixin.qq.com/channels/ec/product/add
|
||||
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
||||
fixed-leaf-cat-id-v2: 537901187
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ external:
|
|||
sph:
|
||||
publish-enabled: false
|
||||
image-upload-url: https://api.weixin.qq.com/shop/ec/basics/img/upload
|
||||
product-classify-url: https://api.weixin.qq.com/channels/ec/product/category/classify
|
||||
product-add-url: https://api.weixin.qq.com/channels/ec/product/add
|
||||
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
||||
fixed-leaf-cat-id-v2: 537901187
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ public class ExternalSphProperties {
|
|||
*/
|
||||
private String productUpdateUrl = "https://api.weixin.qq.com/channels/ec/product/update";
|
||||
|
||||
/**
|
||||
* 类目推荐(product_classify):{@code channels.ec.product.category.classify}。
|
||||
*/
|
||||
private String productClassifyUrl = "https://api.weixin.qq.com/channels/ec/product/category/classify";
|
||||
|
||||
/**
|
||||
* 新类目树叶子类目 ID(本期固定);对应请求体 {@code cats_v2}。
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class ExternalSphPublishService {
|
|||
String lane = update ? "PRODUCT_UPDATE" : "PRODUCT_ADD";
|
||||
try {
|
||||
HeadAndDetailWxImages imgs = buildWxImages(token, req);
|
||||
JSONObject payload = buildProductPayload(goods, skus, req, imgs, update);
|
||||
JSONObject payload = buildProductPayload(goods, skus, req, imgs, update, token);
|
||||
String url = (update ? props.getProductUpdateUrl() : props.getProductAddUrl());
|
||||
if (!StringUtils.hasText(url)) {
|
||||
return failVo(true, lane, "external.sph.product-add-url / product-update-url 未配置");
|
||||
|
|
@ -216,7 +216,7 @@ public class ExternalSphPublishService {
|
|||
}
|
||||
|
||||
private JSONObject buildProductPayload(OGoods goods, List<OGoodsSku> skuRows, ExternalGoodsUpsertRequest req,
|
||||
HeadAndDetailWxImages imgs, boolean update) {
|
||||
HeadAndDetailWxImages imgs, boolean update, String accessToken) throws Exception {
|
||||
JSONObject root = new JSONObject();
|
||||
if (update) {
|
||||
root.put("product_id", req.getSphProductId());
|
||||
|
|
@ -237,7 +237,8 @@ public class ExternalSphPublishService {
|
|||
root.put("deliver_acct_type", new JSONArray());
|
||||
JSONArray catsV2 = new JSONArray();
|
||||
JSONObject leaf = new JSONObject();
|
||||
leaf.put("cat_id", String.valueOf(props.getFixedLeafCatIdV2()));
|
||||
long leafCatId = resolveLeafCatId(req, imgs.headImgUrls(), accessToken);
|
||||
leaf.put("cat_id", String.valueOf(leafCatId));
|
||||
catsV2.add(leaf);
|
||||
root.put("cats_v2", catsV2);
|
||||
JSONObject extra = new JSONObject();
|
||||
|
|
@ -258,9 +259,9 @@ public class ExternalSphPublishService {
|
|||
}
|
||||
root.put("brand_id", NO_BRAND_ID);
|
||||
root.put("listing", props.getListingOnSave());
|
||||
String accessToken = req.getSphPopAuth() != null && StringUtils.hasText(req.getSphPopAuth().getAccessToken())
|
||||
String token = req.getSphPopAuth() != null && StringUtils.hasText(req.getSphPopAuth().getAccessToken())
|
||||
? req.getSphPopAuth().getAccessToken().trim() : "";
|
||||
JSONArray skuArr = buildSkus(skuRows, req, imgs.headImgUrls().get(0), accessToken);
|
||||
JSONArray skuArr = buildSkus(skuRows, req, imgs.headImgUrls().get(0), token);
|
||||
if (skuArr.isEmpty()) {
|
||||
throw new IllegalStateException("无可发品 SKU");
|
||||
}
|
||||
|
|
@ -268,6 +269,128 @@ public class ExternalSphPublishService {
|
|||
return root;
|
||||
}
|
||||
|
||||
private long resolveLeafCatId(ExternalGoodsUpsertRequest req, List<String> headImgUrls, String accessToken) {
|
||||
long fixedCat = props.getFixedLeafCatIdV2();
|
||||
if (!StringUtils.hasText(props.getProductClassifyUrl())) {
|
||||
return fixedCat;
|
||||
}
|
||||
if (!StringUtils.hasText(accessToken) || CollectionUtils.isEmpty(headImgUrls)) {
|
||||
return fixedCat;
|
||||
}
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("req_type", 1);
|
||||
if (StringUtils.hasText(req.getTitle())) {
|
||||
body.put("title", req.getTitle().trim());
|
||||
}
|
||||
JSONArray head = new JSONArray();
|
||||
for (String u : headImgUrls) {
|
||||
if (StringUtils.hasText(u)) {
|
||||
head.add(u);
|
||||
}
|
||||
}
|
||||
if (head.isEmpty()) {
|
||||
return fixedCat;
|
||||
}
|
||||
body.put("head_imgs", head);
|
||||
String url = props.getProductClassifyUrl();
|
||||
String fullUrl = url + (url.contains("?") ? "&" : "?")
|
||||
+ "access_token=" + URLEncoder.encode(accessToken.trim(), StandardCharsets.UTF_8);
|
||||
String raw = httpClient.postJson(fullUrl, body.toJSONString(),
|
||||
Duration.ofSeconds(Math.max(5, props.getHttpReadTimeoutSeconds())));
|
||||
if (!WeixinShopEcHttpClient.isBizOk(raw)) {
|
||||
log.warn("[SPH] classify failed shopId={} outGoodsId={} err={} snippet={}",
|
||||
req.getShopId(), req.getOutGoodsId(), WeixinShopEcHttpClient.formatWxError(raw),
|
||||
WeixinShopEcLogSupport.snippet(raw, 500));
|
||||
return fixedCat;
|
||||
}
|
||||
Long recommended = parseRecommendedLeafCatId(raw);
|
||||
if (recommended == null || recommended <= 0) {
|
||||
return fixedCat;
|
||||
}
|
||||
log.info("[SPH] classify ok shopId={} outGoodsId={} recommendedLeafCatId={} fixedLeafCatId={}",
|
||||
req.getShopId(), req.getOutGoodsId(), recommended, fixedCat);
|
||||
return recommended;
|
||||
} catch (Exception e) {
|
||||
log.warn("[SPH] classify exception shopId={} outGoodsId={} err={}",
|
||||
req.getShopId(), req.getOutGoodsId(), e.getMessage());
|
||||
return fixedCat;
|
||||
}
|
||||
}
|
||||
|
||||
private static Long parseRecommendedLeafCatId(String raw) {
|
||||
JSONObject root = JSON.parseObject(raw);
|
||||
JSONArray categories = root.getJSONArray("categories");
|
||||
if (categories == null || categories.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Long fallback = null;
|
||||
for (Object obj : categories) {
|
||||
if (!(obj instanceof JSONObject item)) {
|
||||
continue;
|
||||
}
|
||||
JSONObject cats = item.getJSONObject("cats");
|
||||
Long leaf = extractLeafCatId(cats);
|
||||
if (leaf == null || leaf <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = leaf;
|
||||
}
|
||||
if (hasCategoryAuth(item) || hasCategoryAuth(cats)) {
|
||||
return leaf;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static boolean hasCategoryAuth(JSONObject o) {
|
||||
if (o == null) {
|
||||
return false;
|
||||
}
|
||||
if (o.containsKey("has_auth")) {
|
||||
return o.getBooleanValue("has_auth");
|
||||
}
|
||||
if (o.containsKey("has_permission")) {
|
||||
return o.getBooleanValue("has_permission");
|
||||
}
|
||||
if (o.containsKey("can_publish")) {
|
||||
return o.getBooleanValue("can_publish");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Long extractLeafCatId(JSONObject cats) {
|
||||
if (cats == null) {
|
||||
return null;
|
||||
}
|
||||
JSONArray catInfo = cats.getJSONArray("cat_info");
|
||||
if (catInfo == null || catInfo.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = catInfo.size() - 1; i >= 0; i--) {
|
||||
Object c = catInfo.get(i);
|
||||
if (c instanceof JSONObject cObj) {
|
||||
String catId = cObj.getString("cat_id");
|
||||
if (StringUtils.hasText(catId)) {
|
||||
try {
|
||||
return Long.parseLong(catId.trim());
|
||||
} catch (NumberFormatException ignore) {
|
||||
// ignore and continue
|
||||
}
|
||||
}
|
||||
if (cObj.containsKey("cat_id")) {
|
||||
try {
|
||||
return cObj.getLong("cat_id");
|
||||
} catch (Exception ignore) {
|
||||
// ignore and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JSONArray buildSkus(List<OGoodsSku> skuRows, ExternalGoodsUpsertRequest req, String defaultThumbWxUrl,
|
||||
String accessToken) {
|
||||
JSONArray arr = new JSONArray();
|
||||
|
|
|
|||
Loading…
Reference in New Issue