refactor(sdk)!: CMS→svc-hall 出站改走 YschOpenCall 命名目标 + 适配 SDK 领域分包
build / build-push (push) Successful in 1m36s
build / build-push (push) Successful in 1m36s
- SvcHallClient:由手搓 IDHttp + yh.svchall.base-url 拼 URL 改为 YschOpenCall 命名目标 svchall(服务目录 yh.open.services.svchall)。原形态对带 context-path 的 svc-hall 必 401(共享拦截器只剥 yh.open.base-path=/userbase);per-target base-path 与提供方 ctx 对齐后签名天然一致。目录未配时回落 legacy 键;两者皆缺则构造期 fail-fast。 - SvcItemRemoteQuery:新增 businessAbsent 判定,统一两条出站路径的「业务 code=2 → 404 哨兵/空集,其余上抛」语义(目录路径抛 DException,legacy 抛 DHttpException)。 - import 迁移:YschBaseOpenApi → openapi.base.*(SDK 领域分包)。 - application.yml:新增 yh.open.services.svchall,删 yh.svchall.base-url。 - 测试:新增 SvcHallClientTest(8 例:目录目标/参数映射/空参不下发/fire-and-forget/ 回落/fail-fast);SvcItemRemoteQueryTest 补目录路径错误语义 4 例。104 测试全绿。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package com.yunhai.cms.service.impl;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.yunhai.cms.model.vo.SchoolVo;
|
||||
import com.yunhai.cms.model.vo.SiteVo;
|
||||
import com.yunhai.cms.repository.CmsArticleRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
|
||||
@@ -1,76 +1,111 @@
|
||||
package com.yunhai.cms.svcitem.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.utils.http.IDHttp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 办事大厅开放接口客户端(V2.0 Part C.3):经共享 IDHttp(出站自动签名,yh.app-auth 身份
|
||||
* appKey=100005)调 svc-hall 的 /oapi/svc/**;办事大厅侧 RequireAppSignFilter 验签,
|
||||
* 其 yh.app-auth.trusted-apps 须包含 CMS 的 appKey(运维项)。
|
||||
* 信封 {code,msg,data} 由 IDHttp 解包(code!=0 抛业务异常,即办事大厅 code=2 语义在此转异常)。
|
||||
* 办事大厅开放接口客户端(V2.0 Part C.3):经 SDK {@link YschOpenCall} 命名目标
|
||||
* {@code svchall}(服务目录 {@code yh.open.services.svchall})调 svc-hall {@code /oapi/svc/**};
|
||||
* 出站签名/信封解包/错误映射全部由 SDK 承载——本应用零专属地址配置
|
||||
* (《子应用配置优化方案》§4.2)。<br><br>
|
||||
* <b>为何不能再用共享 {@code IDHttp}</b>:svc-hall 带 context-path {@code /svc-hall},而共享
|
||||
* 拦截器只剥 {@code yh.open.base-path}(本应用 {@code /userbase})——原手工拼的 URL 会被签成
|
||||
* {@code /svc-hall/oapi/...},服务端剥掉 ctx 后两侧 target 不一致,<b>必然 401</b>。
|
||||
* {@link YschOpenCall} 的 per-target base-path 与提供方 context-path 对齐(同 svc-hall
|
||||
* {@code CmsSiteDirectoryImpl} 的既有范式)。<br><br>
|
||||
* <b>错误语义</b>:业务失败信封 {@code code!=0} → {@code DException}(携带 OpenApiErrorCode,
|
||||
* 办事大厅"不存在/未发布"= code 2);传输/HTTP 失败 → {@code OpenApiException};401/403 →
|
||||
* {@code DException("验签失败")}。降级判定见 {@code SvcItemRemoteQuery}。<br><br>
|
||||
* 目录未显式配置时回落 legacy {@code yh.svchall.base-url}(迁移期兼容);两者皆无则构造期
|
||||
* fail-fast——不给"沉默连不上 localhost"的假象。
|
||||
*/
|
||||
@Component
|
||||
public class SvcHallClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SvcHallClient.class);
|
||||
|
||||
private final IDHttp http;
|
||||
private final String baseUrl;
|
||||
/** 服务目录命名目标(svc-hall 未晋升 PlatformService 枚举,按名直调) */
|
||||
static final String DIRECTORY_KEY = "svchall";
|
||||
|
||||
public SvcHallClient(IDHttp http,
|
||||
@Value("${yh.svchall.base-url:http://localhost:8086}") String baseUrl) {
|
||||
private static final String LEGACY_KEY = "yh.svchall.base-url";
|
||||
|
||||
private final YschOpenCall openCall;
|
||||
private final IDHttp http;
|
||||
private final String legacyBaseUrl;
|
||||
private final boolean directoryResolved;
|
||||
|
||||
public SvcHallClient(YschOpenCall openCall,
|
||||
IDHttp http,
|
||||
@Value("${yh.svchall.base-url:}") String baseUrl) {
|
||||
this.openCall = openCall;
|
||||
this.http = http;
|
||||
this.baseUrl = baseUrl == null ? "" : trimTrailing(baseUrl);
|
||||
this.directoryResolved = openCall != null && openCall.hasTarget(DIRECTORY_KEY);
|
||||
this.legacyBaseUrl = trimTrailing(baseUrl == null ? "" : baseUrl.trim());
|
||||
if (!directoryResolved && legacyBaseUrl.isEmpty()) {
|
||||
throw new IllegalStateException("办事大厅调用地址未配置:请配置服务目录 yh.open.services."
|
||||
+ DIRECTORY_KEY + "(推荐,容器直连完整 URL)或回落键 " + LEGACY_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否走服务目录命名目标(配置自检用) */
|
||||
public boolean directoryResolved() {
|
||||
return directoryResolved;
|
||||
}
|
||||
|
||||
/** GET /oapi/svc/items → data(分页 + 三维筛选 + 关键词) */
|
||||
public JsonNode items(String siteCode, String objectKey, String categoryKey, Integer deptGroupId,
|
||||
String keyword, Integer page, Integer pageSize) {
|
||||
StringBuilder url = new StringBuilder(baseUrl + "/oapi/svc/items?siteCode=" + encode(siteCode));
|
||||
append(url, "objectKey", objectKey);
|
||||
append(url, "categoryKey", categoryKey);
|
||||
append(url, "keyword", keyword);
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
put(query, "siteCode", siteCode);
|
||||
put(query, "objectKey", objectKey);
|
||||
put(query, "categoryKey", categoryKey);
|
||||
put(query, "keyword", keyword);
|
||||
if (deptGroupId != null) {
|
||||
append(url, "deptGroupId", String.valueOf(deptGroupId));
|
||||
query.put("deptGroupId", String.valueOf(deptGroupId));
|
||||
}
|
||||
if (page != null) {
|
||||
append(url, "page", String.valueOf(page));
|
||||
query.put("page", String.valueOf(page));
|
||||
}
|
||||
if (pageSize != null) {
|
||||
append(url, "pageSize", String.valueOf(pageSize));
|
||||
query.put("pageSize", String.valueOf(pageSize));
|
||||
}
|
||||
return http.get(url.toString(), JsonNode.class);
|
||||
return get("/oapi/svc/items", query);
|
||||
}
|
||||
|
||||
/** GET /oapi/svc/filters → data */
|
||||
public JsonNode filters(String siteCode) {
|
||||
return http.get(baseUrl + "/oapi/svc/filters?siteCode=" + encode(siteCode), JsonNode.class);
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
put(query, "siteCode", siteCode);
|
||||
return get("/oapi/svc/filters", query);
|
||||
}
|
||||
|
||||
/** GET /oapi/svc/item/detail → data(itemId 或 itemCode 二选一) */
|
||||
public JsonNode detail(String siteCode, Integer itemId, String itemCode) {
|
||||
StringBuilder url = new StringBuilder(baseUrl + "/oapi/svc/item/detail?siteCode=" + encode(siteCode));
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
put(query, "siteCode", siteCode);
|
||||
if (itemId != null) {
|
||||
append(url, "itemId", String.valueOf(itemId));
|
||||
query.put("itemId", String.valueOf(itemId));
|
||||
}
|
||||
if (itemCode != null) {
|
||||
append(url, "itemCode", itemCode);
|
||||
}
|
||||
return http.get(url.toString(), JsonNode.class);
|
||||
put(query, "itemCode", itemCode);
|
||||
return get("/oapi/svc/item/detail", query);
|
||||
}
|
||||
|
||||
/** POST /oapi/svc/item/view(siteCode+itemCode 口径;fire-and-forget,失败仅 warn) */
|
||||
public void view(String siteCode, String itemCode) {
|
||||
try {
|
||||
http.post(baseUrl + "/oapi/svc/item/view?siteCode=" + encode(siteCode)
|
||||
+ "&itemCode=" + encode(itemCode), null, JsonNode.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[cms] 办事大厅浏览量累加失败 siteCode={} itemCode={}:{}", siteCode, itemCode, e.getMessage());
|
||||
}
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
put(query, "siteCode", siteCode);
|
||||
put(query, "itemCode", itemCode);
|
||||
safeView(query);
|
||||
}
|
||||
|
||||
/** POST /oapi/svc/item/view(itemId 直取口径;fire-and-forget,失败仅 warn) */
|
||||
@@ -78,33 +113,69 @@ public class SvcHallClient {
|
||||
if (itemId == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("itemId", String.valueOf(itemId));
|
||||
safeView(query);
|
||||
}
|
||||
|
||||
private void safeView(Map<String, String> query) {
|
||||
try {
|
||||
http.post(baseUrl + "/oapi/svc/item/view?itemId=" + itemId, null, JsonNode.class);
|
||||
if (directoryResolved) {
|
||||
openCall.post(DIRECTORY_KEY, "/oapi/svc/item/view", query, null, JsonNode.class);
|
||||
} else {
|
||||
http.post(legacyUrl("/oapi/svc/item/view", query), null, JsonNode.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[cms] 办事大厅浏览量累加失败 itemId={}:{}", itemId, e.getMessage());
|
||||
log.warn("[cms] 办事大厅浏览量累加失败 {}:{}", query, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /oapi/svc/item/attachments(itemId 直取;fileUrl 已绝对化) */
|
||||
public JsonNode attachments(Integer itemId) {
|
||||
return http.get(baseUrl + "/oapi/svc/item/attachments?itemId=" + itemId, JsonNode.class);
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("itemId", String.valueOf(itemId));
|
||||
return get("/oapi/svc/item/attachments", query);
|
||||
}
|
||||
|
||||
/** GET /oapi/svc/item/related(itemId 直取:同类别已发布 Top N) */
|
||||
public JsonNode related(Integer itemId, Integer limit) {
|
||||
return http.get(baseUrl + "/oapi/svc/item/related?itemId=" + itemId
|
||||
+ (limit != null ? "&limit=" + limit : ""), JsonNode.class);
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("itemId", String.valueOf(itemId));
|
||||
if (limit != null) {
|
||||
query.put("limit", String.valueOf(limit));
|
||||
}
|
||||
return get("/oapi/svc/item/related", query);
|
||||
}
|
||||
|
||||
private static void append(StringBuilder url, String key, String value) {
|
||||
// ===== 出站(目录优先,legacy 回落)=====
|
||||
|
||||
private JsonNode get(String path, Map<String, String> query) {
|
||||
if (directoryResolved) {
|
||||
return openCall.get(DIRECTORY_KEY, path, query, JsonNode.class);
|
||||
}
|
||||
return http.get(legacyUrl(path, query), JsonNode.class);
|
||||
}
|
||||
|
||||
/** legacy 形态:base-url + path + query(与原手工拼接的语义一致,保住既有调用方预期) */
|
||||
private String legacyUrl(String path, Map<String, String> query) {
|
||||
StringBuilder url = new StringBuilder(legacyBaseUrl).append(path);
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> e : query.entrySet()) {
|
||||
url.append(first ? '?' : '&').append(e.getKey()).append('=').append(encode(e.getValue()));
|
||||
first = false;
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private static void put(Map<String, String> query, String key, String value) {
|
||||
if (value != null && !value.isEmpty()) {
|
||||
url.append("&").append(key).append("=").append(encode(value));
|
||||
query.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String encode(String value) {
|
||||
try {
|
||||
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8.name());
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
||||
} catch (Exception e) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.yunhai.cms.svcitem.model.vo.SvcItemAttachmentVo;
|
||||
import com.yunhai.cms.svcitem.service.SvcItemQuery;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.http.DHttpException;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.platform.utils.exception.IErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -110,6 +112,15 @@ public class SvcItemRemoteQuery implements SvcItemQuery {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
// 目录路径(YschOpenCall)下的同义语义:业务失败信封 code!=0 → DException(携带
|
||||
// OpenApiErrorCode,办事大厅"不存在/未发布"= 2);传输/HTTP 失败 → OpenApiException。
|
||||
// 降级口径与 legacy 分支逐字一致,见上方注释。
|
||||
if (businessAbsent(e)) {
|
||||
log.info("[cms] 办事大厅详情未取到 itemId={}:{}", itemId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return toPortalItem(row);
|
||||
}
|
||||
@@ -131,6 +142,11 @@ public class SvcItemRemoteQuery implements SvcItemQuery {
|
||||
return result;
|
||||
}
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
if (businessAbsent(e)) {
|
||||
return result;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
for (JsonNode rel : row.isArray() ? row : new ArrayList<JsonNode>()) {
|
||||
PortalServiceItem vo = new PortalServiceItem();
|
||||
@@ -159,6 +175,11 @@ public class SvcItemRemoteQuery implements SvcItemQuery {
|
||||
return result;
|
||||
}
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
if (businessAbsent(e)) {
|
||||
return result;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
for (JsonNode att : row.isArray() ? row : new ArrayList<JsonNode>()) {
|
||||
SvcItemAttachmentVo vo = new SvcItemAttachmentVo();
|
||||
@@ -179,6 +200,22 @@ public class SvcItemRemoteQuery implements SvcItemQuery {
|
||||
|
||||
// ===== 装配 =====
|
||||
|
||||
/**
|
||||
* "业务性缺失"判定(办事大厅 code=2 = 不存在/未发布)。两条出站路径的异常载体不同,
|
||||
* 语义必须一致:legacy {@code IDHttp} → {@link DHttpException#getCode()} 为 int;
|
||||
* 服务目录 {@code YschOpenCall} → {@link DException} 携带 {@code OpenApiErrorCode}。
|
||||
*/
|
||||
private static boolean businessAbsent(RuntimeException e) {
|
||||
if (e instanceof DHttpException) {
|
||||
return ((DHttpException) e).getCode() == 2;
|
||||
}
|
||||
if (e instanceof DException) {
|
||||
IErrorCode code = ((DException) e).getCode();
|
||||
return code != null && code.getCode() == 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PortalServiceItem toPortalItem(JsonNode row) {
|
||||
PortalServiceItem vo = new PortalServiceItem();
|
||||
vo.setId(intVal(row, "id"));
|
||||
|
||||
Reference in New Issue
Block a user