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:
|
sph:
|
||||||
publish-enabled: true
|
publish-enabled: true
|
||||||
image-upload-url: https://api.weixin.qq.com/shop/ec/basics/img/upload
|
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-add-url: https://api.weixin.qq.com/channels/ec/product/add
|
||||||
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
||||||
fixed-leaf-cat-id-v2: 537901187
|
fixed-leaf-cat-id-v2: 537901187
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ external:
|
||||||
sph:
|
sph:
|
||||||
publish-enabled: false
|
publish-enabled: false
|
||||||
image-upload-url: https://api.weixin.qq.com/shop/ec/basics/img/upload
|
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-add-url: https://api.weixin.qq.com/channels/ec/product/add
|
||||||
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
product-update-url: https://api.weixin.qq.com/channels/ec/product/update
|
||||||
fixed-leaf-cat-id-v2: 537901187
|
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";
|
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}。
|
* 新类目树叶子类目 ID(本期固定);对应请求体 {@code cats_v2}。
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ public class ExternalSphPublishService {
|
||||||
String lane = update ? "PRODUCT_UPDATE" : "PRODUCT_ADD";
|
String lane = update ? "PRODUCT_UPDATE" : "PRODUCT_ADD";
|
||||||
try {
|
try {
|
||||||
HeadAndDetailWxImages imgs = buildWxImages(token, req);
|
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());
|
String url = (update ? props.getProductUpdateUrl() : props.getProductAddUrl());
|
||||||
if (!StringUtils.hasText(url)) {
|
if (!StringUtils.hasText(url)) {
|
||||||
return failVo(true, lane, "external.sph.product-add-url / product-update-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,
|
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();
|
JSONObject root = new JSONObject();
|
||||||
if (update) {
|
if (update) {
|
||||||
root.put("product_id", req.getSphProductId());
|
root.put("product_id", req.getSphProductId());
|
||||||
|
|
@ -237,7 +237,8 @@ public class ExternalSphPublishService {
|
||||||
root.put("deliver_acct_type", new JSONArray());
|
root.put("deliver_acct_type", new JSONArray());
|
||||||
JSONArray catsV2 = new JSONArray();
|
JSONArray catsV2 = new JSONArray();
|
||||||
JSONObject leaf = new JSONObject();
|
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);
|
catsV2.add(leaf);
|
||||||
root.put("cats_v2", catsV2);
|
root.put("cats_v2", catsV2);
|
||||||
JSONObject extra = new JSONObject();
|
JSONObject extra = new JSONObject();
|
||||||
|
|
@ -258,9 +259,9 @@ public class ExternalSphPublishService {
|
||||||
}
|
}
|
||||||
root.put("brand_id", NO_BRAND_ID);
|
root.put("brand_id", NO_BRAND_ID);
|
||||||
root.put("listing", props.getListingOnSave());
|
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() : "";
|
? 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()) {
|
if (skuArr.isEmpty()) {
|
||||||
throw new IllegalStateException("无可发品 SKU");
|
throw new IllegalStateException("无可发品 SKU");
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +269,128 @@ public class ExternalSphPublishService {
|
||||||
return root;
|
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,
|
private JSONArray buildSkus(List<OGoodsSku> skuRows, ExternalGoodsUpsertRequest req, String defaultThumbWxUrl,
|
||||||
String accessToken) {
|
String accessToken) {
|
||||||
JSONArray arr = new JSONArray();
|
JSONArray arr = new JSONArray();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue