diff --git a/src/main/java/com/yunhai/cms/controller/portal/PortalController.java b/src/main/java/com/yunhai/cms/controller/portal/PortalController.java index a6adc50..c8ca88d 100644 --- a/src/main/java/com/yunhai/cms/controller/portal/PortalController.java +++ b/src/main/java/com/yunhai/cms/controller/portal/PortalController.java @@ -19,6 +19,7 @@ import com.yunhai.cms.svcitem.model.vo.PortalServiceItem; import com.yunhai.cms.svcitem.model.vo.PortalServiceListData; import com.yunhai.cms.svcitem.service.SvcItemQuery; import com.yunhai.cms.utils.PortalUrlHelper; +import com.yunhai.cms.utils.SvcHallLinks; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -491,18 +492,15 @@ public class PortalController { // "我要办理"三态(§5.3):① ext_json.apply_url 手填外链(最高优先) // ② 未填且站点配了 svc_hall_base 且事项开通在线办理(onlineFlag=1 且绑流程,§3.7—— // 与 svc-hall 侧 submit 校验"未开通在线办理"口径对齐,线下/纯指南事项不再渲染按钮) + // ——深链带 from=指南页回跳地址,hall 据此显示"返回门户"按钮(T4 闭环) // ③ 其余 → 不注入(模板不渲染按钮) String applyUrl = item.getApplyUrl(); boolean onlineHandlable = item.getOnlineFlag() != null && item.getOnlineFlag() == 1 && StringUtils.hasText(item.getFlowGuid()); if ((applyUrl == null || applyUrl.trim().isEmpty()) && onlineHandlable && site.getSvcHallBase() != null && !site.getSvcHallBase().trim().isEmpty()) { - String base = site.getSvcHallBase().trim(); - while (base.endsWith("/")) { - base = base.substring(0, base.length() - 1); - } - applyUrl = base + "/#/apply?siteCode=" + urlEncodeValue(site.getCode()) - + "&itemCode=" + urlEncodeValue(item.getItemCode()); + applyUrl = SvcHallLinks.build(site.getSvcHallBase(), site.getCode(), + item.getItemCode(), url.serviceItem(itemId)); } if (applyUrl != null && !applyUrl.trim().isEmpty()) { model.addAttribute("applyUrl", applyUrl); @@ -687,14 +685,6 @@ public class PortalController { return trimmed.isEmpty() ? null : trimmed; } - private static String urlEncodeValue(String value) { - try { - return java.net.URLEncoder.encode(value == null ? "" : value, "UTF-8"); - } catch (java.io.UnsupportedEncodingException e) { - return value; - } - } - /** * 站点身份解析(W4):优先按门户 path(siteSnapshotByPath); diff --git a/src/main/java/com/yunhai/cms/utils/SvcHallLinks.java b/src/main/java/com/yunhai/cms/utils/SvcHallLinks.java new file mode 100644 index 0000000..3125167 --- /dev/null +++ b/src/main/java/com/yunhai/cms/utils/SvcHallLinks.java @@ -0,0 +1,44 @@ +package com.yunhai.cms.utils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * 「我要办理」深链构造器(T4 回跳闭环,2026-10-03 执行方案)。 + * 纯函数无状态:base 尾斜杠裁剪、参数 UTF-8 urlencode、from 缺省不追加。 + */ +public final class SvcHallLinks { + + private SvcHallLinks() { + } + + /** + * 「我要办理」深链(双端契约:hall/src/router/routes.ts 依赖此形态,改参数名两侧同步)。 + * + * @param base svc-hall hall 部署基地址(站点 svc_hall_base,尾斜杠容忍) + * @param siteCode 站点编码 + * @param itemCode 事项编码 + * @param fromUrl 指南页绝对 URL(回跳地址;null/空=不追加 from 段,hall 不显示返回门户) + */ + public static String build(String base, String siteCode, String itemCode, String fromUrl) { + String url = trimTail(base) + "/#/apply?siteCode=" + enc(siteCode) + + "&itemCode=" + enc(itemCode); + return fromUrl == null || fromUrl.isEmpty() ? url : url + "&from=" + enc(fromUrl); + } + + private static String trimTail(String url) { + String s = url == null ? "" : url.trim(); + while (s.endsWith("/")) { + s = s.substring(0, s.length() - 1); + } + return s; + } + + private static String enc(String value) { + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return value; + } + } +} diff --git a/src/test/java/com/yunhai/cms/portal/ServicePortalIT.java b/src/test/java/com/yunhai/cms/portal/ServicePortalIT.java index d6a619b..ab9ad51 100644 --- a/src/test/java/com/yunhai/cms/portal/ServicePortalIT.java +++ b/src/test/java/com/yunhai/cms/portal/ServicePortalIT.java @@ -122,12 +122,13 @@ class ServicePortalIT extends CmsIntegrationTestBase { siteRepository.saveAndFlush(site); clearCaches(); - // 在线办理事项(onlineFlag=1 且绑流程):自动生成发起页 URL + // 在线办理事项(onlineFlag=1 且绑流程):自动生成发起页 URL(T4 起带 from=指南页回跳地址) stub.seed(2001, siteId, "ON-1", "在线事项", "jw", null); assertThat(body(mockMvc.perform(get("/portal/demo1/sv/{id}", 2001)) .andExpect(status().isOk()).andReturn())) .contains("svc-apply-btn") - .contains("http://localhost:8087/#/apply?siteCode=demo1&itemCode=ON-1"); + .contains("http://localhost:8087/#/apply?siteCode=demo1&itemCode=ON-1") + .contains("&from="); // 线下事项(onlineFlag=0 未绑流程):即使站点配了基地址也不出按钮(§3.7 收紧) stub.seed(2002, siteId, "OFF-1", "线下事项", "jw", null); diff --git a/src/test/java/com/yunhai/cms/utils/SvcHallLinksTest.java b/src/test/java/com/yunhai/cms/utils/SvcHallLinksTest.java new file mode 100644 index 0000000..62287fe --- /dev/null +++ b/src/test/java/com/yunhai/cms/utils/SvcHallLinksTest.java @@ -0,0 +1,42 @@ +package com.yunhai.cms.utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 「我要办理」深链构造器单测(T4 回跳闭环,2026-10-03 执行方案)。 + * 覆盖:base 尾斜杠裁剪、siteCode/itemCode/from 的 UTF-8 urlencode、 + * from 为 null/空时不追加 from 段(手填外链状态①不受影响)。 + */ +@DisplayName("SvcHallLinks - 我要办理深链") +class SvcHallLinksTest { + + @Test + @DisplayName("四参形态:base 尾斜杠裁剪 + from 追加且编码") + void build_withFrom() { + assertThat(SvcHallLinks.build( + "http://h/svc-hall/hall/", "demo1", "SVC-001", "http://cms/portal/demo1/sv/12")) + .isEqualTo("http://h/svc-hall/hall/#/apply?siteCode=demo1&itemCode=SVC-001" + + "&from=http%3A%2F%2Fcms%2Fportal%2Fdemo1%2Fsv%2F12"); + } + + @Test + @DisplayName("from 为 null:不追加 from 段(状态①手填外链路径不经过,兜底保护)") + void build_withoutFrom() { + assertThat(SvcHallLinks.build("http://h/svc-hall/hall", "demo1", "SVC-001", null)) + .isEqualTo("http://h/svc-hall/hall/#/apply?siteCode=demo1&itemCode=SVC-001"); + assertThat(SvcHallLinks.build("http://h/svc-hall/hall", "demo1", "SVC-001", "")) + .isEqualTo("http://h/svc-hall/hall/#/apply?siteCode=demo1&itemCode=SVC-001"); + } + + @Test + @DisplayName("siteCode/itemCode 中文与保留字符过 UTF-8 urlencode") + void build_encodesUtf8() throws java.io.UnsupportedEncodingException { + String url = SvcHallLinks.build("http://h/hall", "演示站点", "事项/01", "http://cms/sv/1?a=1&b=2"); + assertThat(url).startsWith("http://h/hall/#/apply?siteCode=%E6%BC%94%E7%A4%BA%E7%AB%99%E7%82%B9"); + assertThat(url).contains("&itemCode=" + java.net.URLEncoder.encode("事项/01", "UTF-8")); + assertThat(url).contains("&from=http%3A%2F%2Fcms%2Fsv%2F1%3Fa%3D1%26b%3D2"); + } +}